1use gobject_ast::model::{
2 CallExpression, EnumInfo, Expression, FileModel, FunctionDefItem, GType, ParamSpecAssignment,
3 SourceLocation, Statement,
4};
5
6use crate::{
7 ast_context::AstContext,
8 config::Config,
9 rules::{Fix, Rule, Violation},
10};
11
12pub struct UseGObjectClassInstallProperties;
13
14impl Rule for UseGObjectClassInstallProperties {
15 fn name(&self) -> &'static str {
16 "use_g_object_class_install_properties"
17 }
18
19 fn description(&self) -> &'static str {
20 "Suggest g_object_class_install_properties for multiple g_object_class_install_property calls"
21 }
22
23 fn category(&self) -> crate::rules::Category {
24 crate::rules::Category::Complexity
25 }
26
27 fn fixable(&self) -> bool {
28 true
29 }
30
31 fn min_glib_version(&self) -> Option<(u32, u32)> {
32 Some((2, 26))
33 }
34
35 fn check_enum(
36 &self,
37 ast_context: &AstContext,
38 config: &Config,
39 enum_info: &EnumInfo,
40 file: &FileModel,
41 violations: &mut Vec<Violation>,
42 ) {
43 if !enum_info.is_property_enum() {
44 return;
45 }
46
47 let Some(mut ctx) = file.resolve_property_enum_context(enum_info) else {
48 return;
49 };
50 if ctx.gobject_type.interfaces.is_empty()
52 && let Some(define) = file
53 .iter_all_gobject_types()
54 .find(|gt| gt.type_name == ctx.gobject_type.type_name && gt.kind.is_define())
55 {
56 ctx.gobject_type = define;
57 }
58
59 let individual_properties: Vec<_> = ctx
60 .gobject_type
61 .properties
62 .iter()
63 .filter(|p| !matches!(p, ParamSpecAssignment::ArraySubscript { .. }))
64 .collect();
65 if individual_properties.is_empty() {
66 return;
67 }
68
69 let fixes = self.generate_fixes(
70 ast_context,
71 file,
72 ctx.class_init,
73 ctx.gobject_type,
74 &individual_properties,
75 enum_info,
76 &config.style,
77 );
78
79 let location = individual_properties
80 .first()
81 .map(|p| p.statement_location())
82 .unwrap();
83 let message = if fixes.is_empty() {
84 format!(
85 "Consider using g_object_class_install_properties() instead of {} individual property installation calls",
86 individual_properties.len()
87 )
88 } else {
89 format!(
90 "Use g_object_class_install_properties() instead of {} individual property installation calls",
91 individual_properties.len()
92 )
93 };
94
95 violations.push(self.violation_with_fixes_at(&file.path, location, message, fixes));
96 }
97}
98
99impl UseGObjectClassInstallProperties {
100 #[allow(clippy::too_many_arguments)]
101 fn generate_fixes(
102 &self,
103 ast_context: &AstContext,
104 file: &FileModel,
105 class_init: &FunctionDefItem,
106 gobject_type: &gobject_ast::model::GObjectType,
107 assignments: &[&ParamSpecAssignment],
108 property_enum: &EnumInfo,
109 style: &crate::config::Style,
110 ) -> Vec<Fix> {
111 let mut fixes = Vec::new();
112
113 let install_calls: Vec<&CallExpression> = assignments
114 .iter()
115 .filter_map(|a| match a {
116 ParamSpecAssignment::DirectInstall { install_call, .. } => Some(install_call),
117 ParamSpecAssignment::Variable { install_call, .. } => install_call.as_ref(),
118 _ => None,
119 })
120 .collect();
121 let override_calls: Vec<&CallExpression> = assignments
122 .iter()
123 .filter_map(|a| match a {
124 ParamSpecAssignment::OverrideProperty { call, .. } => Some(call),
125 _ => None,
126 })
127 .collect();
128
129 let has_convertible_overrides = !override_calls.is_empty()
131 && override_calls.iter().any(|call| {
132 call.get_arg(2)
133 .and_then(|a| a.location().as_str())
134 .map(|s| s.trim_matches('"'))
135 .and_then(|name| {
136 ast_context
137 .project
138 .find_interface_for_property(gobject_type, name)
139 })
140 .is_some()
141 });
142 if install_calls.is_empty() && !has_convertible_overrides {
143 return fixes;
144 }
145
146 let multiple_types = file
147 .iter_all_gobject_types()
148 .filter(|gt| gt.type_name != gobject_type.type_name)
149 .any(|gt| !gt.properties.is_empty());
150
151 let param_spec_assignments: Vec<_> = assignments
153 .iter()
154 .filter_map(|a| {
155 if let ParamSpecAssignment::Variable {
156 variable_name,
157 statement_location,
158 call,
159 ..
160 } = a
161 {
162 Some((variable_name.as_str(), statement_location, call))
163 } else {
164 None
165 }
166 })
167 .collect();
168
169 let (n_props_name, sentinel_fixes) = self.resolve_enum_sentinel(
170 property_enum,
171 has_convertible_overrides,
172 &override_calls,
173 gobject_type,
174 multiple_types,
175 );
176 fixes.extend(sentinel_fixes);
177
178 let (array_name, array_fixes) = self.resolve_array(
179 file,
180 property_enum,
181 assignments,
182 gobject_type,
183 multiple_types,
184 &n_props_name,
185 );
186 fixes.extend(array_fixes);
187
188 let object_class_var = class_init
189 .iter_local_declarations()
190 .find(|decl| decl.type_info.base_type == "GObjectClass")
191 .map_or("object_class", |decl| decl.name.as_str());
192
193 let all_calls_for_indent = install_calls.first().or(override_calls.first()).copied();
194 let indentation = if let Some(first_call) = all_calls_for_indent {
195 if let Some(stmt) = self.find_statement_containing_call(
196 &class_init.body_statements,
197 first_call.location.start_byte,
198 ) {
199 stmt.location().extract_indentation()
200 } else {
201 " ".to_string()
202 }
203 } else {
204 " ".to_string()
205 };
206
207 let (call_fixes, param_spec_vars) = self.convert_install_calls(
208 &install_calls,
209 ¶m_spec_assignments,
210 class_init,
211 &array_name,
212 &indentation,
213 style,
214 );
215 fixes.extend(call_fixes);
216
217 fixes.extend(self.convert_override_calls(
218 &override_calls,
219 ast_context,
220 gobject_type,
221 class_init,
222 &array_name,
223 &indentation,
224 style,
225 ));
226
227 for var_name in param_spec_vars {
228 if let Some(decl) = class_init
229 .body_statements
230 .iter()
231 .flat_map(Statement::iter_declarations)
232 .find(|decl| decl.name == var_name && decl.type_info.base_type == "GParamSpec")
233 {
234 fixes.push(Fix::delete_line(&decl.location));
235 }
236 }
237
238 fixes.extend(self.ensure_install_properties_call(
239 class_init,
240 &install_calls,
241 &override_calls,
242 has_convertible_overrides,
243 object_class_var,
244 &n_props_name,
245 &array_name,
246 &indentation,
247 style,
248 ));
249
250 fixes
251 }
252
253 fn resolve_enum_sentinel(
254 &self,
255 property_enum: &EnumInfo,
256 has_convertible_overrides: bool,
257 override_calls: &[&CallExpression],
258 gobject_type: &gobject_ast::model::GObjectType,
259 multiple_types: bool,
260 ) -> (String, Vec<Fix>) {
261 let mut fixes = Vec::new();
262
263 let split_sentinel = property_enum
264 .values
265 .iter()
266 .enumerate()
267 .find_map(|(i, value)| {
268 if let Some(Expression::Identifier(id)) = &value.value_expr
269 && let Some(sentinel) =
270 property_enum.values[..i].iter().find(|v| v.name == id.name)
271 {
272 Some((sentinel, value))
273 } else {
274 None
275 }
276 });
277
278 let mut deleted_sentinel_name: Option<&str> = None;
279 if has_convertible_overrides
280 && let Some((sentinel_value, first_override_value)) = &split_sentinel
281 {
282 if first_override_value.is_prop_last() {
283 if let Some(value_loc) = &first_override_value.value_location {
285 let eq_start = value_loc.find_before(b'=');
286 fixes.push(Fix::new(eq_start, value_loc.end_byte, String::new()));
287 }
288 } else {
289 fixes.push(Fix::delete_line_and_trailing_blank(
291 &sentinel_value.location,
292 ));
293 deleted_sentinel_name = Some(&sentinel_value.name);
294 if let Some(value_loc) = &first_override_value.value_location {
295 let eq_start = value_loc.find_before(b'=');
296 fixes.push(Fix::new(eq_start, value_loc.end_byte, String::new()));
297 }
298 }
299 }
300
301 let n_props_value =
302 property_enum.values.iter().rev().find(|v| {
303 v.is_prop_last() && deleted_sentinel_name.is_none_or(|name| v.name != name)
304 });
305
306 let n_props_mispositioned = has_convertible_overrides
307 && n_props_value.is_some_and(|nv| {
308 let nv_idx = property_enum
309 .values
310 .iter()
311 .position(|v| v.name == nv.name)
312 .unwrap();
313 override_calls.iter().any(|call| {
314 call.get_arg(1)
315 .and_then(|a| a.location().as_str())
316 .and_then(|name| property_enum.values.iter().position(|v| v.name == name))
317 .is_some_and(|idx| idx > nv_idx)
318 })
319 });
320
321 if n_props_mispositioned && let Some(nv) = n_props_value {
322 fixes.push(Fix::delete_line_and_trailing_blank(&nv.location));
323 }
324
325 let n_props_name = if let Some(n_props) = n_props_value
326 && !n_props_mispositioned
327 {
328 n_props.name.clone()
329 } else {
330 let n_props_name = n_props_value
331 .map(|v| v.name.clone())
332 .or_else(|| deleted_sentinel_name.map(std::string::ToString::to_string))
333 .unwrap_or_else(|| {
334 let base_name = property_enum
335 .values
336 .first()
337 .and_then(|v| v.name.rfind("PROP_").map(|pos| &v.name[..pos]))
338 .map_or("N_PROPS".to_string(), |prefix| {
339 if prefix.is_empty() {
340 "N_PROPS".to_string()
341 } else {
342 format!("{}N_PROPS", prefix)
343 }
344 });
345 if multiple_types && base_name == "N_PROPS" {
346 format!("{}_N_PROPS", gobject_type.function_prefix.to_uppercase())
347 } else {
348 base_name
349 }
350 });
351
352 let last_value = property_enum.values.last().unwrap();
353 let value_indentation = last_value.location.extract_indentation();
354
355 let comma_end = last_value.location.find_after(b',');
356 let (insertion_pos, needs_comma) = if comma_end > last_value.location.end_byte {
357 (comma_end, false)
358 } else {
359 (last_value.location.end_byte, true)
360 };
361
362 let n_props_decl = if needs_comma {
363 format!(",\n{}{}", value_indentation, n_props_name)
364 } else {
365 format!("\n{}{}", value_indentation, n_props_name)
366 };
367
368 fixes.push(Fix::new(insertion_pos, insertion_pos, n_props_decl));
369
370 n_props_name
371 };
372
373 (n_props_name, fixes)
374 }
375
376 fn resolve_array(
377 &self,
378 file: &FileModel,
379 property_enum: &EnumInfo,
380 assignments: &[&ParamSpecAssignment],
381 gobject_type: &gobject_ast::model::GObjectType,
382 multiple_types: bool,
383 n_props_name: &str,
384 ) -> (String, Vec<Fix>) {
385 let mut fixes = Vec::new();
386
387 let enum_member_names: Vec<&str> = property_enum
388 .values
389 .iter()
390 .map(|v| v.name.as_str())
391 .collect();
392 let existing_array_name = assignments
393 .iter()
394 .find_map(|a| {
395 if let ParamSpecAssignment::ArraySubscript { array_name, .. } = a {
396 Some(array_name.as_str())
397 } else {
398 None
399 }
400 })
401 .or_else(|| {
402 file.find_typed_arrays("GParamSpec", true, None)
403 .into_iter()
404 .find(|d| {
405 matches!(&d.array_size, Some(Expression::Identifier(id)) if enum_member_names.contains(&id.name.as_str()))
406 })
407 .map(|d| d.name.as_str())
408 });
409
410 let array_name = if let Some(name) = existing_array_name {
411 name.to_string()
412 } else if multiple_types {
413 format!("{}_props", gobject_type.function_prefix)
414 } else {
415 "props".to_string()
416 };
417
418 if existing_array_name.is_some() {
419 if let Some(decl) = file
420 .find_typed_arrays("GParamSpec", true, None)
421 .into_iter()
422 .find(|d| d.name == array_name)
423 && let Some(Expression::Identifier(size_id)) = &decl.array_size
424 && size_id.name != n_props_name
425 {
426 fixes.push(Fix::new(
427 size_id.location.start_byte,
428 size_id.location.end_byte,
429 n_props_name.to_string(),
430 ));
431 }
432 } else {
433 let semicolon_end = property_enum.location.find_after(b';');
434 let insertion_pos = if semicolon_end > property_enum.location.end_byte {
435 semicolon_end
436 } else {
437 property_enum.location.end_byte
438 };
439
440 let array_decl = format!(
441 "\n\nstatic GParamSpec *{}[{}] = {{ NULL, }};",
442 array_name, n_props_name
443 );
444 fixes.push(Fix::new(insertion_pos, insertion_pos, array_decl));
445 }
446
447 (array_name, fixes)
448 }
449
450 fn convert_install_calls<'a>(
451 &self,
452 install_calls: &[&'a CallExpression],
453 param_spec_assignments: &[(&str, &SourceLocation, &CallExpression)],
454 class_init: &FunctionDefItem,
455 array_name: &str,
456 indentation: &str,
457 style: &crate::config::Style,
458 ) -> (Vec<Fix>, std::collections::HashSet<&'a str>) {
459 let mut fixes = Vec::new();
460 let mut param_spec_vars = std::collections::HashSet::new();
461
462 for call in install_calls {
464 let Some(prop_id_arg) = call.get_arg(1) else {
466 continue;
467 };
468 let Some(prop_id) = prop_id_arg.location().as_str() else {
469 continue;
470 };
471
472 let Some(param_spec_arg) = call.get_arg(2) else {
474 continue;
475 };
476
477 let (param_spec, delete_install_call) =
479 if let Expression::Call(param_spec_call) = param_spec_arg {
480 let func_name = param_spec_call.function_name();
482 let paren = if style.space_before_paren { " (" } else { "(" };
483 let new_line_prefix =
484 format!("{}[{}] = {}{}", array_name, prop_id, func_name, paren);
485 let target_column = indentation.len() + new_line_prefix.len();
486
487 let Some(param_spec_text) = param_spec_arg.location().as_str() else {
488 continue;
489 };
490 (
491 self.reindent_multiline(param_spec_text, target_column),
492 false,
493 )
494 } else {
495 let Some(var_name) = param_spec_arg.location().as_str() else {
498 continue;
499 };
500
501 let assignment = param_spec_assignments
502 .iter()
503 .filter(|(name, stmt_loc, _)| {
504 *name == var_name && stmt_loc.start_byte < call.location.start_byte
505 })
506 .max_by_key(|(_, stmt_loc, _)| stmt_loc.start_byte);
507
508 if let Some((_, statement_location, g_param_spec_call)) = assignment {
509 param_spec_vars.insert(var_name);
510
511 let func_name = g_param_spec_call.function_name();
512 let paren = if style.space_before_paren { " (" } else { "(" };
513 let new_line_prefix =
514 format!("{}[{}] = {}{}", array_name, prop_id, func_name, paren);
515 let assignment_indent = statement_location.extract_indentation();
516 let target_column = assignment_indent.len() + new_line_prefix.len();
517
518 let Some(param_spec_text) = g_param_spec_call.location.as_str() else {
519 continue;
520 };
521
522 let replacement = format!(
523 "{}[{}] = {};",
524 array_name,
525 prop_id,
526 self.reindent_multiline(param_spec_text, target_column)
527 );
528 fixes.push(Fix::new(
529 statement_location.start_byte,
530 statement_location.find_semicolon_end(),
531 replacement,
532 ));
533
534 (String::new(), true)
535 } else {
536 let Some(param_spec_text) = param_spec_arg.location().as_str() else {
537 continue;
538 };
539 (param_spec_text.to_owned(), false)
540 }
541 };
542
543 let Some(stmt) = self.find_statement_containing_call(
545 &class_init.body_statements,
546 call.location.start_byte,
547 ) else {
548 continue;
549 };
550
551 if delete_install_call {
552 fixes.push(Fix::delete_line(stmt.location()));
554 } else {
555 let replacement = format!("{}[{}] = {};", array_name, prop_id, param_spec);
557 fixes.push(Fix::new(
558 stmt.location().start_byte,
559 stmt.location().find_semicolon_end(),
560 replacement,
561 ));
562 }
563 }
564
565 (fixes, param_spec_vars)
566 }
567
568 #[allow(clippy::too_many_arguments)]
569 fn convert_override_calls(
570 &self,
571 override_calls: &[&CallExpression],
572 ast_context: &AstContext,
573 gobject_type: &gobject_ast::model::GObjectType,
574 class_init: &FunctionDefItem,
575 array_name: &str,
576 indentation: &str,
577 style: &crate::config::Style,
578 ) -> Vec<Fix> {
579 let mut fixes = Vec::new();
580
581 for call in override_calls {
582 let Some(prop_id_arg) = call.get_arg(1) else {
583 continue;
584 };
585 let Some(prop_id) = prop_id_arg.location().as_str() else {
586 continue;
587 };
588
589 let Some(prop_name_arg) = call.get_arg(2) else {
590 continue;
591 };
592 let Some(prop_name) = prop_name_arg.location().as_str() else {
593 continue;
594 };
595 let prop_name = prop_name.trim_matches('"');
596
597 let Some(stmt) = self.find_statement_containing_call(
598 &class_init.body_statements,
599 call.location.start_byte,
600 ) else {
601 continue;
602 };
603
604 let resolved = ast_context
605 .project
606 .find_interface_for_property(gobject_type, prop_name);
607 if let Some(iface_gtype) = resolved {
608 let GType::Identifier(iface_type_str) = iface_gtype else {
609 continue;
610 };
611
612 let iface_ref =
613 style.format_call("g_type_default_interface_ref", &[iface_type_str]);
614 let prop_name_quoted = format!("\"{}\"", prop_name);
615 let find_prop = style.format_call(
616 "g_object_interface_find_property",
617 &[&iface_ref, &prop_name_quoted],
618 );
619 let paren = if style.space_before_paren { " (" } else { "(" };
620 let replacement = format!(
621 "{}[{}] = g_param_spec_override{}\"{}\",\n{} {});",
622 array_name, prop_id, paren, prop_name, indentation, find_prop
623 );
624 fixes.push(Fix::new(
625 stmt.location().start_byte,
626 stmt.location().find_semicolon_end(),
627 replacement,
628 ));
629 }
630 }
631
632 fixes
633 }
634
635 #[allow(clippy::too_many_arguments)]
636 fn ensure_install_properties_call(
637 &self,
638 class_init: &FunctionDefItem,
639 install_calls: &[&CallExpression],
640 override_calls: &[&CallExpression],
641 has_convertible_overrides: bool,
642 object_class_var: &str,
643 n_props_name: &str,
644 array_name: &str,
645 indentation: &str,
646 style: &crate::config::Style,
647 ) -> Vec<Fix> {
648 let mut fixes = Vec::new();
649
650 let last_call = if has_convertible_overrides {
651 [
652 install_calls.last().copied(),
653 override_calls.last().copied(),
654 ]
655 .into_iter()
656 .flatten()
657 .max_by_key(|c| c.location.start_byte)
658 } else {
659 install_calls.last().copied()
660 };
661 let Some(last_call) = last_call else {
662 return fixes;
663 };
664 let Some(last_stmt) = self.find_statement_containing_call(
665 &class_init.body_statements,
666 last_call.location.start_byte,
667 ) else {
668 return fixes;
669 };
670 let last_stmt_end = last_stmt.location().find_semicolon_end();
671
672 let existing_install = class_init
673 .find_calls_matching(|name| name == "g_object_class_install_properties")
674 .into_iter()
675 .next();
676
677 if let Some(existing) = existing_install {
678 if existing.location.start_byte < last_call.location.start_byte {
679 let Some(existing_stmt) = self.find_statement_containing_call(
680 &class_init.body_statements,
681 existing.location.start_byte,
682 ) else {
683 return fixes;
684 };
685 fixes.push(Fix::delete_line_and_trailing_blank(
686 existing_stmt.location(),
687 ));
688
689 let call = style.format_call_stmt(
690 "g_object_class_install_properties",
691 &[object_class_var, n_props_name, array_name],
692 );
693 let install_properties_call = format!("\n\n{}{}", indentation, call);
694 fixes.push(Fix::new(
695 last_stmt_end,
696 last_stmt_end,
697 install_properties_call,
698 ));
699 }
700 } else {
701 let call = style.format_call_stmt(
702 "g_object_class_install_properties",
703 &[object_class_var, n_props_name, array_name],
704 );
705 let install_properties_call = format!("\n\n{}{}", indentation, call);
706 fixes.push(Fix::new(
707 last_stmt_end,
708 last_stmt_end,
709 install_properties_call,
710 ));
711 }
712
713 fixes
714 }
715
716 fn find_statement_containing_call<'a>(
717 &self,
718 statements: &'a [Statement],
719 call_start_byte: usize,
720 ) -> Option<&'a Statement> {
721 for stmt in statements {
722 let loc = stmt.location();
723 if call_start_byte >= loc.start_byte && call_start_byte < loc.end_byte {
724 return Some(stmt);
725 }
726 }
727 None
728 }
729
730 fn reindent_multiline(&self, text: &str, target_column: usize) -> String {
733 let lines: Vec<&str> = text.lines().collect();
734 if lines.len() <= 1 {
735 return text.to_string();
736 }
737
738 let continuation_indent = " ".repeat(target_column);
739
740 let mut result = String::new();
741 for (i, line) in lines.iter().enumerate() {
742 if i == 0 {
743 result.push_str(line);
744 } else {
745 result.push('\n');
746 result.push_str(&continuation_indent);
747 result.push_str(line.trim_start());
748 }
749 }
750
751 result
752 }
753}