use gobject_ast::{AssignmentOp, Expression, Statement, UnaryOp};
use crate::{
ast_context::AstContext,
config::Config,
rules::{Fix, Rule, Violation},
};
pub struct UseGSetStr;
impl Rule for UseGSetStr {
fn name(&self) -> &'static str {
"use_g_set_str"
}
fn description(&self) -> &'static str {
"Suggest g_set_str() instead of manual g_free and g_strdup"
}
fn category(&self) -> crate::rules::Category {
crate::rules::Category::Complexity
}
fn fixable(&self) -> bool {
true
}
fn check_func_impl(
&self,
_ast_context: &AstContext,
_config: &Config,
func: &gobject_ast::top_level::FunctionDefItem,
file: &gobject_ast::FileModel,
violations: &mut Vec<Violation>,
) {
Statement::walk_pairs(&func.body_statements, &mut |s1, s2| {
self.try_free_then_strdup(s1, s2, file, violations);
});
}
}
impl UseGSetStr {
fn try_free_then_strdup(
&self,
s1: &Statement,
s2: &Statement,
file: &gobject_ast::FileModel,
violations: &mut Vec<Violation>,
) -> bool {
let Some(var_name) = self.extract_gfree_var(s1, &file.source) else {
return false;
};
let Some((assign_var, new_val)) = self.extract_strdup_assignment(s2, &file.source) else {
return false;
};
if assign_var != var_name {
return false;
}
let replacement = format!("g_set_str (&{var_name}, {new_val});");
let message = format!("Use {replacement} instead of g_free and g_strdup");
let s2_end = s2.location().find_semicolon_end(&file.source);
let fixes = vec![
Fix::delete_line(s1.location(), &file.source),
Fix::new(s2.location().start_byte, s2_end, replacement),
];
violations.push(self.violation_with_fixes(
&file.path,
s1.location().line,
s1.location().column,
message,
fixes,
));
true
}
fn extract_gfree_var<'a>(&self, stmt: &Statement, source: &'a [u8]) -> Option<&'a str> {
let Statement::Expression(expr_stmt) = stmt else {
return None;
};
let Expression::Call(call) = expr_stmt.as_ref() else {
return None;
};
if call.is_function("g_free") {
return call.get_arg(0)?.to_source_string(source);
} else if call.is_function("g_clear_pointer") {
if call.arguments.len() != 2 {
return None;
}
let second_arg = call.get_arg(1)?;
if let Expression::Identifier(id) = second_arg {
if id.name != "g_free" {
return None;
}
} else {
return None;
}
let first_arg = call.get_arg(0)?;
if let Expression::Unary(unary) = first_arg
&& unary.operator == UnaryOp::AddressOf
{
return unary.operand.to_source_string(source);
}
}
None
}
fn extract_strdup_assignment<'a>(
&self,
stmt: &Statement,
source: &'a [u8],
) -> 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_strdup")
&& !call.arguments.is_empty()
{
let new_val = call.get_arg(0)?.to_source_string(source)?;
let var_name = assign.lhs_as_text(source);
if !var_name.is_empty() {
return Some((var_name, new_val));
}
}
if let Expression::Conditional(cond) = &*assign.rhs
&& cond.then_expr.is_call_to_any(&["g_strdup", "g_strndup"])
{
let cond_text = cond.condition.to_source_string(source)?;
let var_name = assign.lhs_as_text(source);
if !var_name.is_empty() {
return Some((var_name, cond_text));
}
}
None
}
}