Skip to main content

gobject_ast/model/
top_level.rs

1use std::collections::HashMap;
2
3use clap::ValueEnum;
4use serde::{Serialize, Serializer, ser::SerializeMap};
5
6use crate::{
7    Comment, EnumInfo, GObjectType, TypeInfo, VariableDecl, VirtualFunction,
8    model::{
9        SourceLocation, Statement,
10        doc::{FunctionDoc, PropertyDoc, SignalDoc, TypeDoc},
11        expression::{CallExpression, Expression},
12        types::{ParamSpecAssignment, Parameter, Property, Signal},
13    },
14};
15
16/// Coarse kind of a top-level item, useful for filtering without
17/// pattern-matching.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, ValueEnum)]
19#[serde(rename_all = "snake_case")]
20pub enum TopLevelItemKind {
21    FunctionDefinition,
22    FunctionDeclaration,
23    Typedef,
24    Struct,
25    Enum,
26    Include,
27    Define,
28    GObjectType,
29    Conditional,
30    GObjectDeclsBlock,
31    Declaration,
32    Expression,
33    Other,
34}
35
36/// Represents a top-level item in a C file
37#[derive(Debug, Clone)]
38pub enum TopLevelItem {
39    /// Preprocessor directive (#define, #include, etc.)
40    Preprocessor(PreprocessorDirective),
41    /// Type definition (typedef, enum, struct)
42    TypeDefinition(TypeDefItem),
43    /// Function declaration (forward declaration)
44    FunctionDeclaration(FunctionDeclItem),
45    /// Function definition (with body)
46    FunctionDefinition(FunctionDefItem),
47    /// Standalone variable declaration
48    Declaration(Box<VariableDecl>),
49    /// Standalone expression statement
50    Expression(Box<Expression>),
51    /// Standalone GTK-Doc comment not attached to any declaration
52    Comment(Comment),
53}
54
55impl TopLevelItem {
56    /// The name associated with this item, if any.
57    pub fn name(&self) -> Option<&str> {
58        match self {
59            Self::FunctionDefinition(f) => Some(&f.name),
60            Self::FunctionDeclaration(f) => Some(&f.name),
61            Self::TypeDefinition(td) => match td {
62                TypeDefItem::Typedef { name, .. } | TypeDefItem::Struct { name, .. } => Some(name),
63                TypeDefItem::Enum(enum_info) => enum_info.name.as_deref(),
64            },
65            Self::Preprocessor(PreprocessorDirective::Include { path, .. }) => Some(path),
66            Self::Preprocessor(PreprocessorDirective::Define { name, .. }) => Some(name),
67            Self::Preprocessor(PreprocessorDirective::GObjectType(gobject_type)) => {
68                Some(&gobject_type.type_name)
69            }
70            Self::Declaration(decl) => Some(&decl.name),
71            Self::Comment(_) => None,
72            _ => None,
73        }
74    }
75
76    /// The coarse kind of this item.
77    pub fn kind(&self) -> TopLevelItemKind {
78        match self {
79            Self::FunctionDefinition(_) => TopLevelItemKind::FunctionDefinition,
80            Self::FunctionDeclaration(_) => TopLevelItemKind::FunctionDeclaration,
81            Self::TypeDefinition(TypeDefItem::Typedef { .. }) => TopLevelItemKind::Typedef,
82            Self::TypeDefinition(TypeDefItem::Struct { .. }) => TopLevelItemKind::Struct,
83            Self::TypeDefinition(TypeDefItem::Enum(_)) => TopLevelItemKind::Enum,
84            Self::Preprocessor(PreprocessorDirective::Include { .. }) => TopLevelItemKind::Include,
85            Self::Preprocessor(PreprocessorDirective::Define { .. }) => TopLevelItemKind::Define,
86            Self::Preprocessor(PreprocessorDirective::GObjectType { .. }) => {
87                TopLevelItemKind::GObjectType
88            }
89            Self::Preprocessor(PreprocessorDirective::Conditional { .. }) => {
90                TopLevelItemKind::Conditional
91            }
92            Self::Preprocessor(PreprocessorDirective::GObjectDeclsBlock { .. }) => {
93                TopLevelItemKind::GObjectDeclsBlock
94            }
95            Self::Declaration(_) => TopLevelItemKind::Declaration,
96            Self::Expression(_) => TopLevelItemKind::Expression,
97            Self::Comment(_) => TopLevelItemKind::Other,
98            Self::Preprocessor(_) => TopLevelItemKind::Other,
99        }
100    }
101}
102
103impl Serialize for TopLevelItem {
104    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
105        match self {
106            Self::Preprocessor(directive) => directive.serialize(s),
107            Self::TypeDefinition(v) => {
108                let mut m = s.serialize_map(Some(1))?;
109                m.serialize_entry("type_definition", v)?;
110                m.end()
111            }
112            Self::FunctionDeclaration(v) => {
113                let mut m = s.serialize_map(Some(1))?;
114                m.serialize_entry("function_declaration", v)?;
115                m.end()
116            }
117            Self::FunctionDefinition(v) => {
118                let mut m = s.serialize_map(Some(1))?;
119                m.serialize_entry("function_definition", v)?;
120                m.end()
121            }
122            Self::Declaration(v) => {
123                let mut m = s.serialize_map(Some(1))?;
124                m.serialize_entry("declaration", v)?;
125                m.end()
126            }
127            Self::Expression(v) => {
128                let mut m = s.serialize_map(Some(1))?;
129                m.serialize_entry("expression", v)?;
130                m.end()
131            }
132            Self::Comment(v) => {
133                let mut m = s.serialize_map(Some(1))?;
134                m.serialize_entry("comment", v)?;
135                m.end()
136            }
137        }
138    }
139}
140
141#[derive(Debug, Clone, Serialize)]
142#[serde(rename_all = "snake_case")]
143pub enum PragmaKind {
144    /// #pragma once
145    Once,
146    /// #pragma GCC/clang diagnostic push
147    DiagnosticPush,
148    /// #pragma GCC/clang diagnostic pop
149    DiagnosticPop,
150    /// #pragma GCC/clang diagnostic ignored "-Wwarning-name"
151    DiagnosticIgnored { warning: String },
152    /// Other pragma directive
153    Other {
154        name: String,
155        arguments: Option<String>,
156    },
157}
158
159#[derive(Debug, Clone, Serialize)]
160#[serde(rename_all = "snake_case")]
161pub enum PreprocessorDirective {
162    Include {
163        path: String,
164        is_system: bool,
165        location: SourceLocation,
166    },
167    Define {
168        name: String,
169        value: Option<String>,
170        location: SourceLocation,
171    },
172    Call {
173        directive: String,
174        location: SourceLocation,
175    },
176    Pragma {
177        kind: PragmaKind,
178        location: SourceLocation,
179    },
180    /// GObject type declaration/definition (G_DECLARE_*, G_DEFINE_*)
181    GObjectType(Box<GObjectType>),
182    /// G_DEFINE_AUTOPTR_CLEANUP_FUNC (Type, cleanup_func)
183    AutoptrCleanupFunc {
184        type_name: String,
185        cleanup_function: String,
186        location: SourceLocation,
187    },
188    /// G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (Type, cleanup_func)
189    AutoCleanupClearFunc {
190        type_name: String,
191        cleanup_function: String,
192        location: SourceLocation,
193    },
194    /// Macro call with code block (e.g., G_DEFINE_BOXED_TYPE_WITH_CODE)
195    /// Contains the macro name and parsed statements from the code block
196    MacroWithCode {
197        macro_name: String,
198        arguments: Vec<String>,
199        code_statements: Vec<Statement>,
200        location: SourceLocation,
201    },
202    Conditional {
203        kind: ConditionalKind,
204        condition: Option<String>,
205        body: Vec<TopLevelItem>,
206        location: SourceLocation,
207    },
208    /// G_BEGIN_DECLS ... G_END_DECLS block
209    GObjectDeclsBlock {
210        body: Vec<TopLevelItem>,
211        location: SourceLocation,
212    },
213}
214
215impl PreprocessorDirective {
216    pub fn location(&self) -> &SourceLocation {
217        match self {
218            Self::Include { location, .. }
219            | Self::Define { location, .. }
220            | Self::Call { location, .. }
221            | Self::Pragma { location, .. }
222            | Self::AutoptrCleanupFunc { location, .. }
223            | Self::AutoCleanupClearFunc { location, .. }
224            | Self::MacroWithCode { location, .. }
225            | Self::Conditional { location, .. }
226            | Self::GObjectDeclsBlock { location, .. } => location,
227            Self::GObjectType(gt) => &gt.location,
228        }
229    }
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
233#[serde(rename_all = "snake_case")]
234pub enum ConditionalKind {
235    Ifdef,
236    Ifndef,
237    If,
238    Elif,
239    Else,
240}
241
242/// A parsed field from a struct body (e.g. `GObject parent` → field_type =
243/// "GObject")
244#[derive(Debug, Clone, Serialize)]
245pub struct StructField {
246    pub field_type: TypeInfo,
247    /// Field name, if present (anonymous bitfields have none)
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub field_name: Option<String>,
250    pub location: SourceLocation,
251    /// Bit-width for bitfield members (`unsigned flags : 1` → `Some(1)`).
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub bit_width: Option<u32>,
254    /// Non-empty for anonymous struct/union fields: the members of the
255    /// embedded aggregate (e.g. `union { A a; B b; } d` → inner_fields = [a,
256    /// b]).
257    #[serde(default, skip_serializing_if = "Vec::is_empty")]
258    pub inner_fields: Vec<Self>,
259}
260
261impl StructField {
262    /// True for future-use padding fields that should never be flagged as dead
263    /// code: names starting with `rfu`, `reserved`, `padding`, or `_padding`.
264    pub fn is_reserved(&self) -> bool {
265        self.field_name.as_deref().is_some_and(|n| {
266            n.starts_with("rfu")
267                || n.starts_with("reserved")
268                || n.starts_with("padding")
269                || n.starts_with("_padding")
270        })
271    }
272
273    /// Visit this field and all nested fields (anonymous struct/union members)
274    /// in pre-order, matching the pattern used by `Statement::walk`.
275    pub fn walk<F>(&self, f: &mut F)
276    where
277        F: FnMut(&Self),
278    {
279        f(self);
280        for inner in &self.inner_fields {
281            inner.walk(f);
282        }
283    }
284}
285
286/// The right-hand side of a typedef declaration.
287#[derive(Debug, Clone, Serialize)]
288#[serde(rename_all = "snake_case")]
289pub enum TypedefTarget {
290    /// Plain type alias: `typedef struct _Foo Foo`, `typedef gint MyInt`.
291    Type(TypeInfo),
292    /// Function-pointer alias: `typedef void (*FooCallback)(GObject *,
293    /// gpointer)`.
294    Callback {
295        return_type: TypeInfo,
296        parameters: Vec<Parameter>,
297    },
298}
299
300impl TypedefTarget {
301    /// Return the inner `TypeInfo` if this is a plain type alias.
302    pub fn as_type(&self) -> Option<&TypeInfo> {
303        match self {
304            Self::Type(t) => Some(t),
305            Self::Callback { .. } => None,
306        }
307    }
308}
309
310#[derive(Debug, Clone, Serialize)]
311#[serde(rename_all = "snake_case")]
312pub enum TypeDefItem {
313    Typedef {
314        name: String,
315        target: TypedefTarget,
316        /// Fields when the typedef wraps an inline struct body:
317        /// `typedef struct { FieldType field; } Name;`
318        #[serde(default, skip_serializing_if = "Vec::is_empty")]
319        struct_fields: Vec<StructField>,
320        location: SourceLocation,
321        #[serde(skip_serializing_if = "Option::is_none")]
322        doc: Option<TypeDoc>,
323    },
324    Struct {
325        name: String,
326        #[serde(default, skip_serializing_if = "Vec::is_empty")]
327        fields: Vec<StructField>,
328        /// Virtual functions (function pointer fields) extracted from class
329        /// structs (structs whose name ends with `Class`).
330        #[serde(default, skip_serializing_if = "Vec::is_empty")]
331        vfuncs: Vec<VirtualFunction>,
332        location: SourceLocation,
333        #[serde(skip_serializing_if = "Option::is_none")]
334        doc: Option<TypeDoc>,
335    },
336    Enum(Box<EnumInfo>),
337}
338
339impl TypeDefItem {
340    /// True for GObject class/interface vtable structs whose fields should not
341    /// be checked for dead code: any struct with vfuncs, or any type whose
342    /// bare name ends with `Class` or `Interface`.
343    pub fn is_vtable_struct(&self) -> bool {
344        match self {
345            Self::Struct { name, vfuncs, .. } => {
346                let bare = name.trim_start_matches('_');
347                bare.ends_with("Class") || bare.ends_with("Interface") || !vfuncs.is_empty()
348            }
349            Self::Typedef { name, .. } => name.ends_with("Class") || name.ends_with("Interface"),
350            Self::Enum { .. } => false,
351        }
352    }
353}
354
355#[derive(Debug, Clone, Serialize)]
356pub struct FunctionDeclItem {
357    pub name: String,
358    pub return_type: TypeInfo,
359    #[serde(skip_serializing_if = "std::ops::Not::not")]
360    pub is_static: bool,
361    #[serde(skip_serializing_if = "std::ops::Not::not")]
362    pub is_inline: bool,
363    #[serde(skip_serializing_if = "Vec::is_empty")]
364    pub parameters: Vec<Parameter>,
365    #[serde(skip_serializing_if = "Vec::is_empty")]
366    pub export_macros: Vec<String>,
367    pub location: SourceLocation,
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub doc: Option<FunctionDoc>,
370}
371
372#[derive(Debug, Clone, Serialize)]
373pub struct FunctionDefItem {
374    pub name: String,
375    pub return_type: TypeInfo,
376    #[serde(skip_serializing_if = "std::ops::Not::not")]
377    pub is_static: bool,
378    #[serde(skip_serializing_if = "std::ops::Not::not")]
379    pub is_inline: bool,
380    #[serde(skip_serializing_if = "Vec::is_empty")]
381    pub parameters: Vec<Parameter>,
382    #[serde(skip_serializing_if = "Vec::is_empty")]
383    pub body_statements: Vec<Statement>,
384    pub location: SourceLocation,
385    #[serde(skip)]
386    pub body_location: Option<SourceLocation>,
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub doc: Option<FunctionDoc>,
389}
390
391impl FunctionDefItem {
392    /// Find all calls to specific functions in the body
393    /// Returns references to all CallExpression nodes that match any of the
394    /// given function names
395    pub fn find_calls<'a>(&'a self, function_names: &[&str]) -> Vec<&'a CallExpression> {
396        self.find_calls_matching(|name| function_names.contains(&name))
397    }
398
399    /// Find all calls matching a predicate in the body
400    pub fn find_calls_matching<F>(&self, predicate: F) -> Vec<&CallExpression>
401    where
402        F: Fn(&str) -> bool,
403    {
404        let mut exprs: Vec<&Expression> = Vec::new();
405        for stmt in &self.body_statements {
406            stmt.walk_expressions(&mut |expr| exprs.push(expr));
407        }
408
409        let mut results = Vec::new();
410        for expr in exprs {
411            expr.walk(&mut |e| {
412                if let Expression::Call(call) = e
413                    && call.function_name_str().is_some_and(&predicate)
414                {
415                    results.push(call);
416                }
417            });
418        }
419        results
420    }
421
422    /// Extract signal registrations from the function body.
423    /// Populates `enum_value` when the signal is assigned via
424    /// `signals[ENUM] = g_signal_new(...)`.
425    pub fn find_signal_registrations(&self, source: &[u8]) -> Vec<Signal> {
426        let mut signals = Vec::new();
427        let mut seen_names = std::collections::HashSet::new();
428
429        // First pass: assignments like `signals[ENUM] = g_signal_new(...)`
430        for (i, stmt) in self.body_statements.iter().enumerate() {
431            for assignment in stmt.iter_assignments() {
432                let Expression::Call(call) = &*assignment.rhs else {
433                    continue;
434                };
435                if !call.function_contains("g_signal_new", source) {
436                    continue;
437                }
438                let Some(mut signal) = Signal::from_g_signal_new_call(call, source) else {
439                    continue;
440                };
441                if let Expression::Subscript(sub) = &*assignment.lhs
442                    && let Expression::Identifier(id) = &*sub.index
443                {
444                    signal.enum_value = Some(id.name.clone());
445                }
446                if i > 0
447                    && let Statement::Comment(c) = &self.body_statements[i - 1]
448                {
449                    signal.doc = SignalDoc::from_comment(c);
450                }
451                seen_names.insert(signal.name.clone());
452                signals.push(signal);
453            }
454        }
455
456        // Second pass: standalone g_signal_new calls not already captured
457        for (i, stmt) in self.body_statements.iter().enumerate() {
458            for call in stmt.iter_calls() {
459                if !call.function_name(source).starts_with("g_signal_new") {
460                    continue;
461                }
462                let Some(name) = call.extract_string_from_arg(0) else {
463                    continue;
464                };
465                if seen_names.contains(&name) {
466                    continue;
467                }
468                if let Some(mut signal) = Signal::from_g_signal_new_call(call, source) {
469                    if i > 0
470                        && let Statement::Comment(c) = &self.body_statements[i - 1]
471                    {
472                        signal.doc = SignalDoc::from_comment(c);
473                    }
474                    signals.push(signal);
475                }
476            }
477        }
478
479        signals
480    }
481
482    /// Iterate all local variable declarations in the function body recursively
483    pub fn iter_local_declarations(&self) -> impl Iterator<Item = &VariableDecl> {
484        self.body_statements
485            .iter()
486            .flat_map(Statement::iter_declarations)
487    }
488
489    /// Collect all return values from the function body
490    pub fn collect_return_values(&self) -> Vec<&Expression> {
491        self.body_statements
492            .iter()
493            .flat_map(Statement::iter_returns)
494            .filter_map(|r| r.value.as_ref())
495            .collect()
496    }
497
498    /// Check if any variable of the given type is directly returned from the
499    /// function
500    pub fn is_var_returned(&self, type_info: &TypeInfo) -> bool {
501        for stmt in &self.body_statements {
502            for ret in stmt.iter_returns() {
503                if let Some(Expression::Identifier(id)) = &ret.value {
504                    // Find the declaration of this identifier in all body statements
505                    for body_stmt in &self.body_statements {
506                        for decl in body_stmt.iter_declarations() {
507                            if decl.name == id.name
508                                && decl.type_info.base_type == type_info.base_type
509                                && decl.type_info.is_pointer() == type_info.is_pointer()
510                            {
511                                return true;
512                            }
513                        }
514                    }
515                }
516            }
517        }
518        false
519    }
520
521    /// Check if any variable of the given type is passed to a cleanup call
522    /// (g_object_unref, g_free, etc.)
523    pub fn is_var_passed_to_cleanup(&self, type_info: &TypeInfo) -> bool {
524        for stmt in &self.body_statements {
525            for call in stmt.iter_calls() {
526                if call.is_cleanup_call()
527                    && let Some(arg) = call.get_arg(0)
528                    && let Expression::Identifier(id) = arg
529                {
530                    // Find the declaration of this identifier
531                    for body_stmt in &self.body_statements {
532                        for decl in body_stmt.iter_declarations() {
533                            if decl.name == id.name
534                                && decl.type_info.base_type == type_info.base_type
535                                && decl.type_info.is_pointer() == type_info.is_pointer()
536                            {
537                                return true;
538                            }
539                        }
540                    }
541                }
542            }
543        }
544        false
545    }
546
547    /// Check if the named variable is passed to a specific function at a
548    /// specific argument position
549    pub fn is_var_passed_to_function(
550        &self,
551        var_name: &str,
552        func_name: &str,
553        arg_index: usize,
554    ) -> bool {
555        self.body_statements.iter().any(|stmt| {
556            stmt.iter_calls().any(|call| {
557                call.is_function(func_name)
558                    && call.get_arg(arg_index).is_some_and(
559                        |arg| matches!(arg, Expression::Identifier(id) if id.name == var_name),
560                    )
561            })
562        })
563    }
564
565    /// Check if any variable of the given type is allocated via an allocation
566    /// call Uses `call.is_allocation_call()` to detect allocations by
567    /// default
568    pub fn is_var_allocated(&self, type_info: &TypeInfo) -> bool {
569        self.is_var_allocated_with(type_info, CallExpression::is_allocation_call)
570    }
571
572    /// Check if any variable of the given type is allocated via a custom
573    /// allocation predicate
574    pub fn is_var_allocated_with(
575        &self,
576        type_info: &TypeInfo,
577        is_allocation: impl Fn(&CallExpression) -> bool,
578    ) -> bool {
579        for stmt in &self.body_statements {
580            let mut found = false;
581            stmt.walk(&mut |s| {
582                match s {
583                    // Check init: Type *var = allocation_call()
584                    Statement::Declaration(decl) => {
585                        if decl.type_info.base_type == type_info.base_type
586                            && decl.type_info.is_pointer() == type_info.is_pointer()
587                            && let Some(Expression::Call(call)) = &decl.initializer
588                            && is_allocation(call)
589                        {
590                            found = true;
591                        }
592                    }
593                    // Check assignment: var = allocation_call()
594                    Statement::Expression(expr_stmt) => {
595                        if let Expression::Assignment(assign) = expr_stmt.as_ref()
596                            && let Expression::Identifier(id) = &*assign.lhs
597                            && let Expression::Call(call) = &*assign.rhs
598                            && is_allocation(call)
599                        {
600                            // Find the declaration of the assigned variable
601                            for body_stmt in &self.body_statements {
602                                for decl in body_stmt.iter_declarations() {
603                                    if decl.name == id.name
604                                        && decl.type_info.base_type == type_info.base_type
605                                        && decl.type_info.is_pointer() == type_info.is_pointer()
606                                    {
607                                        found = true;
608                                    }
609                                }
610                            }
611                        }
612                    }
613                    _ => {}
614                }
615            });
616            if found {
617                return true;
618            }
619        }
620        false
621    }
622
623    /// Find all g_object_class_install_properties calls in the function body
624    pub fn find_install_properties_calls(&self) -> Vec<&CallExpression> {
625        self.find_calls(&["g_object_class_install_properties"])
626    }
627
628    /// Map every named parameter and local variable to its `TypeInfo`.
629    /// Parameters appear first; local declarations in body order after that,
630    /// so an inner-scope shadowing declaration overwrites the outer one.
631    pub fn local_var_types(&self) -> std::collections::HashMap<&str, &TypeInfo> {
632        let mut map = std::collections::HashMap::new();
633        for param in &self.parameters {
634            if let Parameter::Regular {
635                name: Some(name),
636                type_info,
637                ..
638            } = param
639            {
640                map.insert(name.as_str(), type_info);
641            }
642        }
643        for stmt in &self.body_statements {
644            stmt.walk(&mut |s| {
645                if let Statement::Declaration(decl) = s {
646                    map.insert(decl.name.as_str(), &decl.type_info);
647                }
648            });
649        }
650        map
651    }
652
653    /// Get a parameter by name
654    pub fn get_param_by_name(&self, name: &str) -> Option<&Parameter> {
655        self.parameters
656            .iter()
657            .find(|p| matches!(p, Parameter::Regular { name: Some(n), .. } if n == name))
658    }
659
660    /// Find all param_spec assignments in the function body
661    /// Handles array pattern (props[PROP_X] = ...), variable pattern
662    /// (param_spec = ...), and override pattern
663    /// (g_object_class_override_property(...))
664    pub(crate) fn find_param_spec_assignments(&self, source: &[u8]) -> Vec<ParamSpecAssignment> {
665        let mut assignments = Vec::new();
666        let mut array_assignments: HashMap<&str, Vec<usize>> = HashMap::new();
667        let mut variable_assignments: HashMap<&str, Vec<usize>> = HashMap::new();
668
669        // First pass: collect all assignments
670        for (i, stmt) in self.body_statements.iter().enumerate() {
671            stmt.walk(&mut |s| {
672                if let Statement::Expression(expr_stmt) = s {
673                    match expr_stmt.as_ref() {
674                        // Assignment: props[PROP_X] = g_param_spec_*() or spec = g_param_spec_*()
675                        Expression::Assignment(assignment) => {
676                            if let Expression::Call(param_call) = &*assignment.rhs {
677                                let func_name = param_call.function_name(source);
678                                if !func_name.contains("_param_spec_") {
679                                    return;
680                                }
681
682                                // Parse property from call
683                                let Some(mut property) =
684                                    Property::from_param_spec_call(param_call, source)
685                                else {
686                                    return;
687                                };
688                                if i > 0
689                                    && let Statement::Comment(c) = &self.body_statements[i - 1]
690                                {
691                                    property.doc = PropertyDoc::from_comment(c);
692                                }
693
694                                // Check LHS: array subscript or variable?
695                                if let Expression::Subscript(subscript) = &*assignment.lhs {
696                                    // Array pattern: props[PROP_X] = g_param_spec_*()
697                                    if let Some(array_name) =
698                                        subscript.array.to_source_string(source)
699                                        && let Some(enum_value) =
700                                            subscript.index.to_source_string(source)
701                                    {
702                                        let idx = assignments.len();
703                                        array_assignments.entry(array_name).or_default().push(idx);
704                                        assignments.push(ParamSpecAssignment::ArraySubscript {
705                                            array_name: array_name.to_owned(),
706                                            enum_value: enum_value.to_owned(),
707                                            statement_location: *s.location(),
708                                            call: param_call.clone(),
709                                            property,
710                                            install_call: None,
711                                        });
712                                    }
713                                } else if let Some(var_name) =
714                                    assignment.lhs.to_source_string(source)
715                                {
716                                    // Variable pattern: param_spec = g_param_spec_*()
717                                    let idx = assignments.len();
718                                    variable_assignments.entry(var_name).or_default().push(idx);
719                                    assignments.push(ParamSpecAssignment::Variable {
720                                        variable_name: var_name.to_owned(),
721                                        statement_location: *s.location(),
722                                        call: param_call.clone(),
723                                        property,
724                                        install_call: None,
725                                    });
726                                }
727                            }
728                        }
729                        // Direct call: g_object_class_override_property(class, PROP_X, "name")
730                        Expression::Call(call) => {
731                            if call.function_contains("override_property", source)
732                                && let Some(mut property) =
733                                    Property::from_override_property_call(call)
734                                && let Some(enum_arg) = call.get_arg(1)
735                                && let Some(enum_value) = enum_arg.to_source_string(source)
736                            {
737                                if i > 0
738                                    && let Statement::Comment(c) = &self.body_statements[i - 1]
739                                {
740                                    property.doc = PropertyDoc::from_comment(c);
741                                }
742                                assignments.push(ParamSpecAssignment::OverrideProperty {
743                                    enum_value: enum_value.to_owned(),
744                                    statement_location: *s.location(),
745                                    call: call.clone(),
746                                    property,
747                                });
748                            }
749                        }
750                        _ => {}
751                    }
752                }
753            });
754        }
755
756        // Second pass: find install calls and link them to assignments
757        for (i, stmt) in self.body_statements.iter().enumerate() {
758            stmt.walk(&mut |s| {
759                if let Statement::Expression(expr_stmt) = s
760                    && let Expression::Call(call) = expr_stmt.as_ref()
761                {
762                    // g_object_class_install_properties(class, N_PROPS, array)
763                    if call.function_contains("install_properties", source) {
764                        if let Some(array_arg) = call.get_arg(2)
765                            && let Some(array_name) = array_arg.to_source_string(source)
766                            && let Some(indices) = array_assignments.get(&array_name)
767                        {
768                            for &idx in indices {
769                                if let ParamSpecAssignment::ArraySubscript {
770                                    install_call, ..
771                                } = &mut assignments[idx]
772                                {
773                                    *install_call = Some(call.clone());
774                                }
775                            }
776                        }
777                    }
778                    // g_object_class_install_property(class, PROP_X, spec)
779                    else if call.function_contains("install_property", source)
780                        && let Some(spec_expr) = call.get_arg(2)
781                    {
782                        if let Expression::Call(spec_call) = spec_expr
783                            && spec_call.function_contains("_param_spec_", source)
784                            && let Some(enum_arg) = call.get_arg(1)
785                            && let Some(enum_value) = enum_arg.to_source_string(source)
786                            && let Some(mut property) =
787                                Property::from_param_spec_call(spec_call, source)
788                        {
789                            if i > 0
790                                && let Statement::Comment(c) = &self.body_statements[i - 1]
791                            {
792                                property.doc = PropertyDoc::from_comment(c);
793                            }
794                            assignments.push(ParamSpecAssignment::DirectInstall {
795                                enum_value: enum_value.to_owned(),
796                                statement_location: *s.location(),
797                                call: spec_call.clone(),
798                                property,
799                                install_call: call.clone(),
800                            });
801                        } else if let Some(var_name) = spec_expr.to_source_string(source)
802                            && let Some(indices) = variable_assignments.get(var_name)
803                        {
804                            let indices = indices.clone();
805                            for idx in indices {
806                                if let ParamSpecAssignment::Variable { install_call, .. } =
807                                    &mut assignments[idx]
808                                {
809                                    *install_call = Some(call.clone());
810                                }
811                            }
812                        }
813                    }
814                }
815            });
816        }
817
818        assignments
819    }
820}