1use std::{collections::HashMap, sync::LazyLock};
2
3use globset::{Glob, GlobSet, GlobSetBuilder};
4use gobject_ast::model::{
5 Expression, FileModel, FunctionDefItem, SourceLocation, Statement, TypeInfo,
6};
7
8use crate::{
9 ast_context::AstContext,
10 config::Config,
11 rules::{ConfigOption, Rule, Violation},
12};
13
14const AUTOFREE_ALLOCATIONS: &[&str] = &[
15 "g_strdup",
16 "g_strndup",
17 "g_strdup_printf",
18 "g_strdup_vprintf",
19 "g_malloc",
20 "g_malloc0",
21 "g_realloc",
22 "g_try_malloc",
23 "g_try_malloc0",
24 "g_memdup",
25 "g_new",
26 "g_new0",
27];
28
29pub struct UseAutoCleanup;
30
31impl Rule for UseAutoCleanup {
32 fn name(&self) -> &'static str {
33 "use_auto_cleanup"
34 }
35
36 fn description(&self) -> &'static str {
37 "Suggest g_autoptr/g_autofree/g_autolist instead of manual cleanup"
38 }
39
40 fn long_description(&self) -> Option<&'static str> {
41 Some(include_str!("../../docs/rules/use_auto_cleanup.md"))
42 }
43
44 fn category(&self) -> crate::rules::Category {
45 crate::rules::Category::Complexity
46 }
47
48 fn config_options(&self) -> &'static [ConfigOption] {
49 static OPTIONS: LazyLock<Vec<ConfigOption>> = LazyLock::new(|| {
50 vec![ConfigOption {
51 name: "ignore_types",
52 option_type: "array<string>",
53 default_value: "[]",
54 example_value: "[\"cairo_*\", \"Pango*\", \"RsvgHandle\"]",
55 description: "List of glob patterns for types to ignore",
56 }]
57 });
58
59 &OPTIONS
60 }
61
62 fn min_glib_version(&self) -> Option<(u32, u32)> {
63 Some((2, 44))
64 }
65
66 fn requires_auto_cleanup(&self) -> bool {
67 true
68 }
69
70 fn check_all(
71 &self,
72 ast_context: &AstContext,
73 config: &Config,
74 violations: &mut Vec<Violation>,
75 ) {
76 let ignore_types = self.build_ignore_types_matcher(config);
77 for (path, file) in ast_context.iter_c_files() {
78 for func in file.iter_function_definitions() {
79 self.check_function(func, path, violations, &ignore_types);
80 self.check_goto_cleanup(func, file, violations);
81 }
82 }
83 }
84}
85
86impl UseAutoCleanup {
87 fn build_ignore_types_matcher(&self, config: &Config) -> GlobSet {
88 let mut builder = GlobSetBuilder::new();
89
90 for s in config.get_string_list(self.name(), "ignore_types") {
91 if let Ok(glob) = Glob::new(&s) {
92 builder.add(glob);
93 }
94 }
95
96 builder.build().unwrap_or_else(|_| GlobSet::empty())
97 }
98
99 fn check_function(
100 &self,
101 func: &FunctionDefItem,
102 file_path: &std::path::Path,
103 violations: &mut Vec<Violation>,
104 ignore_types: &GlobSet,
105 ) {
106 let local_vars: HashMap<&str, (&TypeInfo, &SourceLocation)> = func
107 .iter_local_declarations()
108 .filter(|d| {
109 !d.type_info.uses_auto_cleanup()
110 && d.type_info.pointer_depth == 1
111 && d.is_simple_identifier()
112 })
113 .map(|d| (d.name.as_str(), (&d.type_info, &d.location)))
114 .collect();
115
116 for (var_name, (type_info, location)) in &local_vars {
117 if let Some(suggestion) = self.suggest_auto_cleanup(func, var_name, type_info) {
118 if ignore_types.is_match(&type_info.base_type) {
119 continue;
120 }
121
122 violations.push(self.violation_at(file_path, location, suggestion));
123 }
124 }
125 }
126
127 fn suggest_auto_cleanup(
128 &self,
129 func: &FunctionDefItem,
130 var_name: &str,
131 type_info: &TypeInfo,
132 ) -> Option<String> {
133 let is_returned = func.is_var_returned(type_info);
134
135 if type_info.is_base_type("GError")
137 && func.is_var_passed_to_function(var_name, "g_error_free", 0)
138 {
139 return Some(format!(
140 "Consider using g_autoptr(GError) {} instead of manual g_error_free",
141 var_name
142 ));
143 }
144
145 if matches!(type_info.base_type.as_str(), "GList" | "GSList") {
147 let free_func = if type_info.base_type == "GList" {
148 "g_list_free_full"
149 } else {
150 "g_slist_free_full"
151 };
152
153 if func.is_var_passed_to_function(var_name, free_func, 0)
154 && !self.uses_basic_destructor(func, free_func)
155 && !is_returned
156 {
157 let (auto_type, base_type) = match type_info.base_type.as_str() {
158 "GList" => ("g_autolist", "g_list"),
159 _ => ("g_autoslist", "g_slist"),
160 };
161 return Some(format!(
162 "Consider using {auto_type} to avoid manual {base_type}_free_full cleanup",
163 ));
164 }
165 return None;
166 }
167
168 let is_freed_with_g_free = func.is_var_passed_to_function(var_name, "g_free", 0);
170 if is_freed_with_g_free {
171 let is_autofree_allocated = func.is_var_allocated_with(type_info, |call| {
172 call.function_name_str()
173 .is_some_and(|name| AUTOFREE_ALLOCATIONS.contains(&name))
174 });
175
176 if is_autofree_allocated && !is_returned {
177 return Some(format!(
178 "Consider using g_autofree {} to avoid manual g_free",
179 var_name
180 ));
181 }
182 return None;
183 }
184
185 if self.frees_array_keeping_data(func, var_name, type_info) {
188 return None;
189 }
190
191 let is_allocated = func.is_var_allocated(type_info);
193 let is_manually_freed = func.is_var_passed_to_cleanup(type_info);
194
195 if is_allocated && is_manually_freed && !is_returned {
196 return Some(format!(
197 "Consider using g_autoptr({}) {} to avoid manual cleanup",
198 type_info.base_type, var_name
199 ));
200 }
201
202 None
203 }
204
205 fn frees_array_keeping_data(
206 &self,
207 func: &FunctionDefItem,
208 var_name: &str,
209 type_info: &TypeInfo,
210 ) -> bool {
211 let free_func = match type_info.base_type.as_str() {
212 "GPtrArray" => "g_ptr_array_free",
213 "GArray" => "g_array_free",
214 _ => return false,
215 };
216
217 let calls = func.find_calls(&[free_func]);
218 for call in calls {
219 if call
220 .get_arg(0)
221 .is_some_and(|arg| matches!(arg, Expression::Identifier(id) if id.name == var_name))
222 && call.arguments.len() >= 2
223 && call.arguments[1].is_falsy()
224 {
225 return true;
226 }
227 }
228
229 false
230 }
231
232 fn uses_basic_destructor(&self, func: &FunctionDefItem, free_func: &str) -> bool {
233 let calls = func.find_calls(&[free_func]);
234
235 for call in calls {
236 if call.arguments.len() >= 2
237 && let Expression::Identifier(destructor) = call.arguments[1].as_ref()
238 && matches!(
239 destructor.name.as_str(),
240 "g_free" | "free" | "g_slice_free" | "g_slice_free1"
241 )
242 {
243 return true;
244 }
245 }
246
247 false
248 }
249
250 fn check_goto_cleanup(
251 &self,
252 func: &FunctionDefItem,
253 file: &FileModel,
254 violations: &mut Vec<Violation>,
255 ) {
256 let allocated_vars = self.find_allocated_variables(&func.body_statements);
257 let goto_labels = self.find_goto_labels(&func.body_statements);
258 let cleanup_labels = self.find_cleanup_labels(&func.body_statements);
259
260 for (var_name, (type_info, location)) in &allocated_vars {
261 for goto_label in &goto_labels {
262 if let Some(cleanup_vars) = cleanup_labels.get(goto_label)
263 && cleanup_vars.contains(*var_name)
264 {
265 violations.push(self.violation_at(
266 &file.path,
267 location,
268 format!(
269 "Consider using g_autoptr({}) {} and g_steal_pointer to avoid goto cleanup",
270 type_info.base_type, var_name
271 ),
272 ));
273 }
274 }
275 }
276 }
277
278 fn find_allocated_variables<'a>(
279 &self,
280 statements: &'a [Statement],
281 ) -> HashMap<&'a str, (&'a TypeInfo, &'a SourceLocation)> {
282 let mut result = HashMap::new();
283
284 let local_vars: HashMap<&str, (&TypeInfo, &SourceLocation)> = statements
285 .iter()
286 .flat_map(Statement::iter_declarations)
287 .filter(|d| {
288 !d.type_info.uses_auto_cleanup()
289 && d.type_info.is_pointer()
290 && d.is_simple_identifier()
291 })
292 .map(|d| (d.name.as_str(), (&d.type_info, &d.location)))
293 .collect();
294
295 self.collect_allocated_vars(statements, &local_vars, &mut result);
296
297 result
298 }
299
300 fn collect_allocated_vars<'a>(
301 &self,
302 statements: &'a [Statement],
303 local_vars: &HashMap<&str, (&'a TypeInfo, &'a SourceLocation)>,
304 result: &mut HashMap<&'a str, (&'a TypeInfo, &'a SourceLocation)>,
305 ) {
306 for stmt in statements {
307 stmt.walk(&mut |s| match s {
308 Statement::Declaration(decl) => {
309 if let Some(Expression::Call(call)) = &decl.initializer
310 && call.is_allocation_call()
311 && let Some((type_info, location)) = local_vars.get(decl.name.as_str())
312 {
313 result.insert(decl.name.as_str(), (*type_info, location));
314 }
315 }
316 Statement::Expression(expr_stmt) => {
317 if let Expression::Assignment(assign) = expr_stmt.as_ref()
318 && let Expression::Call(call) = &*assign.rhs
319 && call.is_allocation_call()
320 && let Expression::Identifier(id) = &*assign.lhs
321 && let Some((type_info, location)) = local_vars.get(id.name.as_str())
322 {
323 result.insert(id.name.as_str(), (*type_info, location));
324 }
325 }
326 _ => {}
327 });
328 }
329 }
330
331 fn find_goto_labels<'a>(
332 &self,
333 statements: &'a [Statement],
334 ) -> std::collections::HashSet<&'a str> {
335 let mut labels = std::collections::HashSet::new();
336 for stmt in statements {
337 stmt.walk(&mut |s| {
338 if let Statement::Goto(goto_stmt) = s {
339 labels.insert(goto_stmt.label.as_str());
340 }
341 });
342 }
343 labels
344 }
345
346 fn find_cleanup_labels<'a>(
347 &'a self,
348 statements: &'a [Statement],
349 ) -> HashMap<&'a str, std::collections::HashSet<&'a str>> {
350 let mut result = HashMap::new();
351
352 for stmt in statements {
353 stmt.walk(&mut |s| {
354 if let Statement::Labeled(labeled) = s {
355 let cleanup_vars = self.find_cleanup_calls(&labeled.statement);
356 if !cleanup_vars.is_empty() {
357 result.insert(labeled.label.as_str(), cleanup_vars);
358 }
359 }
360 });
361 }
362
363 result
364 }
365
366 fn find_cleanup_calls<'a>(&self, stmt: &'a Statement) -> std::collections::HashSet<&'a str> {
367 let mut cleanup_vars = std::collections::HashSet::new();
368 for call in stmt.iter_calls() {
369 if call.is_cleanup_call()
370 && let Some(arg_expr) = call.get_arg(0)
371 && let Some(var_name) = arg_expr.extract_variable_name()
372 {
373 cleanup_vars.insert(var_name);
374 }
375 }
376 cleanup_vars
377 }
378}