use gobject_ast::model::{
AssignmentOp, Expression, FileModel, FunctionDefItem, Statement, UnaryOp,
};
use crate::{
ast_context::AstContext,
config::Config,
rules::{Fix, Rule, Violation},
};
pub struct UseGSetObject;
impl Rule for UseGSetObject {
fn name(&self) -> &'static str {
"use_g_set_object"
}
fn description(&self) -> &'static str {
"Suggest g_set_object() instead of manual g_clear_object and g_object_ref"
}
fn category(&self) -> crate::rules::Category {
crate::rules::Category::Complexity
}
fn fixable(&self) -> bool {
true
}
fn min_glib_version(&self) -> Option<(u32, u32)> {
Some((2, 44))
}
fn check_func_impl(
&self,
_ast_context: &AstContext,
config: &Config,
func: &FunctionDefItem,
file: &FileModel,
violations: &mut Vec<Violation>,
) {
Statement::walk_pairs(&func.body_statements, &mut |s1, s2| {
self.try_clear_then_ref(s1, s2, file, config, violations);
});
}
}
impl UseGSetObject {
fn try_clear_then_ref(
&self,
s1: &Statement,
s2: &Statement,
file: &FileModel,
config: &Config,
violations: &mut Vec<Violation>,
) -> bool {
let Some((var_name, needs_deref)) = self.extract_clear_or_unref_var(s1) else {
return false;
};
let Some((assign_var, new_val)) = self.extract_object_ref_assignment(s2) else {
return false;
};
let matches = if needs_deref {
assign_var.len() == var_name.len() + 1
&& assign_var.starts_with('*')
&& assign_var[1..] == *var_name
} else {
assign_var == var_name
};
if !matches {
return false;
}
let replacement = if needs_deref {
config
.style
.format_call_stmt("g_set_object", &[var_name, new_val])
} else {
config
.style
.format_addr_call_stmt("g_set_object", var_name, &[new_val])
};
let s2_end = s2.location().find_semicolon_end();
let message = format!("Use {replacement} instead of g_clear_object and g_object_ref");
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 extract_clear_or_unref_var<'a>(&self, stmt: &'a Statement) -> Option<(&'a str, bool)> {
let Statement::Expression(expr_stmt) = stmt else {
return None;
};
let Expression::Call(call) = expr_stmt.as_ref() else {
return None;
};
if call.arguments.is_empty() {
return None;
}
if call.is_function("g_clear_object") {
let first_arg = call.get_arg(0)?;
if let Expression::Unary(unary) = first_arg
&& unary.operator == UnaryOp::AddressOf
{
return Some((unary.operand.location().as_str()?, false));
} else {
return Some((first_arg.location().as_str()?, true));
}
} else if call.is_function("g_object_unref") {
let first_arg = call.get_arg(0)?;
return Some((first_arg.location().as_str()?, false));
}
None
}
fn extract_object_ref_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;
}
if let Expression::Call(call) = &*assign.rhs
&& call.is_function("g_object_ref")
&& !call.arguments.is_empty()
{
let new_val = call.get_arg(0)?.location().as_str()?;
let var_name = assign.lhs_as_text();
if !var_name.is_empty() {
return Some((var_name, new_val));
}
}
None
}
}