use gobject_ast::model::{Expression, FileModel, FunctionDefItem, IfStatement, Statement};
use crate::{
ast_context::AstContext,
config::Config,
rules::{Fix, Rule, Violation},
};
pub struct UnnecessaryNullCheck;
impl Rule for UnnecessaryNullCheck {
fn name(&self) -> &'static str {
"unnecessary_null_check"
}
fn description(&self) -> &'static str {
"Detect unnecessary NULL checks before g_free/g_clear_* functions"
}
fn category(&self) -> crate::rules::Category {
crate::rules::Category::Suspicious
}
fn fixable(&self) -> bool {
true
}
fn check_func_impl(
&self,
_ast_context: &AstContext,
_config: &Config,
func: &FunctionDefItem,
file: &FileModel,
violations: &mut Vec<Violation>,
) {
for stmt in &func.body_statements {
for if_stmt in stmt.iter_if_statements() {
self.check_if_statement(if_stmt, file, violations);
}
}
}
}
impl UnnecessaryNullCheck {
fn check_if_statement(
&self,
if_stmt: &IfStatement,
file: &FileModel,
violations: &mut Vec<Violation>,
) {
if if_stmt.has_else() {
return;
}
let Some(checked_var) = if_stmt.extract_null_check_variable() else {
return;
};
if !if_stmt.has_single_statement() {
return;
}
let Statement::Expression(expr_stmt) = &if_stmt.then_body[0] else {
return;
};
let Expression::Call(call) = expr_stmt.as_ref() else {
return;
};
let Some(func_name) = call.function_name_str() else {
return;
};
if !func_name.starts_with("g_free") && !func_name.starts_with("g_clear_") {
return;
}
let references_var = call
.arguments
.iter()
.any(|e| e.contains_identifier(checked_var));
if !references_var {
return;
}
let loc = expr_stmt.location();
let stmt_end = loc.find_semicolon_end();
let stmt_loc = loc.with_byte_range(loc.start_byte, stmt_end);
let stmt_text = stmt_loc.as_str().unwrap_or_default();
let fix = Fix::new(
if_stmt.location.start_byte,
if_stmt.location.end_byte,
stmt_text.to_string(),
);
violations.push(self.violation_with_fix_at(
&file.path,
&if_stmt.location,
format!(
"Remove unnecessary NULL check before {} ({} handles NULL)",
func_name, func_name
),
fix,
));
}
}