Skip to main content

gobject_linter/rules/
use_g_set_object.rs

1use gobject_ast::model::{
2    AssignmentOp, Expression, FileModel, FunctionDefItem, Statement, UnaryOp,
3};
4
5use crate::{
6    ast_context::AstContext,
7    config::Config,
8    rules::{Fix, Rule, Violation},
9};
10
11pub struct UseGSetObject;
12
13impl Rule for UseGSetObject {
14    fn name(&self) -> &'static str {
15        "use_g_set_object"
16    }
17
18    fn description(&self) -> &'static str {
19        "Suggest g_set_object() instead of manual g_clear_object and g_object_ref"
20    }
21
22    fn category(&self) -> crate::rules::Category {
23        crate::rules::Category::Complexity
24    }
25
26    fn fixable(&self) -> bool {
27        true
28    }
29
30    fn min_glib_version(&self) -> Option<(u32, u32)> {
31        Some((2, 44))
32    }
33
34    fn check_func_impl(
35        &self,
36        _ast_context: &AstContext,
37        config: &Config,
38        func: &FunctionDefItem,
39        file: &FileModel,
40        violations: &mut Vec<Violation>,
41    ) {
42        Statement::walk_pairs(&func.body_statements, &mut |s1, s2| {
43            self.try_clear_then_ref(s1, s2, file, config, violations);
44        });
45    }
46}
47
48impl UseGSetObject {
49    fn try_clear_then_ref(
50        &self,
51        s1: &Statement,
52        s2: &Statement,
53        file: &FileModel,
54        config: &Config,
55        violations: &mut Vec<Violation>,
56    ) -> bool {
57        // First statement: g_clear_object(&var) or g_object_unref(var)
58        let Some((var_name, needs_deref)) = self.extract_clear_or_unref_var(s1) else {
59            return false;
60        };
61
62        // Second statement: var = g_object_ref(...) or *var = g_object_ref(...)
63        let Some((assign_var, new_val)) = self.extract_object_ref_assignment(s2) else {
64            return false;
65        };
66
67        let matches = if needs_deref {
68            assign_var.len() == var_name.len() + 1
69                && assign_var.starts_with('*')
70                && assign_var[1..] == *var_name
71        } else {
72            assign_var == var_name
73        };
74        if !matches {
75            return false;
76        }
77
78        // g_set_object takes GObject**, so:
79        // - If var is GObject* (needs_deref=false), use &var
80        // - If var is GObject** (needs_deref=true), use var directly
81        let replacement = if needs_deref {
82            config
83                .style
84                .format_call_stmt("g_set_object", &[var_name, new_val])
85        } else {
86            config
87                .style
88                .format_addr_call_stmt("g_set_object", var_name, &[new_val])
89        };
90
91        // Use two separate fixes to preserve comments between statements
92        let s2_end = s2.location().find_semicolon_end();
93        let message = format!("Use {replacement} instead of g_clear_object and g_object_ref");
94        let fixes = vec![
95            // Delete the entire first line (g_clear_object/g_object_unref)
96            Fix::delete_line(s1.location()),
97            // Replace the second statement with g_set_object
98            Fix::new(s2.location().start_byte, s2_end, replacement),
99        ];
100
101        violations.push(self.violation_with_fixes_at(&file.path, s1.location(), message, fixes));
102        true
103    }
104
105    /// Extract variable from g_clear_object(&var)/g_clear_object(ptr) or
106    /// g_object_unref(var) Returns (var_name, needs_deref) where
107    /// needs_deref indicates if assignment should use *var
108    fn extract_clear_or_unref_var<'a>(&self, stmt: &'a Statement) -> Option<(&'a str, bool)> {
109        let Statement::Expression(expr_stmt) = stmt else {
110            return None;
111        };
112
113        let Expression::Call(call) = expr_stmt.as_ref() else {
114            return None;
115        };
116
117        if call.arguments.is_empty() {
118            return None;
119        }
120
121        if call.is_function("g_clear_object") {
122            // g_clear_object can take:
123            // 1. &var - then assignment is var = ...
124            // 2. ptr - then assignment is *ptr = ...
125            let first_arg = call.get_arg(0)?;
126            if let Expression::Unary(unary) = first_arg
127                && unary.operator == UnaryOp::AddressOf
128            {
129                // Case 1: g_clear_object(&var)
130                return Some((unary.operand.location().as_str()?, false));
131            } else {
132                // Case 2: g_clear_object(ptr) where ptr is GObject**
133                return Some((first_arg.location().as_str()?, true));
134            }
135        } else if call.is_function("g_object_unref") {
136            // g_object_unref(var) - assignment is var = ...
137            let first_arg = call.get_arg(0)?;
138            return Some((first_arg.location().as_str()?, false));
139        }
140
141        None
142    }
143
144    /// Extract (var, new_val) from var = g_object_ref(new_val)
145    fn extract_object_ref_assignment<'a>(&self, stmt: &'a Statement) -> Option<(&'a str, &'a str)> {
146        let Statement::Expression(expr_stmt) = stmt else {
147            return None;
148        };
149
150        let Expression::Assignment(assign) = expr_stmt.as_ref() else {
151            return None;
152        };
153
154        if assign.operator != AssignmentOp::Assign {
155            return None;
156        }
157
158        // var = g_object_ref(new_val)
159        if let Expression::Call(call) = &*assign.rhs
160            && call.is_function("g_object_ref")
161            && !call.arguments.is_empty()
162        {
163            let new_val = call.get_arg(0)?.location().as_str()?;
164            let var_name = assign.lhs_as_text();
165            if !var_name.is_empty() {
166                return Some((var_name, new_val));
167            }
168        }
169
170        None
171    }
172}