Skip to main content

gobject_linter/rules/
use_auto_cleanup.rs

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        // GError → g_autoptr(GError)
136        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        // GList/GSList → g_autolist/g_autoslist
146        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        // g_free'd with autofree-suitable allocation → g_autofree
169        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        // g_ptr_array_free(array, FALSE) / g_array_free(array, FALSE) return the
186        // element data -> Skip
187        if self.frees_array_keeping_data(func, var_name, type_info) {
188            return None;
189        }
190
191        // General case: allocated + manually freed + not returned → g_autoptr(Type)
192        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}