Skip to main content

gobject_linter/rules/
use_g_steal_pointer.rs

1use gobject_ast::model::{AssignmentOp, Expression, FileModel, FunctionDefItem, Statement};
2
3use crate::{
4    ast_context::AstContext,
5    config::{Config, Style},
6    rules::{Fix, Rule, Violation},
7};
8
9pub struct UseGStealPointer;
10
11impl Rule for UseGStealPointer {
12    fn name(&self) -> &'static str {
13        "use_g_steal_pointer"
14    }
15
16    fn description(&self) -> &'static str {
17        "Use g_steal_pointer() instead of manually copying a pointer and setting it to NULL"
18    }
19
20    fn category(&self) -> crate::rules::Category {
21        crate::rules::Category::Style
22    }
23
24    fn fixable(&self) -> bool {
25        true
26    }
27
28    fn check_func_impl(
29        &self,
30        _ast_context: &AstContext,
31        config: &Config,
32        func: &FunctionDefItem,
33        file: &FileModel,
34        violations: &mut Vec<Violation>,
35    ) {
36        self.check_statements(&func.body_statements, file, &config.style, violations);
37    }
38}
39
40impl UseGStealPointer {
41    fn check_statements(
42        &self,
43        statements: &[Statement],
44        file: &FileModel,
45        style: &Style,
46        violations: &mut Vec<Violation>,
47    ) {
48        let mut i = 0;
49        while i < statements.len() {
50            if self.try_if_else_steal(&statements[i], file, style, violations) {
51                i += 1;
52                continue;
53            }
54            if self.try_if_no_else_steal(&statements[i], file, style, violations) {
55                i += 1;
56                continue;
57            }
58            if i + 2 < statements.len()
59                && self.try_declare_null_return(
60                    &statements[i],
61                    &statements[i + 1],
62                    &statements[i + 2],
63                    file,
64                    style,
65                    violations,
66                )
67            {
68                i += 3;
69                continue;
70            }
71            if i + 1 < statements.len()
72                && self.try_assign_null(&statements[i], &statements[i + 1], file, style, violations)
73            {
74                i += 2;
75                continue;
76            }
77            statements[i].for_each_child_block(|body| {
78                self.check_statements(body, file, style, violations);
79            });
80            i += 1;
81        }
82    }
83
84    /// Matches: `T *tmp = ptr_expr; ptr_expr = NULL; return tmp;`
85    fn try_declare_null_return(
86        &self,
87        s1: &Statement,
88        s2: &Statement,
89        s3: &Statement,
90        file: &FileModel,
91        style: &Style,
92        violations: &mut Vec<Violation>,
93    ) -> bool {
94        // s1: T *tmp = ptr_expr
95        let Statement::Declaration(decl) = s1 else {
96            return false;
97        };
98
99        let Some(init_expr) = &decl.initializer else {
100            return false;
101        };
102
103        // Skip NULL initializers
104        if init_expr.is_null() {
105            return false;
106        }
107
108        // Get the variable name from the initializer
109        let Some(ptr_expr) = init_expr.extract_variable_name() else {
110            return false;
111        };
112
113        // Skip dereferences
114        if ptr_expr.starts_with('*') {
115            return false;
116        }
117
118        let tmp_name = &decl.name;
119
120        // s2: ptr_expr = NULL
121        if !s2.is_null_assignment_to(ptr_expr) {
122            return false;
123        }
124
125        // s3: return tmp
126        let Statement::Return(ret) = s3 else {
127            return false;
128        };
129
130        if let Some(Expression::Identifier(id)) = &ret.value {
131            if id.name != *tmp_name {
132                return false;
133            }
134        } else {
135            return false;
136        }
137
138        let steal = style.format_addr_call("g_steal_pointer", ptr_expr, &[]);
139        let replacement = format!("return {steal};");
140        let message =
141            format!("Use {replacement} instead of copying {ptr_expr} and setting it to NULL");
142
143        let fixes = vec![
144            Fix::delete_line(s1.location()),
145            Fix::delete_line(s2.location()),
146            Fix::new(
147                s3.location().start_byte,
148                s3.location().end_byte,
149                replacement,
150            ),
151        ];
152
153        violations.push(self.violation_with_fixes_at(&file.path, s1.location(), message, fixes));
154        true
155    }
156
157    /// Matches: `other_expr = ptr_expr; ptr_expr = NULL;`
158    fn try_assign_null(
159        &self,
160        s1: &Statement,
161        s2: &Statement,
162        file: &FileModel,
163        style: &Style,
164        violations: &mut Vec<Violation>,
165    ) -> bool {
166        let Some((other_expr, ptr_expr)) = self.extract_assignment(s1) else {
167            return false;
168        };
169
170        if ptr_expr.starts_with('*') {
171            return false;
172        }
173
174        if !s2.is_null_assignment_to(ptr_expr) {
175            return false;
176        }
177
178        let steal = style.format_addr_call("g_steal_pointer", ptr_expr, &[]);
179        let replacement = format!("{other_expr} = {steal};");
180        let message = format!("Use {steal} instead of copying and setting to NULL");
181
182        // Use two separate fixes to preserve comments between statements
183        let s2_end = s2.location().find_semicolon_end();
184        let fixes = vec![
185            // Delete the entire first line
186            Fix::delete_line(s1.location()),
187            // Replace the second statement
188            Fix::new(s2.location().start_byte, s2_end, replacement),
189        ];
190
191        violations.push(self.violation_with_fixes_at(&file.path, s1.location(), message, fixes));
192        true
193    }
194
195    /// Matches: if (expr) { dest = expr; expr = NULL; } else { dest = NULL; }
196    fn try_if_else_steal(
197        &self,
198        stmt: &Statement,
199        file: &FileModel,
200        style: &Style,
201        violations: &mut Vec<Violation>,
202    ) -> bool {
203        let Statement::If(if_stmt) = stmt else {
204            return false;
205        };
206
207        // Must have else block
208        let Some(else_body) = &if_stmt.else_body else {
209            return false;
210        };
211
212        // Extract tested expression from condition
213        let Some(expr_text) = if_stmt.extract_null_check_variable() else {
214            return false;
215        };
216
217        // Skip dereference expressions
218        if expr_text.starts_with('*') {
219            return false;
220        }
221
222        // Then-block must have exactly 2 statements
223        if if_stmt.then_body.len() != 2 {
224            return false;
225        }
226
227        // then_body[0]: dest = expr
228        let Some((dest_expr, rhs)) = self.extract_assignment(&if_stmt.then_body[0]) else {
229            return false;
230        };
231        if rhs != expr_text {
232            return false;
233        }
234
235        // then_body[1]: expr = NULL
236        if !if_stmt.then_body[1].is_null_assignment_to(expr_text) {
237            return false;
238        }
239
240        // Else-block must have exactly 1 statement: dest = NULL
241        if else_body.len() != 1 {
242            return false;
243        }
244        if !else_body[0].is_null_assignment_to(dest_expr) {
245            return false;
246        }
247
248        let steal = style.format_addr_call("g_steal_pointer", expr_text, &[]);
249        let replacement = format!("{dest_expr} = {steal};");
250        let message = format!("Use {steal} instead of if/else copy-and-NULL pattern");
251        let fix = Fix::new(
252            if_stmt.location.start_byte,
253            if_stmt.location.end_byte,
254            replacement,
255        );
256        violations.push(self.violation_with_fix_at(&file.path, &if_stmt.location, message, fix));
257        true
258    }
259
260    /// Matches if-without-else with steal pattern in body
261    /// if (c) { dest = ptr; ptr = NULL; } or if (c) { T *tmp = ptr; ptr = NULL;
262    /// return tmp; }
263    fn try_if_no_else_steal(
264        &self,
265        stmt: &Statement,
266        file: &FileModel,
267        style: &Style,
268        violations: &mut Vec<Violation>,
269    ) -> bool {
270        let Statement::If(if_stmt) = stmt else {
271            return false;
272        };
273
274        // Must have no else
275        if if_stmt.else_body.is_some() {
276            return false;
277        }
278
279        // Try to extract condition expression
280        let condition_expr = if_stmt.extract_null_check_variable();
281
282        // Pattern 1: 2 statements - dest = ptr; ptr = NULL;
283        if if_stmt.then_body.len() == 2 {
284            let Some((dest_expr, ptr_expr)) = self.extract_assignment(&if_stmt.then_body[0]) else {
285                return false;
286            };
287
288            // Skip dereference expressions
289            if ptr_expr.starts_with('*') {
290                return false;
291            }
292
293            if !if_stmt.then_body[1].is_null_assignment_to(ptr_expr) {
294                return false;
295            }
296
297            let steal = style.format_addr_call("g_steal_pointer", ptr_expr, &[]);
298            let replacement = format!("{dest_expr} = {steal};");
299            let message = format!("Use {steal} instead of copying and setting to NULL");
300
301            // If condition tests the same variable being stolen, remove entire if
302            // Otherwise just replace the body
303            let fix = if condition_expr == Some(ptr_expr) {
304                Fix::new(
305                    if_stmt.location.start_byte,
306                    if_stmt.location.end_byte,
307                    replacement,
308                )
309            } else if if_stmt.then_has_braces {
310                let (open_brace, close_brace) =
311                    if_stmt.then_body[0].location().find_braces_around();
312                Fix::new(open_brace, close_brace, replacement)
313            } else {
314                let body_start = if_stmt.then_body[0].location().start_byte;
315                let body_end = if_stmt.then_body[1].location().end_byte;
316                Fix::new(body_start, body_end, replacement)
317            };
318
319            violations.push(self.violation_with_fix_at(
320                &file.path,
321                if_stmt.then_body[0].location(),
322                message,
323                fix,
324            ));
325            return true;
326        }
327
328        // Pattern 2: 3 statements - T *tmp = ptr; ptr = NULL; return tmp;
329        if if_stmt.then_body.len() == 3 {
330            let Statement::Declaration(decl) = &if_stmt.then_body[0] else {
331                return false;
332            };
333
334            let Some(init_expr) = &decl.initializer else {
335                return false;
336            };
337
338            // Skip NULL initializers
339            if init_expr.is_null() {
340                return false;
341            }
342
343            let Some(ptr_expr) = init_expr.extract_variable_name() else {
344                return false;
345            };
346
347            // Skip dereference expressions
348            if ptr_expr.starts_with('*') {
349                return false;
350            }
351
352            let tmp_name = &decl.name;
353
354            if !if_stmt.then_body[1].is_null_assignment_to(ptr_expr) {
355                return false;
356            }
357
358            // Third statement must be return tmp
359            let Statement::Return(ret) = &if_stmt.then_body[2] else {
360                return false;
361            };
362
363            if let Some(Expression::Identifier(id)) = &ret.value {
364                if id.name != *tmp_name {
365                    return false;
366                }
367            } else {
368                return false;
369            }
370
371            let steal = style.format_addr_call("g_steal_pointer", ptr_expr, &[]);
372            let replacement = format!("return {steal};");
373            let message =
374                format!("Use {replacement} instead of copying {ptr_expr} and setting it to NULL");
375
376            // If condition tests the same variable being stolen, remove entire if
377            let fix = if condition_expr == Some(ptr_expr) {
378                Fix::new(
379                    if_stmt.location.start_byte,
380                    if_stmt.location.end_byte,
381                    replacement,
382                )
383            } else if if_stmt.then_has_braces {
384                let (open_brace, close_brace) =
385                    if_stmt.then_body[0].location().find_braces_around();
386                Fix::new(open_brace, close_brace, replacement)
387            } else {
388                let body_start = if_stmt.then_body[0].location().start_byte;
389                let body_end = if_stmt.then_body[2].location().end_byte;
390                Fix::new(body_start, body_end, replacement)
391            };
392
393            violations.push(self.violation_with_fix_at(
394                &file.path,
395                if_stmt.then_body[0].location(),
396                message,
397                fix,
398            ));
399            return true;
400        }
401
402        false
403    }
404
405    /// Extract (lhs, rhs) from assignment statement
406    fn extract_assignment<'a>(&self, stmt: &'a Statement) -> Option<(&'a str, &'a str)> {
407        let Statement::Expression(expr_stmt) = stmt else {
408            return None;
409        };
410
411        let Expression::Assignment(assign) = expr_stmt.as_ref() else {
412            return None;
413        };
414
415        if assign.operator != AssignmentOp::Assign {
416            return None;
417        }
418
419        // Get rhs as string - handle various expression types
420        let rhs = match &*assign.rhs {
421            Expression::Identifier(id) => id.name.as_str(),
422            Expression::FieldAccess(f) => f.location.as_str().unwrap_or(""),
423            Expression::Null(_) | Expression::Call(_) => {
424                // For NULL or function calls like g_strdup(), we don't want to suggest
425                // g_steal_pointer
426                return None;
427            }
428            _ => {
429                return None;
430            }
431        };
432
433        let lhs = assign.lhs_as_text();
434        if lhs.is_empty() {
435            return None;
436        }
437        Some((lhs, rhs))
438    }
439}