gobject_linter/rules/
g_error_leak.rs1use 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
123fn 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 expr.walk(&mut |nested_expr| {
130 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
147fn is_error_freed(statements: &[Statement], var_name: &str) -> bool {
149 check_error_handled(statements, var_name, &["g_error_free", "g_clear_error"])
150}
151
152fn is_error_propagated(statements: &[Statement], var_name: &str, extra: &[String]) -> bool {
155 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}