Skip to main content

gobject_linter/rules/
unnecessary_null_check.rs

1use gobject_ast::model::{Expression, FileModel, FunctionDefItem, IfStatement, Statement};
2
3use crate::{
4    ast_context::AstContext,
5    config::Config,
6    rules::{Fix, Rule, Violation},
7};
8
9pub struct UnnecessaryNullCheck;
10
11impl Rule for UnnecessaryNullCheck {
12    fn name(&self) -> &'static str {
13        "unnecessary_null_check"
14    }
15
16    fn description(&self) -> &'static str {
17        "Detect unnecessary NULL checks before g_free/g_clear_* functions"
18    }
19
20    fn category(&self) -> crate::rules::Category {
21        crate::rules::Category::Suspicious
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        // Walk through function body looking for if statements
37
38        for stmt in &func.body_statements {
39            for if_stmt in stmt.iter_if_statements() {
40                self.check_if_statement(if_stmt, file, violations);
41            }
42        }
43    }
44}
45
46impl UnnecessaryNullCheck {
47    fn check_if_statement(
48        &self,
49        if_stmt: &IfStatement,
50        file: &FileModel,
51        violations: &mut Vec<Violation>,
52    ) {
53        // Don't flag if there's an else branch — removing the if would also drop the
54        // else logic
55        if if_stmt.has_else() {
56            return;
57        }
58
59        // Extract variable being checked (e.g., "ptr" from "ptr != NULL")
60        let Some(checked_var) = if_stmt.extract_null_check_variable() else {
61            return;
62        };
63
64        // Check if the body contains only a g_free/g_clear_* call with the checked
65        // variable
66        if !if_stmt.has_single_statement() {
67            return;
68        }
69
70        // Get the single statement in the then body
71        let Statement::Expression(expr_stmt) = &if_stmt.then_body[0] else {
72            return;
73        };
74
75        // Check if it's a g_free/g_clear_* call
76        let Expression::Call(call) = expr_stmt.as_ref() else {
77            return;
78        };
79
80        // Check for g_free or any g_clear_* function
81        let Some(func_name) = call.function_name_str() else {
82            return;
83        };
84        if !func_name.starts_with("g_free") && !func_name.starts_with("g_clear_") {
85            return;
86        }
87
88        // Check if the call arguments reference the checked variable
89        let references_var = call
90            .arguments
91            .iter()
92            .any(|e| e.contains_identifier(checked_var));
93
94        if !references_var {
95            return;
96        }
97
98        // Create a fix: replace the if statement with the call statement
99        // Extract the statement text from the source (including the semicolon)
100        let loc = expr_stmt.location();
101        let stmt_end = loc.find_semicolon_end();
102        let stmt_loc = loc.with_byte_range(loc.start_byte, stmt_end);
103        let stmt_text = stmt_loc.as_str().unwrap_or_default();
104
105        let fix = Fix::new(
106            if_stmt.location.start_byte,
107            if_stmt.location.end_byte,
108            stmt_text.to_string(),
109        );
110
111        violations.push(self.violation_with_fix_at(
112            &file.path,
113            &if_stmt.location,
114            format!(
115                "Remove unnecessary NULL check before {} ({} handles NULL)",
116                func_name, func_name
117            ),
118            fix,
119        ));
120    }
121}