gobject_linter/rules/
unused_vfunc.rs1use std::collections::{HashMap, HashSet};
2
3use gobject_ast::model::{Expression, TypeDefItem, UnaryOp};
4
5use crate::{
6 ast_context::AstContext,
7 config::Config,
8 rules::{Category, Rule, Violation},
9};
10
11pub struct UnusedVfunc;
12
13impl Rule for UnusedVfunc {
14 fn name(&self) -> &'static str {
15 "unused_vfunc"
16 }
17
18 fn description(&self) -> &'static str {
19 "Detect virtual methods assigned in class_init but never called through the vtable"
20 }
21
22 fn category(&self) -> Category {
23 Category::Suspicious
24 }
25
26 fn check_all(
27 &self,
28 ast_context: &AstContext,
29 _config: &Config,
30 violations: &mut Vec<Violation>,
31 ) {
32 let mut vfunc_fields: HashMap<&str, Vec<(&std::path::Path, usize, usize, String)>> =
33 HashMap::new();
34 let mut signal_fields: HashSet<&str> = HashSet::new();
35
36 for (_path, file) in ast_context.iter_all_files() {
37 for gt in file.iter_all_gobject_types() {
38 let Some(class_struct_name) = gt.class_struct_name() else {
39 continue;
40 };
41
42 for sig in >.signals {
43 if let Some(offset) = &sig.class_offset {
44 signal_fields.insert(offset.field.as_str());
45 }
46 }
47
48 let class_struct_vfuncs = match file.find_class_struct_for(gt) {
49 Some(TypeDefItem::Struct { vfuncs, .. }) => vfuncs,
50 _ => continue,
51 };
52
53 let assigned_vfuncs = file.resolve_class_init_vfuncs(gt);
54 let class_init_name = gt.class_init_function_name();
55
56 for (class_type, field) in assigned_vfuncs.keys() {
57 if is_gobject_builtin(field) || class_type != &class_struct_name {
58 continue;
59 }
60 if let Some(vf) = class_struct_vfuncs.iter().find(|v| v.name == *field) {
61 vfunc_fields.entry(field).or_default().push((
62 &file.path,
63 vf.location.line,
64 vf.location.column,
65 class_init_name.clone(),
66 ));
67 }
68 }
69 }
70 }
71
72 for field in &signal_fields {
73 vfunc_fields.remove(field);
74 }
75
76 if vfunc_fields.is_empty() {
77 return;
78 }
79
80 let mut called_fields: HashSet<&str> = HashSet::new();
81
82 for (_path, file) in ast_context.iter_all_files() {
83 for func in file.iter_function_definitions() {
84 for stmt in &func.body_statements {
85 stmt.walk_expressions(&mut |expr| {
86 expr.walk(&mut |e| {
87 if let Expression::Call(call) = e {
88 let func = match &*call.function {
89 Expression::FieldAccess(fa) => Some(fa),
90 Expression::Unary(u) if u.operator == UnaryOp::Dereference => {
91 if let Expression::FieldAccess(fa) = &*u.operand {
92 Some(fa)
93 } else {
94 None
95 }
96 }
97 _ => None,
98 };
99 if let Some(fa) = func {
100 let field = fa.field.as_str();
101 if vfunc_fields.contains_key(field) {
102 called_fields.insert(field);
103 }
104 }
105 }
106 });
107 });
108 }
109 }
110 }
111
112 for (field, infos) in &vfunc_fields {
113 if called_fields.contains(field) {
114 continue;
115 }
116 for (file_path, line, column, class_init_name) in infos {
117 violations.push(self.violation(
118 file_path,
119 *line,
120 *column,
121 format!(
122 "Virtual method '{}' is assigned in {}() but never called through the class vtable",
123 field, class_init_name
124 ),
125 ));
126 }
127 }
128 }
129}
130
131fn is_gobject_builtin(field: &str) -> bool {
132 matches!(
133 field,
134 "dispose"
135 | "finalize"
136 | "constructed"
137 | "get_property"
138 | "set_property"
139 | "notify"
140 | "dispatch_properties_changed"
141 | "constructor"
142 )
143}