use gobject_ast::{Expression, Statement, model::expression::Argument};
use crate::{
ast_context::AstContext,
config::Config,
rules::{Rule, Violation},
};
pub struct GObjectVirtualMethodsChainUp;
impl Rule for GObjectVirtualMethodsChainUp {
fn name(&self) -> &'static str {
"g_object_virtual_methods_chain_up"
}
fn description(&self) -> &'static str {
"Ensure dispose/finalize/constructed methods chain up to parent class"
}
fn category(&self) -> crate::rules::Category {
crate::rules::Category::Correctness
}
fn check_func_impl(
&self,
_ast_context: &AstContext,
_config: &Config,
func: &gobject_ast::top_level::FunctionDefItem,
file: &gobject_ast::FileModel,
violations: &mut Vec<Violation>,
) {
let method_type = if func.name.ends_with("_dispose") {
"dispose"
} else if func.name.ends_with("_finalize") {
"finalize"
} else if func.name.ends_with("_constructed") {
"constructed"
} else {
return;
};
if func.parameters.is_empty() {
return;
}
let Some(gobject_ast::model::types::Parameter::Regular {
type_info: first_type,
..
}) = func.parameters.first()
else {
return;
};
if !first_type.is_pointer() {
return;
}
if !first_type.is_base_type("GObject") {
return;
}
if !self.has_chainup_call(&func.body_statements, method_type) {
violations.push(self.violation(
&file.path,
func.location.line,
1,
format!(
"{} must chain up to parent class (e.g., G_OBJECT_CLASS (parent_class)->{} (object))",
func.name, method_type
),
));
}
}
}
impl GObjectVirtualMethodsChainUp {
fn has_chainup_call(&self, statements: &[Statement], method_type: &str) -> bool {
for stmt in statements {
let mut found = false;
stmt.walk(&mut |s| {
s.visit_expressions(&mut |expr| {
if self.check_expression_for_chainup(expr, method_type) {
found = true;
}
});
});
if found {
return true;
}
}
false
}
fn check_expression_for_chainup(
&self,
expr: &gobject_ast::Expression,
method_type: &str,
) -> bool {
let field_access = match expr {
Expression::FieldAccess(f) => Some(f),
Expression::Call(call) => match &*call.function {
Expression::FieldAccess(f) => Some(f),
_ => None,
},
_ => None,
};
if let Some(field) = field_access
&& field.field == method_type
&& self.looks_like_parent_class_base(&field.base)
{
return true;
}
let mut found = false;
expr.walk(&mut |e| {
if !std::ptr::eq(e, expr) && self.check_expression_for_chainup(e, method_type) {
found = true;
}
});
found
}
fn looks_like_parent_class_base(&self, expr: &gobject_ast::Expression) -> bool {
match expr {
Expression::Identifier(id) => self.is_parent_class_name(&id.name),
Expression::Call(call) => {
let func_is_class_macro = matches!(&*call.function,
Expression::Identifier(id) if id.name.ends_with("_CLASS")
);
if !func_is_class_macro {
return false;
}
call.arguments.iter().any(|arg| {
let Argument::Expression(e) = arg;
matches!(&**e, Expression::Identifier(id) if self.is_parent_class_name(&id.name))
})
}
_ => false,
}
}
fn is_parent_class_name(&self, name: &str) -> bool {
let lower = name.to_lowercase();
if lower.contains("parent") && (lower.contains("class") || lower.contains("klass")) {
return true;
}
if lower.ends_with("_class") || lower.ends_with("_klass") {
return true;
}
lower == "klass"
}
}