Skip to main content

gobject_linter/rules/
unused_vfunc.rs

1use 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 &gt.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}