Skip to main content

gobject_linter/rules/
use_g_object_notify_by_pspec.rs

1use std::collections::HashMap;
2
3use gobject_ast::model::{
4    CallExpression, Expression, FileModel, FunctionDefItem, ParamSpecAssignment, Parameter,
5};
6
7use crate::{
8    ast_context::AstContext,
9    config::Config,
10    rules::{Fix, Rule, Violation},
11};
12
13pub struct UseGObjectNotifyByPspec;
14
15struct PropertyEntry<'a> {
16    assignment: &'a ParamSpecAssignment,
17    class_prefix: &'a str,
18}
19
20impl Rule for UseGObjectNotifyByPspec {
21    fn name(&self) -> &'static str {
22        "use_g_object_notify_by_pspec"
23    }
24
25    fn description(&self) -> &'static str {
26        "Suggest g_object_notify_by_pspec instead of g_object_notify for better performance"
27    }
28
29    fn category(&self) -> crate::rules::Category {
30        crate::rules::Category::Perf
31    }
32
33    fn fixable(&self) -> bool {
34        true
35    }
36
37    fn min_glib_version(&self) -> Option<(u32, u32)> {
38        Some((2, 26))
39    }
40
41    fn check_all(
42        &self,
43        ast_context: &AstContext,
44        config: &Config,
45        violations: &mut Vec<Violation>,
46    ) {
47        for (path, file) in ast_context.iter_all_files() {
48            let property_map = self.build_property_map(file);
49
50            for func in file.iter_function_definitions() {
51                for call in func.find_calls(&["g_object_notify"]) {
52                    self.check_call(path, call, &property_map, func, &config.style, violations);
53                }
54            }
55        }
56    }
57}
58
59impl UseGObjectNotifyByPspec {
60    #[allow(clippy::too_many_arguments)]
61    fn check_call(
62        &self,
63        file_path: &std::path::Path,
64        call: &CallExpression,
65        property_map: &HashMap<&str, Vec<PropertyEntry>>,
66        func: &FunctionDefItem,
67        style: &crate::config::Style,
68        violations: &mut Vec<Violation>,
69    ) {
70        if call.arguments.len() != 2 {
71            return;
72        }
73
74        let Some(property_expr) = call.get_arg(1) else {
75            return;
76        };
77        let Expression::StringLiteral(string_lit) = property_expr else {
78            return;
79        };
80
81        let property_name = string_lit.value.trim_matches('"');
82
83        let Some(candidates) = property_map.get(property_name) else {
84            // Property not found in any GObject type
85            let property_constant = self.property_name_to_constant(property_name);
86            violations.push(self.violation_at(
87                file_path,
88                &call.location,
89                format!(
90                    "Use g_object_notify_by_pspec(obj, properties[{}]) instead of g_object_notify(obj, \"{}\") for better performance",
91                    property_constant, property_name
92                ),
93            ));
94            return;
95        };
96
97        // Filter to only array-subscript candidates (the only ones we can fix)
98        let fixable: Vec<_> = candidates
99            .iter()
100            .filter(|e| matches!(e.assignment, ParamSpecAssignment::ArraySubscript { .. }))
101            .collect();
102
103        if fixable.is_empty() {
104            // Property exists but only as override/direct-install — can't use by_pspec
105            return;
106        }
107
108        let disambiguated = if fixable.len() > 1 {
109            self.disambiguate_by_type(call, func, &fixable)
110        } else {
111            Some(fixable[0])
112        };
113
114        if let Some(entry) = disambiguated {
115            let ParamSpecAssignment::ArraySubscript {
116                array_name,
117                enum_value,
118                ..
119            } = entry.assignment
120            else {
121                return;
122            };
123
124            let Some(obj_expr) = call.get_arg(0) else {
125                return;
126            };
127            let Some(obj_str) = obj_expr.location().as_str() else {
128                return;
129            };
130
131            let pspec = format!("{}[{}]", array_name, enum_value);
132            let replacement = style.format_call("g_object_notify_by_pspec", &[obj_str, &pspec]);
133
134            violations.push(self.violation_with_fix_at(
135                file_path,
136                &call.location,
137                format!(
138                    "Use g_object_notify_by_pspec({}, {}[{}]) instead of g_object_notify({}, \"{}\") for better performance",
139                    obj_str, array_name, enum_value, obj_str, property_name
140                ),
141                Fix::new(call.location.start_byte, call.location.end_byte, replacement),
142            ));
143        } else {
144            let property_constant = self.property_name_to_constant(property_name);
145            let ParamSpecAssignment::ArraySubscript { array_name, .. } = fixable[0].assignment
146            else {
147                return;
148            };
149            violations.push(self.violation_at(
150                file_path,
151                &call.location,
152                format!(
153                    "Use g_object_notify_by_pspec(obj, {}[{}]) instead of g_object_notify(obj, \"{}\") for better performance (ambiguous: multiple classes define this property)",
154                    array_name, property_constant, property_name
155                ),
156            ));
157        }
158    }
159
160    fn build_property_map<'a>(
161        &self,
162        file: &'a FileModel,
163    ) -> HashMap<&'a str, Vec<PropertyEntry<'a>>> {
164        let mut map: HashMap<&str, Vec<PropertyEntry>> = HashMap::new();
165
166        for gt in file.iter_all_gobject_types() {
167            for assignment in &gt.properties {
168                map.entry(assignment.property().name.as_str())
169                    .or_default()
170                    .push(PropertyEntry {
171                        assignment,
172                        class_prefix: &gt.function_prefix,
173                    });
174            }
175        }
176
177        map
178    }
179
180    fn disambiguate_by_type<'a>(
181        &self,
182        call: &CallExpression,
183        func: &FunctionDefItem,
184        candidates: &[&'a PropertyEntry<'a>],
185    ) -> Option<&'a PropertyEntry<'a>> {
186        let obj_expr = call.get_arg(0)?;
187        let obj_identifier = obj_expr.extract_identifier_name()?;
188
189        let param_type = func.get_param_by_name(obj_identifier).and_then(|p| {
190            if let Parameter::Regular { type_info, .. } = p {
191                Some(&type_info.base_type)
192            } else {
193                None
194            }
195        })?;
196
197        use heck::ToSnakeCase;
198        let full = param_type.to_snake_case();
199        if let Some(found) = candidates.iter().find(|e| e.class_prefix == full) {
200            return Some(found);
201        }
202        let trimmed = param_type
203            .trim_end_matches("Object")
204            .trim_end_matches("Class")
205            .to_snake_case();
206        candidates
207            .iter()
208            .find(|e| e.class_prefix == trimmed)
209            .copied()
210    }
211
212    /// Convert property-name to PROP_NAME constant style
213    fn property_name_to_constant(&self, property_name: &str) -> String {
214        let mut result = String::with_capacity(property_name.len() + 5);
215        result.push_str("PROP_");
216
217        for c in property_name.chars() {
218            if c == '-' {
219                result.push('_');
220            } else {
221                result.push(c.to_ascii_uppercase());
222            }
223        }
224
225        result
226    }
227}