Skip to main content

gobject_linter/rules/
g_error_leak.rs

1use std::sync::LazyLock;
2
3use gobject_ast::model::{Expression, FileModel, FunctionDefItem, Statement, UnaryOp};
4
5use crate::{
6    ast_context::AstContext,
7    config::Config,
8    rules::{Category, ConfigOption, Rule, Violation},
9};
10
11pub struct GErrorLeak;
12
13impl Rule for GErrorLeak {
14    fn name(&self) -> &'static str {
15        "g_error_leak"
16    }
17
18    fn description(&self) -> &'static str {
19        "Check for GError variables that are neither freed nor propagated"
20    }
21
22    fn category(&self) -> Category {
23        Category::Correctness
24    }
25
26    fn config_options(&self) -> &'static [ConfigOption] {
27        static OPTIONS: LazyLock<Vec<ConfigOption>> = LazyLock::new(|| {
28            vec![
29                ConfigOption {
30                    name: "extra_noreturn_functions",
31                    option_type: "array<string>",
32                    default_value: "[]",
33                    example_value: "[\"my_app_abort\", \"test_fail\"]",
34                    description: "Additional function names that never return (terminate the program), suppressing leak warnings",
35                },
36                ConfigOption {
37                    name: "extra_propagation_functions",
38                    option_type: "array<string>",
39                    default_value: "[]",
40                    example_value: "[\"my_app_report_error\", \"dbus_reply_error\"]",
41                    description: "Additional function names that take ownership of the GError (propagation/transfer)",
42                },
43            ]
44        });
45        &OPTIONS
46    }
47
48    fn check_func_impl(
49        &self,
50        _ast_context: &AstContext,
51        config: &Config,
52        func: &FunctionDefItem,
53        file: &FileModel,
54        violations: &mut Vec<Violation>,
55    ) {
56        let extra_noreturn = config.get_string_list(self.name(), "extra_noreturn_functions");
57        let extra_propagation = config.get_string_list(self.name(), "extra_propagation_functions");
58
59        let mut gerror_vars = Vec::new();
60
61        for stmt in &func.body_statements {
62            for decl in stmt.iter_declarations() {
63                if decl.type_info.is_base_type("GError")
64                    && decl.type_info.is_pointer()
65                    && decl.initializer.as_ref().is_some_and(Expression::is_null)
66                {
67                    gerror_vars.push((decl.name.as_str(), &decl.location));
68                }
69            }
70        }
71
72        for (var_name, loc) in gerror_vars {
73            let is_used = is_error_used(&func.body_statements, var_name);
74
75            if !is_used {
76                continue;
77            }
78
79            let is_freed = is_error_freed(&func.body_statements, var_name);
80            let is_propagated =
81                is_error_propagated(&func.body_statements, var_name, &extra_propagation);
82            let has_noreturn = calls_noreturn_function(&func.body_statements, &extra_noreturn);
83
84            if !is_freed && !is_propagated && !has_noreturn {
85                violations.push(self.violation_at(
86                    &file.path,
87                    loc,
88                    format!(
89                        "GError variable '{}' may be leaked; it should be freed with g_error_free/g_clear_error or propagated with g_propagate_error/g_task_return_error/g_steal_pointer",
90                        var_name
91                    ),
92                ));
93            }
94        }
95    }
96}
97
98fn calls_noreturn_function(statements: &[Statement], extra: &[String]) -> bool {
99    const BUILTIN: &[&str] = &[
100        "g_error",
101        "g_assert",
102        "g_assert_not_reached",
103        "g_assert_no_error",
104        "g_return_if_fail",
105        "g_return_val_if_fail",
106        "exit",
107        "abort",
108        "_exit",
109    ];
110
111    for stmt in statements {
112        for call in stmt.iter_calls() {
113            if let Some(func_name) = call.function_name_str()
114                && (BUILTIN.contains(&func_name) || extra.iter().any(|e| e == func_name))
115            {
116                return true;
117            }
118        }
119    }
120    false
121}
122
123/// Check if the error variable is used (passed to functions as &error)
124fn is_error_used(statements: &[Statement], var_name: &str) -> bool {
125    for stmt in statements {
126        let mut found = false;
127        stmt.walk_expressions(&mut |expr| {
128            // Recursively walk ALL nested expressions
129            expr.walk(&mut |nested_expr| {
130                // Check for &error pattern (address-of operator)
131                if let Expression::Unary(unary) = nested_expr
132                    && unary.operator == UnaryOp::AddressOf
133                    && let Expression::Identifier(id) = &*unary.operand
134                    && id.name == var_name
135                {
136                    found = true;
137                }
138            });
139        });
140        if found {
141            return true;
142        }
143    }
144    false
145}
146
147/// Check if the error variable is freed (g_error_free or g_clear_error)
148fn is_error_freed(statements: &[Statement], var_name: &str) -> bool {
149    check_error_handled(statements, var_name, &["g_error_free", "g_clear_error"])
150}
151
152/// Check if the error variable is propagated (g_propagate_error,
153/// g_steal_pointer, g_task_return_error, etc.)
154fn is_error_propagated(statements: &[Statement], var_name: &str, extra: &[String]) -> bool {
155    // Check for known ownership-transfer functions
156    if check_error_handled(
157        statements,
158        var_name,
159        &[
160            "g_propagate_error",
161            "g_propagate_prefixed_error",
162            "g_steal_pointer",
163            "g_task_return_error",
164            "g_dbus_method_invocation_take_error",
165        ],
166    ) {
167        return true;
168    }
169
170    if !extra.is_empty() && check_error_handled(statements, var_name, extra) {
171        return true;
172    }
173
174    for stmt in statements {
175        for call in stmt.iter_calls() {
176            if let Some(func_name) = call.function_name_str()
177                && (func_name.contains("_terminate_") && func_name.contains("error")
178                    || func_name.ends_with("_set_error")
179                    || func_name.contains("_set_g_error"))
180            {
181                for arg in &call.arguments {
182                    if arg.contains_identifier(var_name) {
183                        return true;
184                    }
185                }
186            }
187        }
188    }
189
190    false
191}
192
193fn check_error_handled<S: AsRef<str>>(
194    statements: &[Statement],
195    var_name: &str,
196    functions: &[S],
197) -> bool {
198    for stmt in statements {
199        for call in stmt.iter_calls() {
200            if let Some(func_name) = call.function_name_str()
201                && functions.iter().any(|f| f.as_ref() == func_name)
202            {
203                for arg in &call.arguments {
204                    if arg.contains_identifier(var_name) {
205                        return true;
206                    }
207                }
208            }
209        }
210    }
211    false
212}