Skip to main content

gobject_linter/rules/
property_enum_convention.rs

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        // Get style configuration (default to "typed")
51        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                // Invalid style, default to legacy
63                self.check_all_legacy_style(ast_context, violations);
64            }
65        }
66    }
67}
68
69impl PropertyEnumConvention {
70    /// Check using modern typed enum style (PROP_FOO = 1, no PROP_0/N_PROPS)
71    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            // Collect N_PROPS names from enums that will be transformed
79            // (skip override pattern enums and already-modern enums)
80            let n_props_usage: HashMap<&str, usize> = file
81                .iter_property_enums()
82                .filter(|e| {
83                    // Apply same checks as main loop to see if this enum will be transformed
84                    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                    // Only count if it will be transformed (not in-middle pattern, not already
95                    // modern) Note: N_PROPS = PROP_X where PROP_X is override
96                    // is still transformable
97                    !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                // Check if this uses the old pattern: PROP_0 at start and N_PROPS at end
112                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                // Check if N_PROPS appears in the middle (not last) - this is the override
117                // properties pattern
118                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                    // Already using new pattern, skip
126                    continue;
127                }
128
129                // Get the names we need to work with
130                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                // Skip if this is the interface override pattern:
134                // - N_PROPS in the middle
135                // - N_PROPS used in switch case expressions
136                if has_n_props_in_middle
137                    || (has_n_props && self.n_props_used_in_switch_cases(file, n_props_name))
138                {
139                    // Skip: interface override pattern detected
140                    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                // Get the name of the last REAL property (the one before N_PROPS)
150                // If N_PROPS = PROP_X and PROP_X is an override, find the last non-override
151                // property
152                let last_real_prop_name = if has_n_props && enum_info.values.len() >= 2 {
153                    // Check if the value before N_PROPS is an override
154                    let second_to_last = &enum_info.values[enum_info.values.len() - 2];
155
156                    // Also check if N_PROPS = PROP_X where PROP_X is an override
157                    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                        // N_PROPS = PROP_ORIENTATION (override), so find last non-override property
171                        enum_info
172                            .values
173                            .iter()
174                            .rev()
175                            .skip(1) // Skip N_PROPS
176                            .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                // Fix 0: Convert anonymous enum to typedef if needed
199                if let Some(ref enum_name) = derived_enum_name {
200                    fixes.extend(self.create_typedef_fixes(file, enum_info, enum_name));
201                }
202
203                // Fix 1: Remove PROP_0 line entirely (including any blank line after it)
204                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                // Fix 2: Add " = 1" to the first real property (second value)
210                if has_prop_0 && enum_info.values.len() >= 2 {
211                    let first_real = &enum_info.values[1];
212
213                    // If the property already has a value (e.g., "= 0"), remove it first
214                    if first_real.value == Some(0)
215                        && let Some(value_loc) = &first_real.value_location
216                    {
217                        // Remove existing " = 0" or "= 0" and replace with " = 1"
218                        fixes.push(Fix::new(
219                            first_real.name_location.end_byte,
220                            value_loc.end_byte,
221                            " = 1".to_string(),
222                        ));
223                    } else {
224                        // Just insert " = 1" right after the property name
225                        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                // Fix 3: Remove N_PROPS line entirely (including any blank line before it)
234                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                // Fix 4 & 5: Find GParamSpec arrays and fix both their declarations and
240                // install_properties calls
241                // Only fix if this N_PROPS name is unique in the file (avoid ambiguity)
242                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                    // Fix install_properties calls that use these arrays
251                    for func in file.iter_class_init_functions() {
252                        for call in func.find_install_properties_calls() {
253                            // Second argument (index 1) should be N_PROPS
254                            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                                // Get the array name from third argument
259                                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                // Fix 6: Add enum cast to switch statements in get_property/set_property
277                // This enables -Wswitch-enum to catch missing properties
278                // Only apply to the specific property functions for this enum
279                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            // Check modern enums (without PROP_0/N_PROPS) for outdated array sizes
321            // and missing switch casts
322            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                // Skip old-style enums (already handled above)
327                if has_prop_0 || has_n_props {
328                    continue;
329                }
330
331                // Only check already-modern enums (ones with explicit = 1 on first value)
332                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                // Find the last real (non-override) property
349                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                // Check GParamSpec arrays for outdated PROP_X + 1 pattern
360                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    /// Check using legacy enum style (PROP_0 at start, N_PROPS at end)
374    fn check_all_legacy_style(&self, ast_context: &AstContext, violations: &mut Vec<Violation>) {
375        // Check each file's enums
376        for (path, file) in ast_context.iter_all_files() {
377            // First pass: collect all existing PROP_0 variants to avoid duplicates
378            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            // Second pass: check each enum
393            for enum_info in file.iter_property_enums() {
394                // Determine if we need a prefix for PROP_0
395                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                // Check first enumerator - should be PROP_0
410                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                    // Get indentation from the source
417                    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                    // Insert PROP_0 before first property
426                    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                    // Remove " = 0" if it exists on first property
434                    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                    // Mark that we're adding PROP_0
449                    if prefix.is_none() {
450                        will_add_unprefixed_prop_zero = true;
451                    }
452                }
453
454                // Check last enumerator - should be N_PROPS or similar
455                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                    // Insert N_PROPS after last enumerator
469                    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    /// Find GParamSpec arrays that use N_PROPS, fix their declarations, and
497    /// return their names e.g., static GParamSpec *props[N_PROPS] -> static
498    /// GParamSpec *props[LAST_PROP + 1]
499    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    /// Add switch cast fix for a specific function
533    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        // Find the function definition
541        for func in file.iter_function_definitions() {
542            if func.name != func_name {
543                continue;
544            }
545
546            // Find switch statements using iterator to handle nested cases
547            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    /// Helper to add switch cast if not already present
556    fn add_switch_cast_if_needed(
557        &self,
558        switch_stmt: &SwitchStatement,
559        enum_name: &str,
560        fixes: &mut Vec<Fix>,
561    ) {
562        // Check if the condition is already a cast to this enum type
563        let already_cast = match &switch_stmt.condition {
564            Expression::Cast(cast) => {
565                // Check if cast type contains the enum name
566                cast.type_info.base_type.contains(enum_name)
567            }
568            _ => false,
569        };
570
571        if !already_cast {
572            // Add fix to wrap the condition in a cast
573            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    /// Derive enum name from class type base
583    /// e.g., "ClutterActorClass" -> Some("ClutterActorProps")
584    /// e.g., "MyObjectClass" -> Some("MyObjectProps")
585    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    /// Check if N_PROPS is used in switch case expressions in
593    /// get_property/set_property e.g., case N_PROPS +
594    /// META_DBUS_SESSION_PROP_FOO:
595    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            // Check all switch statements in the function
602            for stmt in &func.body_statements {
603                for switch_stmt in stmt.iter_switches() {
604                    // Check if any case label uses n_props_name
605                    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    /// Build a map of enum value name -> whether it's an override property
619    /// from the given param_spec assignments
620    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            // Only track assignments that have an enum_value (ArraySubscript and
628            // OverrideProperty)
629            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    /// Check if modern enum needs switch casts in getter/setter (and typedef if
640    /// anonymous)
641    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 anonymous enum, add typedef
664        if enum_info.name.is_none() {
665            fixes.extend(self.create_typedef_fixes(file, enum_info, &enum_name));
666        }
667
668        // Add switch casts for getter/setter functions
669        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        // Only create a violation if we actually need to add fixes
677        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    /// Create fixes to convert an anonymous enum to a typedef enum
701    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        // Add "typedef " before "enum"
710        fixes.push(Fix::new(
711            enum_info.location.start_byte,
712            enum_info.location.start_byte,
713            "typedef ".to_string(),
714        ));
715
716        // Add enum name and semicolon after the closing brace
717        let semicolon_end = enum_info.body_location.find_after(b';');
718        if semicolon_end > enum_info.body_location.end_byte {
719            // Replace the semicolon with " EnumName;"
720            fixes.push(Fix::new(
721                semicolon_end - 1,
722                semicolon_end,
723                format!(" {};", enum_name),
724            ));
725        }
726
727        fixes
728    }
729
730    /// Check for GParamSpec arrays with outdated PROP_X + 1 sizes
731    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        // Build set of property names from this enum
740        let property_names: std::collections::HashSet<&str> =
741            enum_info.values.iter().map(|v| v.name.as_str()).collect();
742
743        // Find all GParamSpec pointer arrays
744        let arrays = file.find_typed_arrays("GParamSpec", true, None);
745
746        for decl in arrays {
747            // Check for PROP_X + 1 pattern
748            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                // This array uses a property from our enum
753                // Check if this property is outdated (not the expected last property)
754                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}