1use std::collections::HashMap;
2
3use gobject_ast::model::{
4 EnumInfo, EnumValue, Expression, FileModel, ParamSpecAssignment, PropertyEnumContext,
5 PropertyType, SwitchStatement, TopLevelItem, TypeInfo,
6};
7use heck::ToShoutySnakeCase;
8
9use crate::{
10 ast_context::AstContext,
11 config::Config,
12 rules::{Fix, Rule, Violation},
13};
14
15pub struct PropertyEnumConvention;
16
17impl Rule for PropertyEnumConvention {
18 fn name(&self) -> &'static str {
19 "property_enum_convention"
20 }
21
22 fn description(&self) -> &'static str {
23 "Enforce property enum conventions (typed or legacy style)"
24 }
25
26 fn category(&self) -> crate::rules::Category {
27 crate::rules::Category::Style
28 }
29
30 fn fixable(&self) -> bool {
31 true
32 }
33
34 fn config_options(&self) -> &'static [crate::rules::ConfigOption] {
35 &[crate::rules::ConfigOption {
36 name: "style",
37 option_type: "string",
38 default_value: "\"typed\"",
39 example_value: "\"legacy\"",
40 description: "Property enum style: \"typed\" (PROP_FOO = 1, no PROP_0/N_PROPS) or \"legacy\" (PROP_0, N_PROPS)",
41 }]
42 }
43
44 fn check_all(
45 &self,
46 ast_context: &AstContext,
47 config: &Config,
48 violations: &mut Vec<Violation>,
49 ) {
50 let rule_config = &config.rules.property_enum_convention;
52 let style = rule_config
53 .options
54 .get("style")
55 .and_then(|v| v.as_str())
56 .unwrap_or("typed");
57
58 match style {
59 "typed" => self.check_all_typed_style(ast_context, &config.style, violations),
60 "legacy" => self.check_all_legacy_style(ast_context, violations),
61 _ => {
62 self.check_all_legacy_style(ast_context, violations);
64 }
65 }
66 }
67}
68
69impl PropertyEnumConvention {
70 fn check_all_typed_style(
72 &self,
73 ast_context: &AstContext,
74 call_style: &crate::config::Style,
75 violations: &mut Vec<Violation>,
76 ) {
77 for (path, file) in ast_context.iter_all_files() {
78 let n_props_usage: HashMap<&str, usize> = file
81 .iter_property_enums()
82 .filter(|e| {
83 let has_prop_0 = e.values.first().is_some_and(EnumValue::is_prop_0);
85
86 let has_n_props_at_end = e.values.last().is_some_and(EnumValue::is_prop_last);
87
88 let has_n_props_in_middle = e
89 .values
90 .iter()
91 .enumerate()
92 .any(|(idx, v)| idx < e.values.len() - 1 && v.is_prop_last());
93
94 !has_n_props_in_middle && (has_prop_0 || has_n_props_at_end)
98 })
99 .filter_map(|e| {
100 e.values
101 .iter()
102 .find(|v| v.is_prop_last())
103 .map(|v| v.name.as_str())
104 })
105 .fold(HashMap::new(), |mut map, name| {
106 *map.entry(name).or_insert(0) += 1;
107 map
108 });
109
110 for enum_info in file.iter_property_enums() {
111 let has_prop_0 = enum_info.values.first().is_some_and(EnumValue::is_prop_0);
113
114 let has_n_props = enum_info.values.last().is_some_and(EnumValue::is_prop_last);
115
116 let has_n_props_in_middle = enum_info
119 .values
120 .iter()
121 .enumerate()
122 .any(|(idx, v)| idx < enum_info.values.len() - 1 && v.is_prop_last());
123
124 if !has_prop_0 && !has_n_props {
125 continue;
127 }
128
129 let prop_0_name = enum_info.values.first().unwrap().name.as_str();
131 let n_props_name = enum_info.values.last().unwrap().name.as_str();
132
133 if has_n_props_in_middle
137 || (has_n_props && self.n_props_used_in_switch_cases(file, n_props_name))
138 {
139 continue;
141 }
142
143 let Some(ctx) = file.resolve_property_enum_context(enum_info) else {
144 continue;
145 };
146
147 let property_map = self.build_property_override_map(&ctx.gobject_type.properties);
148
149 let last_real_prop_name = if has_n_props && enum_info.values.len() >= 2 {
153 let second_to_last = &enum_info.values[enum_info.values.len() - 2];
155
156 let n_props_value = enum_info.values.last().unwrap();
158 let n_props_points_to_override = if n_props_value.value_location.is_some()
159 && n_props_value.value.is_none()
160 {
161 n_props_value
162 .value_text()
163 .and_then(|value_text| property_map.get(value_text).copied())
164 .unwrap_or(false)
165 } else {
166 false
167 };
168
169 if n_props_points_to_override {
170 enum_info
172 .values
173 .iter()
174 .rev()
175 .skip(1) .find(|v| {
177 !v.is_prop_0()
178 && !v.is_prop_last()
179 && !property_map.get(&v.name).copied().unwrap_or(false)
180 })
181 .map_or_else(|| second_to_last.name.as_str(), |v| v.name.as_str())
182 } else {
183 second_to_last.name.as_str()
184 }
185 } else {
186 enum_info.values.last().unwrap().name.as_str()
187 };
188
189 let derived_enum_name = if enum_info.name.is_none() {
190 ctx.class_type_info
191 .and_then(|ti| self.derive_enum_name_from_class_type(ti))
192 } else {
193 None
194 };
195
196 let mut fixes = Vec::new();
197
198 if let Some(ref enum_name) = derived_enum_name {
200 fixes.extend(self.create_typedef_fixes(file, enum_info, enum_name));
201 }
202
203 if has_prop_0 && enum_info.values.len() >= 2 {
205 let prop_0 = &enum_info.values[0];
206 fixes.push(Fix::delete_line_and_trailing_blank(&prop_0.location));
207 }
208
209 if has_prop_0 && enum_info.values.len() >= 2 {
211 let first_real = &enum_info.values[1];
212
213 if first_real.value == Some(0)
215 && let Some(value_loc) = &first_real.value_location
216 {
217 fixes.push(Fix::new(
219 first_real.name_location.end_byte,
220 value_loc.end_byte,
221 " = 1".to_string(),
222 ));
223 } else {
224 fixes.push(Fix::new(
226 first_real.name_location.end_byte,
227 first_real.name_location.end_byte,
228 " = 1".to_string(),
229 ));
230 }
231 }
232
233 if has_n_props && enum_info.values.len() >= 2 {
235 let n_props = enum_info.values.last().unwrap();
236 fixes.push(Fix::delete_line_and_leading_blank(&n_props.location));
237 }
238
239 if has_n_props && n_props_usage.get(n_props_name).copied().unwrap_or(0) == 1 {
243 let array_names = self.find_and_fix_param_spec_arrays(
244 file,
245 n_props_name,
246 last_real_prop_name,
247 &mut fixes,
248 );
249
250 for func in file.iter_class_init_functions() {
252 for call in func.find_install_properties_calls() {
253 if let Some(arg) = call.get_arg(1)
255 && let Some(arg_str) = arg.location().as_str()
256 && arg_str == n_props_name
257 {
258 if let Some(array_arg) = call.get_arg(2)
260 && let Some(array_name) = array_arg.location().as_str()
261 && array_names.contains(&array_name)
262 {
263 let replacement =
264 call_style.format_call("G_N_ELEMENTS", &[array_name]);
265 fixes.push(Fix::new(
266 arg.location().start_byte,
267 arg.location().end_byte,
268 replacement,
269 ));
270 }
271 }
272 }
273 }
274 }
275
276 let enum_name = if let Some(ref name) = enum_info.name {
280 name.clone()
281 } else if let Some(ref derived) = derived_enum_name {
282 derived.clone()
283 } else {
284 ctx.class_type_info
285 .and_then(|ti| self.derive_enum_name_from_class_type(ti))
286 .unwrap_or_else(|| "UnknownProps".to_string())
287 };
288
289 if !enum_name.is_empty() {
290 if let Some(func_name) = ctx.get_property_func {
291 self.add_switch_cast_for_function(file, func_name, &enum_name, &mut fixes);
292 }
293 if let Some(func_name) = ctx.set_property_func {
294 self.add_switch_cast_for_function(file, func_name, &enum_name, &mut fixes);
295 }
296 }
297
298 if !fixes.is_empty() {
299 let message = if has_prop_0 && has_n_props {
300 format!(
301 "Use modern property enum pattern (remove {}, {}, start from = 1)",
302 prop_0_name, n_props_name
303 )
304 } else if has_prop_0 {
305 format!("Remove {} and start enum from = 1", prop_0_name)
306 } else {
307 format!("Remove {}", n_props_name)
308 };
309
310 violations.push(self.violation_with_fixes(
311 path,
312 enum_info.location.line,
313 1,
314 message,
315 fixes,
316 ));
317 }
318 }
319
320 for enum_info in file.iter_property_enums() {
323 let has_prop_0 = enum_info.values.first().is_some_and(EnumValue::is_prop_0);
324 let has_n_props = enum_info.values.last().is_some_and(EnumValue::is_prop_last);
325
326 if has_prop_0 || has_n_props {
328 continue;
329 }
330
331 let is_already_modern = enum_info
333 .values
334 .first()
335 .and_then(|v| v.value.as_ref())
336 .is_some_and(|val| *val == 1);
337
338 if !is_already_modern {
339 continue;
340 }
341
342 let Some(ctx) = file.resolve_property_enum_context(enum_info) else {
343 continue;
344 };
345
346 let property_map = self.build_property_override_map(&ctx.gobject_type.properties);
347
348 let last_real_prop = enum_info
350 .values
351 .iter()
352 .rev()
353 .find(|v| !property_map.get(&v.name).copied().unwrap_or(false));
354
355 let Some(last_real_prop) = last_real_prop else {
356 continue;
357 };
358
359 self.check_outdated_array_sizes(
361 file,
362 path,
363 enum_info,
364 &last_real_prop.name,
365 violations,
366 );
367
368 self.check_modern_enum_switch_casts(file, path, enum_info, &ctx, violations);
369 }
370 }
371 }
372
373 fn check_all_legacy_style(&self, ast_context: &AstContext, violations: &mut Vec<Violation>) {
375 for (path, file) in ast_context.iter_all_files() {
377 let existing_prop_zeros: std::collections::HashSet<&str> = file
379 .iter_property_enums()
380 .flat_map(|enum_info| &enum_info.values)
381 .filter_map(|val| {
382 if val.is_prop_0() {
383 Some(val.name.as_str())
384 } else {
385 None
386 }
387 })
388 .collect();
389
390 let mut will_add_unprefixed_prop_zero = existing_prop_zeros.contains("PROP_0");
391
392 for enum_info in file.iter_property_enums() {
394 let prefix = if will_add_unprefixed_prop_zero {
396 enum_info
397 .name
398 .as_ref()
399 .map(|name| name.to_shouty_snake_case() + "_")
400 } else {
401 None
402 };
403
404 let mut fixes = Vec::new();
405 let mut has_violations = false;
406 let mut violation_line = enum_info.location.line;
407 let mut message = String::new();
408
409 if let Some(first_val) = enum_info.values.first()
411 && !first_val.is_prop_0()
412 {
413 has_violations = true;
414 violation_line = enum_info.location.line;
415
416 let indent = first_val.location.extract_line_indentation();
418
419 let prop_zero_name = if let Some(ref p) = prefix {
420 format!("{}PROP_0", p)
421 } else {
422 "PROP_0".to_string()
423 };
424
425 let insertion = format!("{},\n{}", prop_zero_name, indent);
427 fixes.push(Fix::new(
428 first_val.location.start_byte,
429 first_val.location.start_byte,
430 insertion,
431 ));
432
433 if first_val.value == Some(0)
435 && let Some(value_loc) = &first_val.value_location
436 {
437 fixes.push(Fix::delete(
438 first_val.name_location.end_byte,
439 value_loc.end_byte,
440 ));
441 }
442
443 message = format!(
444 "Property enum should start with {}, not {}",
445 prop_zero_name, first_val.name
446 );
447
448 if prefix.is_none() {
450 will_add_unprefixed_prop_zero = true;
451 }
452 }
453
454 if let Some(last) = enum_info.values.last()
456 && !last.is_prop_last()
457 {
458 has_violations = true;
459
460 let indent = last.location.extract_line_indentation();
461
462 let n_props_name = if let Some(ref p) = prefix {
463 format!("{}N_PROPS", p)
464 } else {
465 "N_PROPS".to_string()
466 };
467
468 let insertion = format!(",\n{}{}", indent, n_props_name);
470 fixes.push(Fix::new(
471 last.location.end_byte,
472 last.location.end_byte,
473 insertion,
474 ));
475
476 if !message.is_empty() {
477 message.push_str(&format!(", and should end with {}", n_props_name));
478 } else {
479 message = format!("Property enum should end with {}", n_props_name);
480 }
481 }
482
483 if has_violations {
484 violations.push(self.violation_with_fixes(
485 path,
486 violation_line,
487 1,
488 message,
489 fixes,
490 ));
491 }
492 }
493 }
494 }
495
496 fn find_and_fix_param_spec_arrays<'a>(
500 &self,
501 file: &'a FileModel,
502 n_props_name: &str,
503 last_prop_name: &str,
504 fixes: &mut Vec<Fix>,
505 ) -> Vec<&'a str> {
506 let mut array_names = Vec::new();
507
508 for item in file.iter_all_items() {
509 let TopLevelItem::Declaration(decl) = item else {
510 continue;
511 };
512 if !decl.type_info.is_base_type("GParamSpec") || !decl.type_info.is_pointer() {
513 continue;
514 }
515 let Some(Expression::Identifier(size_id)) = &decl.array_size else {
516 continue;
517 };
518 if size_id.name != n_props_name {
519 continue;
520 }
521 fixes.push(Fix::new(
522 size_id.location.start_byte,
523 size_id.location.end_byte,
524 format!("{} + 1", last_prop_name),
525 ));
526 array_names.push(decl.name.as_str());
527 }
528
529 array_names
530 }
531
532 fn add_switch_cast_for_function(
534 &self,
535 file: &FileModel,
536 func_name: &str,
537 enum_name: &str,
538 fixes: &mut Vec<Fix>,
539 ) {
540 for func in file.iter_function_definitions() {
542 if func.name != func_name {
543 continue;
544 }
545
546 for stmt in &func.body_statements {
548 for switch_stmt in stmt.iter_switches() {
549 self.add_switch_cast_if_needed(switch_stmt, enum_name, fixes);
550 }
551 }
552 }
553 }
554
555 fn add_switch_cast_if_needed(
557 &self,
558 switch_stmt: &SwitchStatement,
559 enum_name: &str,
560 fixes: &mut Vec<Fix>,
561 ) {
562 let already_cast = match &switch_stmt.condition {
564 Expression::Cast(cast) => {
565 cast.type_info.base_type.contains(enum_name)
567 }
568 _ => false,
569 };
570
571 if !already_cast {
572 let cast_expr = format!("({}) ", enum_name);
574 fixes.push(Fix::new(
575 switch_stmt.condition_location.start_byte,
576 switch_stmt.condition_location.start_byte,
577 cast_expr,
578 ));
579 }
580 }
581
582 fn derive_enum_name_from_class_type(&self, type_info: &TypeInfo) -> Option<String> {
586 type_info
587 .base_type
588 .strip_suffix("Class")
589 .map(|base_name| format!("{}Props", base_name))
590 }
591
592 fn n_props_used_in_switch_cases(&self, file: &FileModel, n_props_name: &str) -> bool {
596 for func in file.iter_function_definitions() {
597 if !func.name.ends_with("_get_property") && !func.name.ends_with("_set_property") {
598 continue;
599 }
600
601 for stmt in &func.body_statements {
603 for switch_stmt in stmt.iter_switches() {
604 for case in &switch_stmt.cases {
606 if let Some(value_expr) = &case.label.value
607 && value_expr.contains_identifier(n_props_name)
608 {
609 return true;
610 }
611 }
612 }
613 }
614 }
615 false
616 }
617
618 fn build_property_override_map(
621 &self,
622 assignments: &[ParamSpecAssignment],
623 ) -> HashMap<String, bool> {
624 let mut property_map = HashMap::new();
625
626 for assignment in assignments {
627 if let Some(enum_value) = assignment.enum_value() {
630 let is_override =
631 matches!(assignment.property().property_type, PropertyType::Override);
632 property_map.insert(enum_value.to_string(), is_override);
633 }
634 }
635
636 property_map
637 }
638
639 fn check_modern_enum_switch_casts(
642 &self,
643 file: &FileModel,
644 path: &std::path::Path,
645 enum_info: &EnumInfo,
646 ctx: &PropertyEnumContext<'_>,
647 violations: &mut Vec<Violation>,
648 ) {
649 let enum_name = if let Some(ref name) = enum_info.name {
650 name.clone()
651 } else {
652 match ctx
653 .class_type_info
654 .and_then(|ti| self.derive_enum_name_from_class_type(ti))
655 {
656 Some(name) => name,
657 None => return,
658 }
659 };
660
661 let mut fixes = Vec::new();
662
663 if enum_info.name.is_none() {
665 fixes.extend(self.create_typedef_fixes(file, enum_info, &enum_name));
666 }
667
668 if let Some(func_name) = ctx.get_property_func {
670 self.add_switch_cast_for_function(file, func_name, &enum_name, &mut fixes);
671 }
672 if let Some(func_name) = ctx.set_property_func {
673 self.add_switch_cast_for_function(file, func_name, &enum_name, &mut fixes);
674 }
675
676 if !fixes.is_empty() {
678 let message = if enum_info.name.is_none() {
679 format!(
680 "Add typedef {} and use cast in switch statements for type safety",
681 enum_name
682 )
683 } else {
684 format!(
685 "Add ({}) cast to switch statements for type safety",
686 enum_name
687 )
688 };
689
690 violations.push(self.violation_with_fixes(
691 path,
692 enum_info.location.line,
693 1,
694 message,
695 fixes,
696 ));
697 }
698 }
699
700 fn create_typedef_fixes(
702 &self,
703 _file: &FileModel,
704 enum_info: &EnumInfo,
705 enum_name: &str,
706 ) -> Vec<Fix> {
707 let mut fixes = Vec::new();
708
709 fixes.push(Fix::new(
711 enum_info.location.start_byte,
712 enum_info.location.start_byte,
713 "typedef ".to_string(),
714 ));
715
716 let semicolon_end = enum_info.body_location.find_after(b';');
718 if semicolon_end > enum_info.body_location.end_byte {
719 fixes.push(Fix::new(
721 semicolon_end - 1,
722 semicolon_end,
723 format!(" {};", enum_name),
724 ));
725 }
726
727 fixes
728 }
729
730 fn check_outdated_array_sizes(
732 &self,
733 file: &FileModel,
734 path: &std::path::Path,
735 enum_info: &EnumInfo,
736 expected_last_prop: &str,
737 violations: &mut Vec<Violation>,
738 ) {
739 let property_names: std::collections::HashSet<&str> =
741 enum_info.values.iter().map(|v| v.name.as_str()).collect();
742
743 let arrays = file.find_typed_arrays("GParamSpec", true, None);
745
746 for decl in arrays {
747 if let Some(Expression::Binary(binary)) = &decl.array_size
749 && let Expression::Identifier(prop_id) = &*binary.left
750 && property_names.contains(prop_id.name.as_str())
751 {
752 if prop_id.name != expected_last_prop {
755 let replacement = format!("{} + 1", expected_last_prop);
756 let fix = Fix::new(
757 binary.location.start_byte,
758 binary.location.end_byte,
759 replacement,
760 );
761
762 violations.push(self.violation_with_fixes_at(
763 path,
764 &binary.location,
765 format!(
766 "GParamSpec array size uses outdated property (should be {} + 1)",
767 expected_last_prop
768 ),
769 vec![fix],
770 ));
771 }
772 }
773 }
774 }
775}