use gobject_ast::model::{AssignmentOp, Expression, FileModel, FunctionDefItem, Statement};
use crate::{
ast_context::AstContext,
config::{Config, Style},
rules::{Fix, Rule, Violation},
};
pub struct UseGStealPointer;
impl Rule for UseGStealPointer {
fn name(&self) -> &'static str {
"use_g_steal_pointer"
}
fn description(&self) -> &'static str {
"Use g_steal_pointer() instead of manually copying a pointer and setting it to NULL"
}
fn category(&self) -> crate::rules::Category {
crate::rules::Category::Style
}
fn fixable(&self) -> bool {
true
}
fn check_func_impl(
&self,
_ast_context: &AstContext,
config: &Config,
func: &FunctionDefItem,
file: &FileModel,
violations: &mut Vec<Violation>,
) {
self.check_statements(&func.body_statements, file, &config.style, violations);
}
}
impl UseGStealPointer {
fn check_statements(
&self,
statements: &[Statement],
file: &FileModel,
style: &Style,
violations: &mut Vec<Violation>,
) {
let mut i = 0;
while i < statements.len() {
if self.try_if_else_steal(&statements[i], file, style, violations) {
i += 1;
continue;
}
if self.try_if_no_else_steal(&statements[i], file, style, violations) {
i += 1;
continue;
}
if i + 2 < statements.len()
&& self.try_declare_null_return(
&statements[i],
&statements[i + 1],
&statements[i + 2],
file,
style,
violations,
)
{
i += 3;
continue;
}
if i + 1 < statements.len()
&& self.try_assign_null(&statements[i], &statements[i + 1], file, style, violations)
{
i += 2;
continue;
}
statements[i].for_each_child_block(|body| {
self.check_statements(body, file, style, violations);
});
i += 1;
}
}
fn try_declare_null_return(
&self,
s1: &Statement,
s2: &Statement,
s3: &Statement,
file: &FileModel,
style: &Style,
violations: &mut Vec<Violation>,
) -> bool {
let Statement::Declaration(decl) = s1 else {
return false;
};
let Some(init_expr) = &decl.initializer else {
return false;
};
if init_expr.is_null() {
return false;
}
let Some(ptr_expr) = init_expr.extract_variable_name() else {
return false;
};
if ptr_expr.starts_with('*') {
return false;
}
let tmp_name = &decl.name;
if !s2.is_null_assignment_to(ptr_expr) {
return false;
}
let Statement::Return(ret) = s3 else {
return false;
};
if let Some(Expression::Identifier(id)) = &ret.value {
if id.name != *tmp_name {
return false;
}
} else {
return false;
}
let steal = style.format_addr_call("g_steal_pointer", ptr_expr, &[]);
let replacement = format!("return {steal};");
let message =
format!("Use {replacement} instead of copying {ptr_expr} and setting it to NULL");
let fixes = vec![
Fix::delete_line(s1.location()),
Fix::delete_line(s2.location()),
Fix::new(
s3.location().start_byte,
s3.location().end_byte,
replacement,
),
];
violations.push(self.violation_with_fixes_at(&file.path, s1.location(), message, fixes));
true
}
fn try_assign_null(
&self,
s1: &Statement,
s2: &Statement,
file: &FileModel,
style: &Style,
violations: &mut Vec<Violation>,
) -> bool {
let Some((other_expr, ptr_expr)) = self.extract_assignment(s1) else {
return false;
};
if ptr_expr.starts_with('*') {
return false;
}
if !s2.is_null_assignment_to(ptr_expr) {
return false;
}
let steal = style.format_addr_call("g_steal_pointer", ptr_expr, &[]);
let replacement = format!("{other_expr} = {steal};");
let message = format!("Use {steal} instead of copying and setting to NULL");
let s2_end = s2.location().find_semicolon_end();
let fixes = vec![
Fix::delete_line(s1.location()),
Fix::new(s2.location().start_byte, s2_end, replacement),
];
violations.push(self.violation_with_fixes_at(&file.path, s1.location(), message, fixes));
true
}
fn try_if_else_steal(
&self,
stmt: &Statement,
file: &FileModel,
style: &Style,
violations: &mut Vec<Violation>,
) -> bool {
let Statement::If(if_stmt) = stmt else {
return false;
};
let Some(else_body) = &if_stmt.else_body else {
return false;
};
let Some(expr_text) = if_stmt.extract_null_check_variable() else {
return false;
};
if expr_text.starts_with('*') {
return false;
}
if if_stmt.then_body.len() != 2 {
return false;
}
let Some((dest_expr, rhs)) = self.extract_assignment(&if_stmt.then_body[0]) else {
return false;
};
if rhs != expr_text {
return false;
}
if !if_stmt.then_body[1].is_null_assignment_to(expr_text) {
return false;
}
if else_body.len() != 1 {
return false;
}
if !else_body[0].is_null_assignment_to(dest_expr) {
return false;
}
let steal = style.format_addr_call("g_steal_pointer", expr_text, &[]);
let replacement = format!("{dest_expr} = {steal};");
let message = format!("Use {steal} instead of if/else copy-and-NULL pattern");
let fix = Fix::new(
if_stmt.location.start_byte,
if_stmt.location.end_byte,
replacement,
);
violations.push(self.violation_with_fix_at(&file.path, &if_stmt.location, message, fix));
true
}
fn try_if_no_else_steal(
&self,
stmt: &Statement,
file: &FileModel,
style: &Style,
violations: &mut Vec<Violation>,
) -> bool {
let Statement::If(if_stmt) = stmt else {
return false;
};
if if_stmt.else_body.is_some() {
return false;
}
let condition_expr = if_stmt.extract_null_check_variable();
if if_stmt.then_body.len() == 2 {
let Some((dest_expr, ptr_expr)) = self.extract_assignment(&if_stmt.then_body[0]) else {
return false;
};
if ptr_expr.starts_with('*') {
return false;
}
if !if_stmt.then_body[1].is_null_assignment_to(ptr_expr) {
return false;
}
let steal = style.format_addr_call("g_steal_pointer", ptr_expr, &[]);
let replacement = format!("{dest_expr} = {steal};");
let message = format!("Use {steal} instead of copying and setting to NULL");
let fix = if condition_expr == Some(ptr_expr) {
Fix::new(
if_stmt.location.start_byte,
if_stmt.location.end_byte,
replacement,
)
} else if if_stmt.then_has_braces {
let (open_brace, close_brace) =
if_stmt.then_body[0].location().find_braces_around();
Fix::new(open_brace, close_brace, replacement)
} else {
let body_start = if_stmt.then_body[0].location().start_byte;
let body_end = if_stmt.then_body[1].location().end_byte;
Fix::new(body_start, body_end, replacement)
};
violations.push(self.violation_with_fix_at(
&file.path,
if_stmt.then_body[0].location(),
message,
fix,
));
return true;
}
if if_stmt.then_body.len() == 3 {
let Statement::Declaration(decl) = &if_stmt.then_body[0] else {
return false;
};
let Some(init_expr) = &decl.initializer else {
return false;
};
if init_expr.is_null() {
return false;
}
let Some(ptr_expr) = init_expr.extract_variable_name() else {
return false;
};
if ptr_expr.starts_with('*') {
return false;
}
let tmp_name = &decl.name;
if !if_stmt.then_body[1].is_null_assignment_to(ptr_expr) {
return false;
}
let Statement::Return(ret) = &if_stmt.then_body[2] else {
return false;
};
if let Some(Expression::Identifier(id)) = &ret.value {
if id.name != *tmp_name {
return false;
}
} else {
return false;
}
let steal = style.format_addr_call("g_steal_pointer", ptr_expr, &[]);
let replacement = format!("return {steal};");
let message =
format!("Use {replacement} instead of copying {ptr_expr} and setting it to NULL");
let fix = if condition_expr == Some(ptr_expr) {
Fix::new(
if_stmt.location.start_byte,
if_stmt.location.end_byte,
replacement,
)
} else if if_stmt.then_has_braces {
let (open_brace, close_brace) =
if_stmt.then_body[0].location().find_braces_around();
Fix::new(open_brace, close_brace, replacement)
} else {
let body_start = if_stmt.then_body[0].location().start_byte;
let body_end = if_stmt.then_body[2].location().end_byte;
Fix::new(body_start, body_end, replacement)
};
violations.push(self.violation_with_fix_at(
&file.path,
if_stmt.then_body[0].location(),
message,
fix,
));
return true;
}
false
}
fn extract_assignment<'a>(&self, stmt: &'a Statement) -> Option<(&'a str, &'a str)> {
let Statement::Expression(expr_stmt) = stmt else {
return None;
};
let Expression::Assignment(assign) = expr_stmt.as_ref() else {
return None;
};
if assign.operator != AssignmentOp::Assign {
return None;
}
let rhs = match &*assign.rhs {
Expression::Identifier(id) => id.name.as_str(),
Expression::FieldAccess(f) => f.location.as_str().unwrap_or(""),
Expression::Null(_) | Expression::Call(_) => {
return None;
}
_ => {
return None;
}
};
let lhs = assign.lhs_as_text();
if lhs.is_empty() {
return None;
}
Some((lhs, rhs))
}
}