gobject_linter/rules/
use_g_object_notify_by_pspec.rs1use 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 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 let fixable: Vec<_> = candidates
99 .iter()
100 .filter(|e| matches!(e.assignment, ParamSpecAssignment::ArraySubscript { .. }))
101 .collect();
102
103 if fixable.is_empty() {
104 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 >.properties {
168 map.entry(assignment.property().name.as_str())
169 .or_default()
170 .push(PropertyEntry {
171 assignment,
172 class_prefix: >.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 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}