Skip to main content

gobject_linter/rules/
use_clear_functions.rs

1use gobject_ast::model::{
2    AssignmentOp, BinaryExpression, BinaryOp, Expression, FileModel, FunctionDefItem, IfStatement,
3    SourceLocation, Statement, UnaryExpression, UnaryOp,
4};
5
6use crate::{
7    ast_context::AstContext,
8    config::Config,
9    rules::{Fix, Rule, Violation},
10};
11
12#[derive(Clone, Copy)]
13enum NullCheck {
14    Null,
15    Zero,
16    NullOrZero,
17}
18
19impl NullCheck {
20    fn matches(self, expr: &Expression) -> bool {
21        match self {
22            Self::Null => expr.is_null(),
23            Self::Zero => expr.is_zero(),
24            Self::NullOrZero => expr.is_null() || expr.is_zero(),
25        }
26    }
27}
28
29#[derive(Clone, Copy)]
30enum ClearReplacement {
31    Object,
32    Pointer,
33    HandleId,
34    SignalHandler,
35    WeakPointer,
36    List { clear_func: &'static str },
37    Error,
38}
39
40#[derive(Clone, Copy)]
41struct ClearMapping {
42    source_func: &'static str,
43    replacement: ClearReplacement,
44    null_check: NullCheck,
45    min_version: (u32, u32),
46}
47
48macro_rules! PointerMapping {
49    ($name:literal) => {
50        ClearMapping {
51            source_func: $name,
52            replacement: ClearReplacement::Pointer,
53            null_check: NullCheck::NullOrZero,
54            min_version: (2, 28),
55        }
56    };
57}
58
59const CLEAR_MAPPINGS: &[ClearMapping] = &[
60    ClearMapping {
61        source_func: "g_source_remove",
62        replacement: ClearReplacement::HandleId,
63        null_check: NullCheck::Zero,
64        min_version: (2, 56),
65    },
66    PointerMapping!("g_source_destroy"),
67    ClearMapping {
68        source_func: "g_signal_handler_disconnect",
69        replacement: ClearReplacement::SignalHandler,
70        null_check: NullCheck::Zero,
71        min_version: (2, 0),
72    },
73    ClearMapping {
74        source_func: "g_object_remove_weak_pointer",
75        replacement: ClearReplacement::WeakPointer,
76        null_check: NullCheck::Null,
77        min_version: (2, 56),
78    },
79    ClearMapping {
80        source_func: "g_list_free",
81        replacement: ClearReplacement::List {
82            clear_func: "g_clear_list",
83        },
84        null_check: NullCheck::Null,
85        min_version: (2, 64),
86    },
87    ClearMapping {
88        source_func: "g_slist_free",
89        replacement: ClearReplacement::List {
90            clear_func: "g_clear_slist",
91        },
92        null_check: NullCheck::Null,
93        min_version: (2, 64),
94    },
95    ClearMapping {
96        source_func: "g_error_free",
97        replacement: ClearReplacement::Error,
98        null_check: NullCheck::Null,
99        min_version: (2, 0),
100    },
101    ClearMapping {
102        source_func: "g_object_unref",
103        replacement: ClearReplacement::Object,
104        null_check: NullCheck::NullOrZero,
105        min_version: (2, 28),
106    },
107    PointerMapping!("g_free"),
108    PointerMapping!("g_hash_table_destroy"),
109    PointerMapping!("g_hash_table_unref"),
110    PointerMapping!("g_array_unref"),
111    PointerMapping!("g_async_queue_unref"),
112    PointerMapping!("g_byte_array_unref"),
113    PointerMapping!("g_bytes_unref"),
114    PointerMapping!("g_checksum_free"),
115    PointerMapping!("g_date_free"),
116    PointerMapping!("g_date_time_unref"),
117    PointerMapping!("g_key_file_free"),
118    PointerMapping!("g_option_context_free"),
119    PointerMapping!("g_ptr_array_unref"),
120    PointerMapping!("g_queue_free"),
121    PointerMapping!("g_sequence_free"),
122    PointerMapping!("g_time_zone_unref"),
123    PointerMapping!("g_uri_unref"),
124    PointerMapping!("g_variant_unref"),
125    PointerMapping!("g_variant_dict_unref"),
126    PointerMapping!("g_variant_iter_unref"),
127    PointerMapping!("g_variant_type_unref"),
128    PointerMapping!("gdk_color_state_unref"),
129    PointerMapping!("gdk_event_unref"),
130    PointerMapping!("gdk_texture_downloader_free"),
131    PointerMapping!("gsk_path_unref"),
132    PointerMapping!("gsk_path_builder_unref"),
133    PointerMapping!("gsk_path_measure_unref"),
134    PointerMapping!("gsk_render_node_unref"),
135    PointerMapping!("gsk_render_replay_free"),
136    PointerMapping!("gsk_stroke_free"),
137    PointerMapping!("gsk_transform_unref"),
138    PointerMapping!("gtk_expression_unref"),
139    PointerMapping!("gtk_expression_watch_unref"),
140];
141
142impl ClearMapping {
143    fn is_enabled(&self, config: &Config) -> bool {
144        if let Some((major, minor)) = config.min_glib_version
145            && (major < self.min_version.0
146                || (major == self.min_version.0 && minor < self.min_version.1))
147        {
148            return false;
149        }
150        true
151    }
152}
153
154fn address_of(var_name: &str) -> String {
155    if let Some(inner) = var_name.strip_prefix('*') {
156        inner.to_string()
157    } else {
158        format!("&{var_name}")
159    }
160}
161
162fn format_replacement(
163    mapping: &ClearMapping,
164    var_name: &str,
165    obj: Option<&str>,
166    style: &crate::config::Style,
167) -> String {
168    let addr = address_of(var_name);
169    match mapping.replacement {
170        ClearReplacement::Object => style.format_call_stmt("g_clear_object", &[&addr]),
171        ClearReplacement::Pointer => {
172            style.format_call_stmt("g_clear_pointer", &[&addr, mapping.source_func])
173        }
174        ClearReplacement::HandleId => {
175            style.format_call_stmt("g_clear_handle_id", &[&addr, mapping.source_func])
176        }
177        ClearReplacement::SignalHandler => {
178            let obj = obj.unwrap_or("obj");
179            style.format_call_stmt("g_clear_signal_handler", &[&addr, obj])
180        }
181        ClearReplacement::WeakPointer => style.format_call_stmt("g_clear_weak_pointer", &[&addr]),
182        ClearReplacement::List { clear_func } => {
183            style.format_call_stmt(clear_func, &[&addr, "NULL"])
184        }
185        ClearReplacement::Error => style.format_call_stmt("g_clear_error", &[&addr]),
186    }
187}
188
189pub struct UseClearFunctions;
190
191impl Rule for UseClearFunctions {
192    fn name(&self) -> &'static str {
193        "use_clear_functions"
194    }
195
196    fn description(&self) -> &'static str {
197        "Suggest g_clear_* functions instead of manual cleanup and NULL/zero assignment"
198    }
199
200    fn category(&self) -> crate::rules::Category {
201        crate::rules::Category::Complexity
202    }
203
204    fn fixable(&self) -> bool {
205        true
206    }
207
208    fn check_func_impl(
209        &self,
210        _ast_context: &AstContext,
211        config: &Config,
212        func: &FunctionDefItem,
213        file: &FileModel,
214        violations: &mut Vec<Violation>,
215    ) {
216        self.check_statements(config, file, &func.body_statements, violations);
217    }
218}
219
220impl UseClearFunctions {
221    fn check_statements(
222        &self,
223        config: &Config,
224        file: &FileModel,
225        statements: &[Statement],
226        violations: &mut Vec<Violation>,
227    ) {
228        // Check consecutive pairs
229        let mut i = 0;
230        while i < statements.len() {
231            // Try signal_handler's if-guarded pattern
232            if self.try_signal_handler_if_guarded(&statements[i], config, file, violations) {
233                i += 1;
234                continue;
235            }
236
237            // Try handle_id's if pattern
238            if self.try_handle_id_if_pattern(&statements[i], config, file, violations) {
239                i += 1;
240                continue;
241            }
242
243            // Try generic if-statement pattern (clear_functions style)
244            if let Statement::If(if_stmt) = &statements[i]
245                && self.try_generic_if_pattern(if_stmt, config, file, violations)
246            {
247                i += 1;
248                continue;
249            }
250
251            // Try consecutive pair patterns
252            if i + 1 < statements.len() {
253                if let Some(matched) = self.try_consecutive_pair(
254                    &statements[i],
255                    &statements[i + 1],
256                    config,
257                    file,
258                    violations,
259                ) && matched
260                {
261                    i += 2;
262                    continue;
263                }
264
265                // Try signal_handler's disconnect + zero pattern
266                if self.try_signal_disconnect_then_zero(
267                    &statements[i],
268                    &statements[i + 1],
269                    config,
270                    file,
271                    violations,
272                ) {
273                    i += 2;
274                    continue;
275                }
276            }
277
278            // Try signal_handler's bare member disconnect
279            if self.try_bare_disconnect_on_member(
280                &statements[i],
281                statements,
282                config,
283                file,
284                violations,
285            ) {
286                i += 1;
287                continue;
288            }
289
290            // Recurse into nested blocks
291            if let Statement::If(if_stmt) = &statements[i] {
292                self.check_statements(config, file, &if_stmt.then_body, violations);
293                if let Some(else_body) = &if_stmt.else_body {
294                    self.check_statements(config, file, else_body, violations);
295                }
296            } else {
297                statements[i].for_each_child_block(|body| {
298                    self.check_statements(config, file, body, violations);
299                });
300            }
301
302            i += 1;
303        }
304
305        // handle_id: check for unnecessary braces around single g_clear_handle_id
306        for stmt in statements {
307            if let Statement::If(if_stmt) = stmt {
308                self.check_unnecessary_braces(if_stmt, file, violations);
309            }
310        }
311    }
312
313    fn try_consecutive_pair(
314        &self,
315        stmt1: &Statement,
316        stmt2: &Statement,
317        config: &Config,
318        file: &FileModel,
319        violations: &mut Vec<Violation>,
320    ) -> Option<bool> {
321        let call = stmt1.extract_call()?;
322
323        for mapping in CLEAR_MAPPINGS {
324            if !mapping.is_enabled(config) {
325                continue;
326            }
327            // Skip signal_handler — handled separately with arg reversal
328            if matches!(mapping.replacement, ClearReplacement::SignalHandler) {
329                continue;
330            }
331
332            if !call.is_function(mapping.source_func) {
333                continue;
334            }
335
336            let var_name = match mapping.replacement {
337                ClearReplacement::WeakPointer => {
338                    self.extract_weak_pointer_var(call.arguments.get(1)?)?
339                }
340                _ => call.get_arg(0)?.location().as_str()?,
341            };
342
343            if !stmt2.is_assignment_to(var_name, |expr| mapping.null_check.matches(expr)) {
344                continue;
345            }
346
347            let replacement = format_replacement(mapping, var_name, None, &config.style);
348            let message = format!(
349                "Use {} instead of {} and NULL/zero assignment",
350                replacement.trim_end_matches(';'),
351                mapping.source_func
352            );
353
354            let stmt1_end = stmt1.location().find_semicolon_end();
355            let fixes = vec![
356                Fix::new(stmt1.location().start_byte, stmt1_end, replacement),
357                Fix::delete_line(stmt2.location()),
358            ];
359
360            violations.push(self.violation_with_fixes_at(
361                &file.path,
362                stmt1.location(),
363                message,
364                fixes,
365            ));
366
367            return Some(true);
368        }
369        Some(false)
370    }
371
372    fn try_generic_if_pattern(
373        &self,
374        if_stmt: &IfStatement,
375        config: &Config,
376        file: &FileModel,
377        violations: &mut Vec<Violation>,
378    ) -> bool {
379        if self.has_logical_operators(&if_stmt.condition) {
380            return false;
381        }
382
383        let Some(checked_var) = self.find_variable_in_condition(&if_stmt.condition) else {
384            return false;
385        };
386
387        if if_stmt.then_body.len() != 2 {
388            return false;
389        }
390
391        let Some(mapping) = self.find_unref_in_body(&if_stmt.then_body, checked_var, config) else {
392            return false;
393        };
394
395        if !self.has_null_assignment(&if_stmt.then_body, checked_var, mapping.null_check) {
396            return false;
397        }
398
399        let replacement = format_replacement(&mapping, checked_var, None, &config.style);
400        let message = format!(
401            "Use {} instead of manual NULL check, unref, and assignment",
402            replacement.trim_end_matches(';')
403        );
404
405        let fix = Fix::new(
406            if_stmt.location.start_byte,
407            if_stmt.location.end_byte,
408            replacement,
409        );
410
411        violations.push(self.violation_with_fix_at(&file.path, &if_stmt.location, message, fix));
412
413        true
414    }
415
416    fn find_variable_in_condition<'a>(&self, expr: &'a Expression) -> Option<&'a str> {
417        if let Some(var) = expr.extract_variable_name() {
418            return Some(var);
419        }
420
421        match expr {
422            Expression::Binary(BinaryExpression {
423                left: l,
424                operator: op,
425                right: r,
426                ..
427            }) => {
428                let l_empty = l.is_null() || l.is_zero();
429                let r_empty = r.is_null() || r.is_zero();
430                match (op, l_empty, r_empty) {
431                    (BinaryOp::NotEqual, true, false) => r.extract_variable_name(),
432                    (BinaryOp::Less, true, false) => r.extract_variable_name(),
433                    (BinaryOp::NotEqual, false, true) => l.extract_variable_name(),
434                    (BinaryOp::Greater, false, true) => l.extract_variable_name(),
435                    _ => None,
436                }
437            }
438            Expression::Unary(UnaryExpression {
439                operator: UnaryOp::Not,
440                operand: op,
441                ..
442            }) => op.extract_variable_name(),
443            _ => expr.location().as_str(),
444        }
445    }
446
447    fn has_logical_operators(&self, expr: &Expression) -> bool {
448        let mut found = false;
449        expr.walk(&mut |e| {
450            if let Expression::Binary(bin) = e
451                && matches!(bin.operator, BinaryOp::LogicalAnd | BinaryOp::LogicalOr)
452            {
453                found = true;
454            }
455        });
456        found
457    }
458
459    fn find_unref_in_body(
460        &self,
461        statements: &[Statement],
462        var_name: &str,
463        config: &Config,
464    ) -> Option<ClearMapping> {
465        for stmt in statements {
466            if let Some(call) = stmt.extract_call() {
467                for mapping in CLEAR_MAPPINGS {
468                    if !mapping.is_enabled(config) {
469                        continue;
470                    }
471                    // Skip patterns that don't apply to generic if-check
472                    if matches!(
473                        mapping.replacement,
474                        ClearReplacement::SignalHandler
475                            | ClearReplacement::HandleId
476                            | ClearReplacement::WeakPointer
477                    ) {
478                        continue;
479                    }
480                    if call.is_function(mapping.source_func) {
481                        for arg in &call.arguments {
482                            if let Some(arg_text) = arg.location().as_str()
483                                && arg_text.contains(var_name)
484                            {
485                                return Some(*mapping);
486                            }
487                        }
488                    }
489                }
490            }
491        }
492        None
493    }
494
495    fn has_null_assignment(
496        &self,
497        statements: &[Statement],
498        var_name: &str,
499        null_check: NullCheck,
500    ) -> bool {
501        statements
502            .iter()
503            .any(|stmt| stmt.is_assignment_to(var_name, |expr| null_check.matches(expr)))
504    }
505
506    fn try_handle_id_if_pattern(
507        &self,
508        stmt: &Statement,
509        config: &Config,
510        file: &FileModel,
511        violations: &mut Vec<Violation>,
512    ) -> bool {
513        let Statement::If(if_stmt) = stmt else {
514            return false;
515        };
516        let conversions = self.check_handle_cleanup_then_zero(config, &if_stmt.then_body);
517
518        if conversions.is_empty() {
519            return false;
520        }
521
522        let stmt_count = if_stmt.then_body.len();
523        let has_else = if_stmt.else_body.is_some();
524        let cond_id = if_stmt.extract_nonzero_check_variable();
525
526        for (var_name, mapping, first_loc, second_loc) in conversions {
527            let replacement = format_replacement(&mapping, &var_name, None, &config.style);
528            let message = format!(
529                "Use {} instead of {} and zero assignment",
530                replacement.trim_end_matches(';'),
531                mapping.source_func
532            );
533            let can_remove_if = !has_else && cond_id == Some(var_name.as_str()) && stmt_count == 2;
534
535            let fix = if can_remove_if {
536                Fix::new(
537                    if_stmt.location.start_byte,
538                    if_stmt.location.end_byte,
539                    replacement,
540                )
541            } else if stmt_count == 2 {
542                let first_loc = if_stmt.then_body[0].location();
543                let (brace_start, brace_end) = first_loc.find_braces_around();
544                let brace_loc = first_loc.with_byte_range(brace_start, brace_start);
545                let (line_start, _) = brace_loc.find_line_bounds();
546                let indent = brace_loc.extract_line_indentation();
547                let fix_start = line_start.saturating_sub(1);
548                let formatted_replacement = format!("\n{}{}", indent, replacement);
549
550                Fix::new(fix_start, brace_end, formatted_replacement)
551            } else {
552                Fix::new(
553                    first_loc.start_byte,
554                    second_loc.find_semicolon_end(),
555                    replacement,
556                )
557            };
558
559            violations.push(self.violation_with_fix_at(&file.path, &first_loc, message, fix));
560        }
561        true
562    }
563
564    fn check_handle_cleanup_then_zero(
565        &self,
566        config: &Config,
567        statements: &[Statement],
568    ) -> Vec<(String, ClearMapping, SourceLocation, SourceLocation)> {
569        let mut results = Vec::new();
570
571        Statement::for_each_pair(statements, |first, second| {
572            if let Some((var_name, mapping)) = self.extract_handle_cleanup(first, config)
573                && second.is_assignment_to(&var_name, Expression::is_zero)
574            {
575                results.push((
576                    var_name,
577                    mapping,
578                    first.location().clone(),
579                    second.location().clone(),
580                ));
581            }
582        });
583
584        results
585    }
586
587    fn extract_handle_cleanup(
588        &self,
589        stmt: &Statement,
590        config: &Config,
591    ) -> Option<(String, ClearMapping)> {
592        let call = stmt.extract_call()?;
593        let func_name = call.function_name_str()?;
594
595        let mapping = CLEAR_MAPPINGS.iter().find(|m| {
596            matches!(m.replacement, ClearReplacement::HandleId)
597                && m.source_func == func_name
598                && m.is_enabled(config)
599        })?;
600
601        let arg_expr = call.get_arg(0)?;
602        let var_name = arg_expr.location().as_str()?.trim().to_owned();
603
604        Some((var_name, *mapping))
605    }
606
607    fn check_unnecessary_braces(
608        &self,
609        if_stmt: &IfStatement,
610        file: &FileModel,
611        violations: &mut Vec<Violation>,
612    ) {
613        if if_stmt.then_body.len() == 1
614            && if_stmt.then_has_braces
615            && let Statement::Expression(expr_stmt) = &if_stmt.then_body[0]
616            && let Expression::Call(call) = expr_stmt.as_ref()
617            && call.is_function("g_clear_handle_id")
618            && let Some(cond_var) = if_stmt.extract_nonzero_check_variable()
619            && let Some(cleared_var) = call.get_arg_text(0).and_then(|s| s.strip_prefix('&'))
620            && cond_var == cleared_var
621        {
622            let call_text = call.location.as_str().unwrap_or("");
623            let loc = if_stmt.then_body[0].location();
624            let fix = Fix::new(
625                loc.start_byte,
626                loc.find_semicolon_end(),
627                format!("{};", call_text),
628            );
629
630            violations.push(self.violation_with_fix_at(
631                &file.path,
632                &if_stmt.location,
633                "Remove unnecessary braces around single g_clear_handle_id call".to_string(),
634                fix,
635            ));
636        }
637    }
638
639    fn try_signal_handler_if_guarded(
640        &self,
641        stmt: &Statement,
642        config: &Config,
643        file: &FileModel,
644        violations: &mut Vec<Violation>,
645    ) -> bool {
646        let signal_mapping = match self.find_signal_mapping(config) {
647            Some(m) => m,
648            None => return false,
649        };
650
651        let Statement::If(if_stmt) = stmt else {
652            return false;
653        };
654
655        if if_stmt.has_else() {
656            return false;
657        }
658
659        let Some(guarded_id) = if_stmt.extract_nonzero_check_variable() else {
660            return false;
661        };
662
663        if if_stmt.then_body.len() != 2 {
664            return false;
665        }
666
667        let Some((obj, handler_id)) = self.extract_disconnect_args(&if_stmt.then_body[0]) else {
668            return false;
669        };
670
671        if handler_id != guarded_id {
672            return false;
673        }
674
675        if !self.is_zero_assign(&if_stmt.then_body[1], handler_id) {
676            return false;
677        }
678
679        let replacement = format_replacement(&signal_mapping, handler_id, Some(obj), &config.style);
680        let message = format!(
681            "Use {} instead of if-guarded g_signal_handler_disconnect",
682            replacement.trim_end_matches(';')
683        );
684        let fix = Fix::new(
685            if_stmt.location.start_byte,
686            if_stmt.location.end_byte,
687            replacement,
688        );
689
690        violations.push(self.violation_with_fix_at(&file.path, &if_stmt.location, message, fix));
691        true
692    }
693
694    fn try_signal_disconnect_then_zero(
695        &self,
696        s1: &Statement,
697        s2: &Statement,
698        config: &Config,
699        file: &FileModel,
700        violations: &mut Vec<Violation>,
701    ) -> bool {
702        let signal_mapping = match self.find_signal_mapping(config) {
703            Some(m) => m,
704            None => return false,
705        };
706
707        let Some((obj, handler_id)) = self.extract_disconnect_args(s1) else {
708            return false;
709        };
710
711        if !self.is_zero_assign(s2, handler_id) {
712            return false;
713        }
714
715        let replacement = format_replacement(&signal_mapping, handler_id, Some(obj), &config.style);
716        let message = format!(
717            "Use {} instead of g_signal_handler_disconnect and zeroing the ID",
718            replacement.trim_end_matches(';')
719        );
720        let s1_end = s1.location().find_semicolon_end();
721
722        let fixes = vec![
723            Fix::new(s1.location().start_byte, s1_end, replacement),
724            Fix::delete_line(s2.location()),
725        ];
726
727        violations.push(self.violation_with_fixes_at(&file.path, s1.location(), message, fixes));
728        true
729    }
730
731    fn try_bare_disconnect_on_member(
732        &self,
733        stmt: &Statement,
734        all_stmts: &[Statement],
735        config: &Config,
736        file: &FileModel,
737        violations: &mut Vec<Violation>,
738    ) -> bool {
739        let signal_mapping = match self.find_signal_mapping(config) {
740            Some(m) => m,
741            None => return false,
742        };
743
744        let Some((obj, handler_id)) = self.extract_disconnect_args(stmt) else {
745            return false;
746        };
747
748        if !handler_id.contains("->") {
749            return false;
750        }
751
752        let base = handler_id.split("->").next().unwrap_or("").trim();
753        if base.is_empty() {
754            return false;
755        }
756
757        if self.is_freed_in_stmts(all_stmts, base) || self.is_freed_in_stmts(all_stmts, obj) {
758            return false;
759        }
760
761        let replacement = format_replacement(&signal_mapping, handler_id, Some(obj), &config.style);
762        let message = format!(
763            "Use {} instead of g_signal_handler_disconnect (also zeroes the stored ID)",
764            replacement.trim_end_matches(';')
765        );
766        let stmt_end = stmt.location().find_semicolon_end();
767        let fix = Fix::new(stmt.location().start_byte, stmt_end, replacement);
768
769        violations.push(self.violation_with_fix_at(&file.path, stmt.location(), message, fix));
770        true
771    }
772
773    fn find_signal_mapping(&self, config: &Config) -> Option<ClearMapping> {
774        CLEAR_MAPPINGS
775            .iter()
776            .find(|m| {
777                matches!(m.replacement, ClearReplacement::SignalHandler) && m.is_enabled(config)
778            })
779            .copied()
780    }
781
782    fn extract_disconnect_args<'a>(&self, stmt: &'a Statement) -> Option<(&'a str, &'a str)> {
783        let call = stmt.extract_call()?;
784
785        if !call.is_function("g_signal_handler_disconnect") {
786            return None;
787        }
788
789        if call.arguments.len() != 2 {
790            return None;
791        }
792
793        let obj = call.get_arg(0)?.extract_variable_name()?;
794        let handler_id = call.get_arg(1)?.extract_variable_name()?;
795
796        Some((obj, handler_id))
797    }
798
799    fn is_zero_assign(&self, stmt: &Statement, expected_id: &str) -> bool {
800        let Statement::Expression(expr_stmt) = stmt else {
801            return false;
802        };
803
804        let Expression::Assignment(assign) = expr_stmt.as_ref() else {
805            return false;
806        };
807
808        assign.lhs_as_text() == expected_id
809            && assign.operator == AssignmentOp::Assign
810            && assign.rhs.is_zero()
811    }
812
813    fn is_freed_in_stmts(&self, stmts: &[Statement], target: &str) -> bool {
814        for stmt in stmts {
815            let Statement::Expression(expr_stmt) = stmt else {
816                continue;
817            };
818
819            let Expression::Call(call) = expr_stmt.as_ref() else {
820                continue;
821            };
822
823            if !call.function_contains("free")
824                && !call.function_contains("unref")
825                && !call.function_contains("destroy")
826                && !call.function_contains("clear")
827            {
828                continue;
829            }
830
831            for arg in &call.arguments {
832                if self.arg_references(arg, target) {
833                    return true;
834                }
835            }
836        }
837        false
838    }
839
840    fn arg_references(&self, arg: &Expression, target: &str) -> bool {
841        let mut found = false;
842        arg.walk(&mut |e| match e {
843            Expression::Identifier(id) if id.name == target => {
844                found = true;
845            }
846            Expression::FieldAccess(f) if f.location.as_str() == Some(target) => {
847                found = true;
848            }
849            _ => {}
850        });
851        found
852    }
853
854    fn extract_weak_pointer_var<'a>(&self, expr: &'a Expression) -> Option<&'a str> {
855        // Handle cast expressions: (gpointer*)&var
856        let inner_expr = match expr {
857            Expression::Cast(cast) => &*cast.operand,
858            other => other,
859        };
860
861        if let Expression::Unary(unary) = inner_expr
862            && unary.operator == UnaryOp::AddressOf
863        {
864            return unary.operand.extract_variable_name();
865        }
866
867        None
868    }
869}