Skip to main content

gobject_linter/rules/
use_g_object_new_with_properties.rs

1use gobject_ast::model::{
2    CallExpression, Expression, FileModel, FunctionDefItem, SourceLocation, Statement,
3};
4
5use crate::{
6    ast_context::AstContext,
7    config::Config,
8    rules::{Rule, Violation},
9};
10
11pub struct UseGObjectNewWithProperties;
12
13impl Rule for UseGObjectNewWithProperties {
14    fn name(&self) -> &'static str {
15        "use_g_object_new_with_properties"
16    }
17
18    fn description(&self) -> &'static str {
19        "Suggest setting properties in g_object_new instead of separate g_object_set calls"
20    }
21
22    fn category(&self) -> crate::rules::Category {
23        crate::rules::Category::Complexity
24    }
25
26    fn check_func_impl(
27        &self,
28        _ast_context: &AstContext,
29        _config: &Config,
30        func: &FunctionDefItem,
31        file: &FileModel,
32        violations: &mut Vec<Violation>,
33    ) {
34        // Find all g_object_new calls with no properties
35        let empty_new_calls: Vec<_> = func
36            .find_calls(&["g_object_new"])
37            .into_iter()
38            .filter(|call| self.is_g_object_new_empty(call))
39            .collect();
40
41        if empty_new_calls.is_empty() {
42            return;
43        }
44
45        // Check statements for the pattern
46        self.check_statements(&func.body_statements, &empty_new_calls, file, violations);
47    }
48}
49
50impl UseGObjectNewWithProperties {
51    fn check_statements(
52        &self,
53        statements: &[Statement],
54        empty_new_calls: &[&CallExpression],
55        file: &FileModel,
56        violations: &mut Vec<Violation>,
57    ) {
58        for i in 0..statements.len() {
59            // Check if this statement contains one of our empty g_object_new calls
60            if let Some((var_name, location)) =
61                self.find_empty_new_in_statement(&statements[i], empty_new_calls)
62            {
63                // Count consecutive g_object_set calls on the same variable
64                let mut set_count = 0;
65
66                for next_stmt in statements.iter().skip(i + 1) {
67                    if let Some(set_var) = self.extract_g_object_set(next_stmt)
68                        && set_var == var_name
69                    {
70                        set_count += 1;
71                        continue;
72                    }
73
74                    // Stop if we hit something that's not a g_object_set on our variable
75                    break;
76                }
77
78                // Only report if there's at least one g_object_set call
79                if set_count > 0 {
80                    violations.push(self.violation_at(
81                        &file.path,
82                        location,
83                        format!(
84                            "Set properties in g_object_new() instead of {} separate g_object_set() call{}",
85                            set_count,
86                            if set_count > 1 { "s" } else { "" }
87                        ),
88                    ));
89                }
90            }
91
92            statements[i].for_each_child_block(|body| {
93                self.check_statements(body, empty_new_calls, file, violations);
94            });
95        }
96    }
97
98    /// Check if a statement contains one of the empty g_object_new calls
99    /// Returns (variable_name, statement_location) if found
100    fn find_empty_new_in_statement<'a>(
101        &self,
102        stmt: &'a Statement,
103        empty_new_calls: &[&CallExpression],
104    ) -> Option<(&'a str, &'a SourceLocation)> {
105        match stmt {
106            // Declaration: FooObject *obj = g_object_new(TYPE, NULL);
107            Statement::Declaration(decl) => {
108                if let Some(Expression::Call(call)) = &decl.initializer {
109                    // Check if this call is one of our empty g_object_new calls
110                    for empty_call in empty_new_calls {
111                        if call.location.start_byte == empty_call.location.start_byte {
112                            return Some((decl.name.as_str(), &decl.location));
113                        }
114                    }
115                }
116            }
117            // Assignment: obj = g_object_new(TYPE, NULL);
118            Statement::Expression(expr_stmt) => {
119                if let Expression::Assignment(assign) = expr_stmt.as_ref()
120                    && let Expression::Call(call) = &*assign.rhs
121                {
122                    for empty_call in empty_new_calls {
123                        if call.location.start_byte == empty_call.location.start_byte {
124                            let var_name = assign.lhs_as_text();
125                            if !var_name.is_empty() {
126                                return Some((var_name, expr_stmt.location()));
127                            }
128                        }
129                    }
130                }
131            }
132            _ => {}
133        }
134
135        None
136    }
137
138    /// Check if a call is g_object_new with no properties (just NULL or type
139    /// only)
140    fn is_g_object_new_empty(&self, call: &CallExpression) -> bool {
141        if !call.is_function("g_object_new") {
142            return false;
143        }
144
145        // g_object_new with just type and NULL, or just type
146        // g_object_new(TYPE, NULL) - 2 args
147        // g_object_new(TYPE) - 1 arg (rare but valid)
148        match call.arguments.len() {
149            1 => true,
150            2 => call.arguments[1].is_null(),
151            _ => false,
152        }
153    }
154
155    /// Extract g_object_set call, return the object variable
156    fn extract_g_object_set(&self, stmt: &Statement) -> Option<String> {
157        let Statement::Expression(expr_stmt) = stmt else {
158            return None;
159        };
160
161        let Expression::Call(call) = expr_stmt.as_ref() else {
162            return None;
163        };
164
165        if !call.is_function("g_object_set") {
166            return None;
167        }
168
169        // Get the first argument (the object)
170        let expr = call.get_arg(0)?;
171        expr.extract_variable_name()
172            .map(std::string::ToString::to_string)
173    }
174}