Skip to main content

perl_semantic_analyzer/analysis/
class_model.rs

1//! Class model for Moose/Moo/Mouse/Class::Accessor intelligence.
2//!
3//! Provides a structured representation of Perl OOP class declarations,
4//! including attributes, methods, inheritance, and role composition.
5//! Built from AST traversal, reusing existing framework detection.
6
7use crate::SourceLocation;
8use crate::ast::{Node, NodeKind};
9use std::collections::{HashMap, HashSet};
10
11/// Which OO framework a package uses.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Framework {
14    /// `use Moose;`
15    Moose,
16    /// `use Moo;`
17    Moo,
18    /// `use Mouse;`
19    Mouse,
20    /// `use Class::Accessor;` or `use parent 'Class::Accessor';`
21    ClassAccessor,
22    /// `use Object::Pad;`
23    ObjectPad,
24    /// Native Perl OOP (bless-based)
25    Native,
26    /// Native Perl 5.38+ class (use feature 'class')
27    NativeClass,
28    /// Plain OO via `use parent`, `use base`, or `@ISA` (no framework)
29    PlainOO,
30    /// `use Role::Tiny;` (package is a role) or `use Role::Tiny::With;` (package consumes roles)
31    RoleTiny,
32    /// No OO framework detected
33    None,
34}
35
36/// Accessor mode from the `is` option.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum AccessorType {
39    /// `is => 'ro'`
40    Ro,
41    /// `is => 'rw'`
42    Rw,
43    /// `is => 'lazy'` (Moo shorthand for `ro` + `lazy => 1`)
44    Lazy,
45    /// `is => 'bare'` (no accessor generated)
46    Bare,
47}
48
49/// Method-resolution order for inherited method lookup.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum MethodResolutionOrder {
52    /// Default Perl depth-first resolution order.
53    #[default]
54    Dfs,
55    /// C3 linearization enabled via `use mro 'c3';`.
56    C3,
57}
58
59/// A Moose/Moo attribute declared via `has`.
60#[derive(Debug, Clone)]
61pub struct Attribute {
62    /// Attribute name (e.g., `name` from `has 'name' => (...)`)
63    pub name: String,
64    /// Accessor mode
65    pub is: Option<AccessorType>,
66    /// Type constraint string (e.g., `Str`, `ArrayRef[Int]`)
67    pub isa: Option<String>,
68    /// Whether a default value is specified
69    pub default: bool,
70    /// Whether `required => 1` is set
71    pub required: bool,
72    /// Name of the accessor method (may differ from attribute name)
73    pub accessor_name: String,
74    /// Source location of the `has` declaration
75    pub location: SourceLocation,
76    /// Builder method name. `builder => 1` derives `_build_<attr>`, a string names the method.
77    pub builder: Option<String>,
78    /// Whether a coercion is applied (`coerce => 1`)
79    pub coerce: bool,
80    /// Predicate method name. `predicate => 1` derives `has_<attr>`.
81    pub predicate: Option<String>,
82    /// Clearer method name. `clearer => 1` derives `clear_<attr>`.
83    pub clearer: Option<String>,
84    /// Whether a trigger is set (`trigger => \&sub`)
85    pub trigger: bool,
86}
87
88/// An Object::Pad field declaration.
89#[derive(Debug, Clone)]
90pub struct FieldInfo {
91    /// Field name (e.g., `name` from `field $name :param`)
92    pub name: String,
93    /// Source location of the field declaration
94    pub location: SourceLocation,
95    /// Raw field traits such as `param`, `reader`, and `writer`
96    pub attributes: Vec<String>,
97    /// Whether `:param` is present
98    pub param: bool,
99    /// Explicit or synthesized reader method name
100    pub reader: Option<String>,
101    /// Explicit or synthesized writer method name
102    pub writer: Option<String>,
103    /// Explicit or synthesized accessor method name
104    pub accessor: Option<String>,
105    /// Explicit or synthesized mutator method name
106    pub mutator: Option<String>,
107    /// Optional initializer expression summary
108    pub default: Option<String>,
109}
110
111/// Information about a method modifier (`before`, `after`, `around`, `override`, `augment`).
112#[derive(Debug, Clone)]
113pub struct MethodModifier {
114    /// Modifier type
115    pub kind: ModifierKind,
116    /// Name of the method being modified
117    pub method_name: String,
118    /// Source location
119    pub location: SourceLocation,
120}
121
122/// The type of method modifier.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum ModifierKind {
125    /// `before 'method' => sub { ... }`
126    Before,
127    /// `after 'method' => sub { ... }`
128    After,
129    /// `around 'method' => sub { ... }`
130    Around,
131    /// `override 'method' => sub { ... }`
132    Override,
133    /// `augment 'method' => sub { ... }`
134    Augment,
135}
136
137/// Synthetic accessor mode generated by `Class::Accessor`.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum ClassAccessorMode {
140    /// `mk_accessors(...)`
141    Rw,
142    /// `mk_ro_accessors(...)`
143    Ro,
144    /// `mk_wo_accessors(...)`
145    Wo,
146}
147
148/// Information about a method (subroutine) in a class.
149#[derive(Debug, Clone)]
150pub struct MethodInfo {
151    /// Method name
152    pub name: String,
153    /// Source location of the sub declaration
154    pub location: SourceLocation,
155    /// Whether this method was synthesized from framework metadata.
156    pub synthetic: bool,
157    /// Accessor mode for `Class::Accessor`-generated methods.
158    pub accessor_mode: Option<ClassAccessorMode>,
159}
160
161impl MethodInfo {
162    /// Construct a regular declared method.
163    pub fn new(name: String, location: SourceLocation) -> Self {
164        Self { name, location, synthetic: false, accessor_mode: None }
165    }
166
167    /// Construct a synthetic method generated from framework metadata.
168    pub fn synthetic(
169        name: String,
170        location: SourceLocation,
171        accessor_mode: Option<ClassAccessorMode>,
172    ) -> Self {
173        Self { name, location, synthetic: true, accessor_mode }
174    }
175}
176
177/// Structured model of a Perl OOP class or role.
178#[derive(Debug, Clone)]
179pub struct ClassModel {
180    /// Package name (e.g., `MyApp::User`)
181    pub name: String,
182    /// Detected OO framework
183    pub framework: Framework,
184    /// Attributes declared via `has`
185    pub attributes: Vec<Attribute>,
186    /// Fields declared via Object::Pad `field`
187    pub fields: Vec<FieldInfo>,
188    /// Methods declared via `sub`
189    pub methods: Vec<MethodInfo>,
190    /// Object::Pad `ADJUST` blocks
191    pub adjusts: Vec<MethodInfo>,
192    /// Parent classes from `extends 'Parent'`, `use parent`, `use base`, or `@ISA`
193    pub parents: Vec<String>,
194    /// Method-resolution order for inherited method lookup.
195    pub mro: MethodResolutionOrder,
196    /// Roles consumed via `with 'Role'`
197    pub roles: Vec<String>,
198    /// Method modifiers (before/after/around/override/augment)
199    pub modifiers: Vec<MethodModifier>,
200    /// Names exported by default via `@EXPORT`
201    pub exports: Vec<String>,
202    /// Names available for explicit import via `@EXPORT_OK`
203    pub export_ok: Vec<String>,
204    /// Per-file Exporter metadata with subroutine resolution.
205    pub exporter_metadata: Option<ExporterMetadata>,
206}
207
208/// Exporter-derived metadata captured for a package in a single file.
209#[derive(Debug, Clone)]
210#[non_exhaustive]
211pub struct ExporterMetadata {
212    /// Names exported by default via `@EXPORT` that resolve to local subs.
213    pub exports: Vec<ResolvedExport>,
214    /// Names available for explicit import via `@EXPORT_OK` that resolve to local subs.
215    pub export_ok: Vec<ResolvedExport>,
216    /// `%EXPORT_TAGS` entries mapped to local subroutine definitions.
217    pub export_tags: HashMap<String, Vec<ResolvedExport>>,
218    /// Export names referenced in export declarations but not defined as local subs.
219    pub unresolved: Vec<String>,
220}
221
222/// Export symbol that resolves to a same-package subroutine definition.
223#[derive(Debug, Clone)]
224#[non_exhaustive]
225pub struct ResolvedExport {
226    /// Exported symbol name (without sigil).
227    pub name: String,
228    /// Location of the matching subroutine definition in this file/package.
229    pub location: SourceLocation,
230}
231
232impl ClassModel {
233    /// Returns true if this class uses any OO framework.
234    pub fn has_framework(&self) -> bool {
235        !matches!(self.framework, Framework::None)
236    }
237
238    /// Return the names of Object::Pad fields that participate in constructor parameters.
239    pub fn object_pad_param_field_names(&self) -> impl Iterator<Item = &str> {
240        self.fields.iter().filter(|field| field.param).map(|field| field.name.as_str())
241    }
242}
243
244/// Builds `ClassModel` instances by walking an AST.
245pub struct ClassModelBuilder {
246    models: Vec<ClassModel>,
247    current_package: String,
248    current_framework: Framework,
249    current_attributes: Vec<Attribute>,
250    current_fields: Vec<FieldInfo>,
251    current_methods: Vec<MethodInfo>,
252    current_adjusts: Vec<MethodInfo>,
253    current_parents: Vec<String>,
254    current_mro: MethodResolutionOrder,
255    current_roles: Vec<String>,
256    current_modifiers: Vec<MethodModifier>,
257    current_exports: Vec<String>,
258    current_export_ok: Vec<String>,
259    current_export_tags: HashMap<String, Vec<String>>,
260    current_uses_exporter: bool,
261    current_package_aliases: HashSet<String>,
262    /// Track which packages have framework detection applied
263    framework_map: HashMap<String, Framework>,
264}
265
266impl Default for ClassModelBuilder {
267    fn default() -> Self {
268        Self::new()
269    }
270}
271
272impl ClassModelBuilder {
273    /// Create a new builder.
274    pub fn new() -> Self {
275        Self {
276            models: Vec::new(),
277            current_package: "main".to_string(),
278            current_framework: Framework::None,
279            current_attributes: Vec::new(),
280            current_fields: Vec::new(),
281            current_methods: Vec::new(),
282            current_adjusts: Vec::new(),
283            current_parents: Vec::new(),
284            current_mro: MethodResolutionOrder::Dfs,
285            current_roles: Vec::new(),
286            current_modifiers: Vec::new(),
287            current_exports: Vec::new(),
288            current_export_ok: Vec::new(),
289            current_export_tags: HashMap::new(),
290            current_uses_exporter: false,
291            current_package_aliases: HashSet::new(),
292            framework_map: HashMap::new(),
293        }
294    }
295
296    /// Build class models from an AST.
297    pub fn build(mut self, node: &Node) -> Vec<ClassModel> {
298        self.visit_node(node);
299        self.flush_current_package();
300        self.models
301    }
302
303    /// Flush the current package's accumulated data into a ClassModel.
304    fn flush_current_package(&mut self) {
305        let framework = self.current_framework;
306        // Produce a ClassModel if the package uses a framework, has attributes, or has parents
307        let has_oo_indicator = framework != Framework::None
308            || !self.current_attributes.is_empty()
309            || !self.current_fields.is_empty()
310            || !self.current_parents.is_empty()
311            || !self.current_adjusts.is_empty()
312            || !self.current_exports.is_empty()
313            || !self.current_export_ok.is_empty()
314            || !self.current_export_tags.is_empty();
315        if has_oo_indicator {
316            let exporter_metadata = self.build_exporter_metadata_for_current_package();
317            let model = ClassModel {
318                name: self.current_package.clone(),
319                framework,
320                attributes: std::mem::take(&mut self.current_attributes),
321                fields: std::mem::take(&mut self.current_fields),
322                methods: std::mem::take(&mut self.current_methods),
323                adjusts: std::mem::take(&mut self.current_adjusts),
324                parents: std::mem::take(&mut self.current_parents),
325                mro: self.current_mro,
326                roles: std::mem::take(&mut self.current_roles),
327                modifiers: std::mem::take(&mut self.current_modifiers),
328                exports: std::mem::take(&mut self.current_exports),
329                export_ok: std::mem::take(&mut self.current_export_ok),
330                exporter_metadata,
331            };
332            self.models.push(model);
333            self.current_package_aliases.clear();
334        } else {
335            // Reset accumulators even if we don't produce a model
336            self.current_attributes.clear();
337            self.current_fields.clear();
338            self.current_methods.clear();
339            self.current_adjusts.clear();
340            self.current_parents.clear();
341            self.current_mro = MethodResolutionOrder::Dfs;
342            self.current_roles.clear();
343            self.current_modifiers.clear();
344            self.current_exports.clear();
345            self.current_export_ok.clear();
346            self.current_export_tags.clear();
347            self.current_uses_exporter = false;
348            self.current_package_aliases.clear();
349        }
350    }
351
352    fn visit_node(&mut self, node: &Node) {
353        match &node.kind {
354            NodeKind::Program { statements } => {
355                self.visit_statement_list(statements);
356            }
357
358            NodeKind::Package { name, block, .. } => {
359                // Flush previous package
360                self.flush_current_package();
361
362                self.current_package = name.clone();
363                self.current_framework =
364                    self.framework_map.get(name).copied().unwrap_or(Framework::None);
365                self.current_mro = MethodResolutionOrder::Dfs;
366                self.current_uses_exporter = false;
367
368                if let Some(block) = block {
369                    self.visit_node(block);
370                }
371            }
372
373            NodeKind::Block { statements, .. } => {
374                self.visit_statement_list(statements);
375            }
376
377            NodeKind::Subroutine { name, body, .. } => {
378                if let Some(sub_name) = name {
379                    self.current_methods.push(MethodInfo::new(sub_name.clone(), node.location));
380                }
381                self.visit_node(body);
382            }
383
384            NodeKind::Use { module, args, .. } => {
385                self.detect_framework(module, args);
386            }
387
388            NodeKind::No { module, .. } if module == "mro" => {
389                self.current_mro = MethodResolutionOrder::Dfs;
390            }
391
392            // `our @ISA = qw(Parent1 Parent2);` / `our @EXPORT = qw(...);` / `our @EXPORT_OK = qw(...);`
393            NodeKind::VariableDeclaration { variable, initializer, .. } => {
394                if let NodeKind::Variable { sigil, name } = &variable.kind {
395                    if sigil == "$"
396                        && let Some(init) = initializer
397                        && self.initializer_is_current_package(init)
398                    {
399                        self.current_package_aliases.insert(name.clone());
400                    }
401
402                    if sigil == "@"
403                        && let Some(init) = initializer
404                    {
405                        match name.as_str() {
406                            "ISA" => self.extract_isa_from_node(init),
407                            "EXPORT" => {
408                                self.current_exports.extend(collect_symbol_names(init));
409                            }
410                            "EXPORT_OK" => {
411                                self.current_export_ok.extend(collect_symbol_names(init));
412                            }
413                            _ => {}
414                        }
415                    }
416                    if sigil == "%"
417                        && name == "EXPORT_TAGS"
418                        && let Some(init) = initializer
419                    {
420                        merge_export_tags(&mut self.current_export_tags, collect_export_tags(init));
421                    }
422                }
423            }
424
425            // `@ISA = qw(...);` / `@EXPORT = qw(...);` / `@EXPORT_OK = qw(...);` (bare assignment without `our`)
426            NodeKind::Assignment { lhs, rhs, .. } => {
427                if let NodeKind::Variable { sigil, name } = &lhs.kind
428                    && sigil == "@"
429                {
430                    match name.as_str() {
431                        "ISA" => self.extract_isa_from_node(rhs),
432                        "EXPORT" => {
433                            self.current_exports.extend(collect_symbol_names(rhs));
434                        }
435                        "EXPORT_OK" => {
436                            self.current_export_ok.extend(collect_symbol_names(rhs));
437                        }
438                        _ => {}
439                    }
440                }
441                if let NodeKind::Variable { sigil, name } = &lhs.kind
442                    && sigil == "%"
443                    && name == "EXPORT_TAGS"
444                {
445                    merge_export_tags(&mut self.current_export_tags, collect_export_tags(rhs));
446                }
447            }
448
449            // `push @ISA, 'Parent';` or `push @ISA, 'Base1', 'Base2';`
450            // Also recurse into the inner expression so that assignments like
451            // `@EXPORT_OK = qw(...)` (wrapped in ExpressionStatement) are still handled.
452            NodeKind::ExpressionStatement { expression } => {
453                if let NodeKind::FunctionCall { name, args } = &expression.kind
454                    && name == "push"
455                {
456                    if let Some(first_arg) = args.first() {
457                        if let NodeKind::Variable { sigil, name: var_name } = &first_arg.kind
458                            && sigil == "@"
459                            && var_name == "ISA"
460                        {
461                            for arg in args.iter().skip(1) {
462                                self.extract_isa_from_node(arg);
463                            }
464                            return;
465                        }
466                    }
467                }
468                // Fall through: visit the inner expression for assignments, etc.
469                self.visit_node(expression);
470            }
471
472            NodeKind::Class { name, parents, body } => {
473                self.flush_current_package();
474                self.current_package = name.clone();
475                self.current_framework = if self.current_framework == Framework::ObjectPad {
476                    Framework::ObjectPad
477                } else {
478                    Framework::NativeClass
479                };
480                self.current_mro = MethodResolutionOrder::Dfs;
481                self.framework_map.insert(name.clone(), self.current_framework);
482                self.current_package_aliases.clear();
483                // Populate parent classes from `:isa(Parent)` attributes
484                self.current_parents.extend(parents.iter().cloned());
485                self.visit_node(body);
486            }
487
488            NodeKind::Method { name, body, .. } => {
489                self.current_methods.push(MethodInfo::new(name.clone(), node.location));
490                self.visit_node(body);
491            }
492
493            NodeKind::Error { partial, .. } => {
494                if let Some(partial) = partial {
495                    self.visit_node(partial);
496                }
497            }
498
499            _ => {
500                // Recurse into children for other node types
501                self.visit_children(node);
502            }
503        }
504    }
505
506    fn visit_children(&mut self, node: &Node) {
507        match &node.kind {
508            NodeKind::ExpressionStatement { expression } => {
509                self.visit_node(expression);
510            }
511            NodeKind::Block { statements, .. } => {
512                self.visit_statement_list(statements);
513            }
514            NodeKind::If { condition, then_branch, else_branch, .. } => {
515                self.visit_node(condition);
516                self.visit_node(then_branch);
517                if let Some(else_node) = else_branch {
518                    self.visit_node(else_node);
519                }
520            }
521            _ => {}
522        }
523    }
524
525    fn visit_statement_list(&mut self, statements: &[Node]) {
526        let mut idx = 0;
527        while idx < statements.len() {
528            // First, check for `use` declarations to detect frameworks
529            if let NodeKind::Use { module, args, .. } = &statements[idx].kind {
530                self.detect_mro(module, args);
531                self.detect_framework(module, args);
532                idx += 1;
533                continue;
534            }
535
536            let is_framework_package = self.current_framework != Framework::None;
537
538            if is_framework_package {
539                if self.current_framework == Framework::ObjectPad
540                    && let Some(consumed) = self.try_extract_object_pad_constructs(statements, idx)
541                {
542                    idx += consumed;
543                    continue;
544                }
545                if self.current_framework == Framework::ClassAccessor
546                    && let Some(consumed) = self.try_extract_class_accessor_methods(statements, idx)
547                {
548                    idx += consumed;
549                    continue;
550                }
551                // Try to extract `has` declarations
552                if let Some(consumed) = self.try_extract_has(statements, idx) {
553                    idx += consumed;
554                    continue;
555                }
556                // Try to extract method modifiers
557                if let Some(consumed) = self.try_extract_modifier(statements, idx) {
558                    idx += consumed;
559                    continue;
560                }
561                // Try to extract extends/with
562                if let Some(consumed) = self.try_extract_extends_with(statements, idx) {
563                    idx += consumed;
564                    continue;
565                }
566            }
567
568            // Recurse into the statement for subroutines etc.
569            self.visit_node(&statements[idx]);
570            idx += 1;
571        }
572    }
573
574    /// Detect framework from a `use` statement.
575    fn detect_framework(&mut self, module: &str, args: &[String]) {
576        let framework = match module {
577            "Exporter" => {
578                self.current_uses_exporter = true;
579                return;
580            }
581            "Moose" | "Moose::Role" => Framework::Moose,
582            "Moo" | "Moo::Role" => Framework::Moo,
583            "Mouse" | "Mouse::Role" => Framework::Mouse,
584            "Role::Tiny" | "Role::Tiny::With" => Framework::RoleTiny,
585            "Class::Accessor" => Framework::ClassAccessor,
586            "Object::Pad" => Framework::ObjectPad,
587            "base" | "parent" => {
588                // Capture parent class names, skipping -norequire flag and Class::Accessor sentinel.
589                // args may be:
590                //   - A quoted string: "'Parent'"
591                //   - A qw-string:     "qw(Base1 Base2)"  (single arg, space-separated inside)
592                //   - A bare flag:     "-norequire"
593                let mut has_class_accessor = false;
594                let mut captured_parents: Vec<String> = Vec::new();
595
596                for arg in args {
597                    let trimmed = arg.trim();
598                    if trimmed.starts_with('-') || trimmed.is_empty() {
599                        // Skip flags like -norequire
600                        continue;
601                    }
602                    // Expand qw(...) into individual names
603                    let names = expand_arg_to_names(trimmed);
604                    for name in names {
605                        if name == "Class::Accessor" {
606                            has_class_accessor = true;
607                        } else if name == "Exporter" {
608                            self.current_uses_exporter = true;
609                        } else {
610                            captured_parents.push(name);
611                        }
612                    }
613                }
614
615                self.current_parents.extend(captured_parents);
616
617                if has_class_accessor {
618                    Framework::ClassAccessor
619                } else if self.current_framework == Framework::None {
620                    // Only promote to PlainOO if no stronger framework already detected
621                    Framework::PlainOO
622                } else {
623                    // Already a Moose/Moo/etc. package — keep existing framework
624                    return;
625                }
626            }
627            _ => return,
628        };
629
630        self.current_framework = framework;
631        self.framework_map.insert(self.current_package.clone(), framework);
632    }
633
634    /// Detect `use mro 'c3'` / `use mro 'dfs'` for the current package.
635    fn detect_mro(&mut self, module: &str, args: &[String]) {
636        if module != "mro" {
637            return;
638        }
639
640        if args.is_empty() {
641            self.current_mro = MethodResolutionOrder::Dfs;
642            return;
643        }
644
645        for arg in args {
646            let trimmed = arg.trim().trim_matches('\'').trim_matches('"');
647            match trimmed {
648                "c3" => {
649                    self.current_mro = MethodResolutionOrder::C3;
650                    return;
651                }
652                "dfs" => {
653                    self.current_mro = MethodResolutionOrder::Dfs;
654                    return;
655                }
656                _ => {}
657            }
658        }
659    }
660
661    /// Extract Moo/Moose `has` declarations.
662    ///
663    /// Mirrors the two-statement pattern from `SymbolExtractor::try_extract_moo_has_declaration`.
664    fn try_extract_has(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
665        let first = &statements[idx];
666
667        // Form A: two statements
668        // 1) ExpressionStatement(Identifier("has"))
669        // 2) ExpressionStatement(HashLiteral(...)) or ExpressionStatement(ArrayLiteral([..., HashLiteral]))
670        if idx + 1 < statements.len() {
671            let second = &statements[idx + 1];
672            let is_has_marker = matches!(
673                &first.kind,
674                NodeKind::ExpressionStatement { expression }
675                    if matches!(&expression.kind, NodeKind::Identifier { name } if name == "has")
676            );
677
678            if is_has_marker {
679                if let NodeKind::ExpressionStatement { expression } = &second.kind {
680                    let has_location =
681                        SourceLocation { start: first.location.start, end: second.location.end };
682
683                    match &expression.kind {
684                        NodeKind::HashLiteral { pairs } => {
685                            self.extract_has_from_pairs(pairs, has_location, false);
686                            return Some(2);
687                        }
688                        NodeKind::ArrayLiteral { elements } => {
689                            if let Some(Node { kind: NodeKind::HashLiteral { pairs }, .. }) =
690                                elements.last()
691                            {
692                                let mut names = Vec::new();
693                                for el in elements.iter().take(elements.len() - 1) {
694                                    names.extend(collect_symbol_names(el));
695                                }
696                                if !names.is_empty() {
697                                    self.extract_has_with_names(&names, pairs, has_location);
698                                    return Some(2);
699                                }
700                            }
701                        }
702                        _ => {}
703                    }
704                }
705            }
706        }
707
708        // Form B: single statement with embedded `has` marker
709        if let NodeKind::ExpressionStatement { expression } = &first.kind
710            && let NodeKind::HashLiteral { pairs } = &expression.kind
711        {
712            let has_embedded = pairs.iter().any(|(key_node, _)| {
713                matches!(
714                    &key_node.kind,
715                    NodeKind::Binary { op, left, .. }
716                        if op == "[]" && matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
717                )
718            });
719
720            if has_embedded {
721                self.extract_has_from_pairs(pairs, first.location, true);
722                return Some(1);
723            }
724        }
725
726        // Form C: FunctionCall { name: "has", args: [name_expr, HashLiteral { ... }] }
727        // Produced when the parser recognises `has 'name' => (is => 'ro', ...)` as a bare call.
728        if let NodeKind::ExpressionStatement { expression } = &first.kind
729            && let NodeKind::FunctionCall { name, args } = &expression.kind
730            && name == "has"
731            && !args.is_empty()
732        {
733            // The last arg that is a HashLiteral holds the options.
734            let options_hash_idx =
735                args.iter().rposition(|a| matches!(a.kind, NodeKind::HashLiteral { .. }));
736            if let Some(opts_idx) = options_hash_idx {
737                if let NodeKind::HashLiteral { pairs } = &args[opts_idx].kind {
738                    let names: Vec<String> =
739                        args[..opts_idx].iter().flat_map(collect_symbol_names).collect();
740                    if !names.is_empty() {
741                        self.extract_has_with_names(&names, pairs, first.location);
742                        return Some(1);
743                    }
744                }
745            }
746        }
747
748        None
749    }
750
751    /// Extract attributes from parsed `has` key/value pairs.
752    fn extract_has_from_pairs(
753        &mut self,
754        pairs: &[(Node, Node)],
755        location: SourceLocation,
756        require_embedded: bool,
757    ) {
758        for (attr_expr, options_expr) in pairs {
759            let attr_expr = if let NodeKind::Binary { op, left, right } = &attr_expr.kind
760                && op == "[]"
761                && matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
762            {
763                right.as_ref()
764            } else if require_embedded {
765                continue;
766            } else {
767                attr_expr
768            };
769
770            let names = collect_symbol_names(attr_expr);
771            if names.is_empty() {
772                continue;
773            }
774
775            if let NodeKind::HashLiteral { pairs: option_pairs } = &options_expr.kind {
776                self.extract_has_with_names(&names, option_pairs, location);
777            }
778        }
779    }
780
781    /// Build Attribute structs from attribute names and option pairs.
782    fn extract_has_with_names(
783        &mut self,
784        names: &[String],
785        option_pairs: &[(Node, Node)],
786        location: SourceLocation,
787    ) {
788        let options = extract_hash_options(option_pairs);
789
790        let is = options.get("is").and_then(|v| match v.as_str() {
791            "ro" => Some(AccessorType::Ro),
792            "rw" => Some(AccessorType::Rw),
793            "lazy" => Some(AccessorType::Lazy),
794            "bare" => Some(AccessorType::Bare),
795            _ => None,
796        });
797
798        let isa = options.get("isa").cloned();
799        let default = options.contains_key("default")
800            || options.contains_key("builder")
801            || is == Some(AccessorType::Lazy);
802        let required = options.get("required").is_some_and(|v| v == "1" || v == "true");
803        let coerce = options.get("coerce").is_some_and(|v| v == "1" || v == "true");
804        let trigger = options.contains_key("trigger");
805
806        // Determine accessor name: explicit accessor/reader overrides default
807        let explicit_accessor = options.get("accessor").or_else(|| options.get("reader")).cloned();
808
809        for raw_name in names {
810            let Some(name) = normalize_attribute_name(raw_name) else { continue };
811            let accessor_name = explicit_accessor.clone().unwrap_or_else(|| name.clone());
812
813            // builder => 1 derives `_build_<attr>`; a string value names the method directly
814            let builder = options
815                .get("builder")
816                .map(|v| if v == "1" { format!("_build_{name}") } else { v.clone() });
817
818            // predicate => 1 derives `has_<attr>`; a string value is used directly
819            let predicate = options
820                .get("predicate")
821                .map(|v| if v == "1" { format!("has_{name}") } else { v.clone() });
822
823            // clearer => 1 derives `clear_<attr>`; a string value is used directly
824            let clearer = options
825                .get("clearer")
826                .map(|v| if v == "1" { format!("clear_{name}") } else { v.clone() });
827
828            self.current_attributes.push(Attribute {
829                name: name.clone(),
830                is,
831                isa: isa.clone(),
832                default,
833                required,
834                accessor_name,
835                location,
836                builder,
837                coerce,
838                predicate,
839                clearer,
840                trigger,
841            });
842        }
843    }
844
845    /// Extract method modifiers (before/after/around/override/augment).
846    fn try_extract_modifier(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
847        let first = &statements[idx];
848
849        // FunctionCall form: `before 'save' => sub { }` parsed as a bare call.
850        if let NodeKind::ExpressionStatement { expression } = &first.kind
851            && let NodeKind::FunctionCall { name, args } = &expression.kind
852        {
853            let modifier_kind = modifier_kind_from_name(name);
854            if let Some(modifier_kind) = modifier_kind {
855                // args[0] is the method name (String or ArrayLiteral), rest is the impl.
856                let method_names: Vec<String> =
857                    args.first().map(collect_symbol_names).unwrap_or_default();
858                if !method_names.is_empty() {
859                    for method_name in method_names {
860                        self.current_modifiers.push(MethodModifier {
861                            kind: modifier_kind,
862                            method_name,
863                            location: first.location,
864                        });
865                    }
866                    return Some(1);
867                }
868            }
869        }
870
871        // Two-statement legacy form:
872        // 1) ExpressionStatement(Identifier("before"/"after"/"around"))
873        // 2) ExpressionStatement(HashLiteral((method_name, Subroutine)))
874        if idx + 1 >= statements.len() {
875            return None;
876        }
877        let second = &statements[idx + 1];
878
879        let modifier_kind = match &first.kind {
880            NodeKind::ExpressionStatement { expression } => match &expression.kind {
881                NodeKind::Identifier { name } => modifier_kind_from_name(name),
882                _ => None,
883            },
884            _ => None,
885        };
886
887        let modifier_kind = modifier_kind?;
888
889        let NodeKind::ExpressionStatement { expression } = &second.kind else {
890            return None;
891        };
892        let NodeKind::HashLiteral { pairs } = &expression.kind else {
893            return None;
894        };
895
896        let location = SourceLocation { start: first.location.start, end: second.location.end };
897
898        for (key_node, _) in pairs {
899            let method_names = collect_symbol_names(key_node);
900            for method_name in method_names {
901                self.current_modifiers.push(MethodModifier {
902                    kind: modifier_kind,
903                    method_name,
904                    location,
905                });
906            }
907        }
908
909        Some(2)
910    }
911
912    /// Extract `extends 'Parent'` and `with 'Role'` declarations.
913    fn try_extract_extends_with(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
914        let first = &statements[idx];
915
916        // Form: FunctionCall { name: "extends"/"with", args: [...] }
917        // Produced when `extends 'Parent'` / `with 'Role'` are parsed as bare calls.
918        if let NodeKind::ExpressionStatement { expression } = &first.kind
919            && let NodeKind::FunctionCall { name, args } = &expression.kind
920            && matches!(name.as_str(), "extends" | "with")
921        {
922            let names: Vec<String> = args.iter().flat_map(collect_symbol_names).collect();
923            if !names.is_empty() {
924                if name == "extends" {
925                    self.current_parents.extend(names);
926                } else {
927                    self.current_roles.extend(names);
928                }
929                return Some(1);
930            }
931        }
932
933        // Two-statement form (legacy parser output):
934        // 1) ExpressionStatement(Identifier("extends"/"with"))
935        // 2) ExpressionStatement(String/ArrayLiteral)
936        if idx + 1 >= statements.len() {
937            return None;
938        }
939        let second = &statements[idx + 1];
940
941        let keyword = match &first.kind {
942            NodeKind::ExpressionStatement { expression } => match &expression.kind {
943                NodeKind::Identifier { name } if matches!(name.as_str(), "extends" | "with") => {
944                    name.as_str()
945                }
946                _ => return None,
947            },
948            _ => return None,
949        };
950
951        let NodeKind::ExpressionStatement { expression } = &second.kind else {
952            return None;
953        };
954
955        let names = collect_symbol_names(expression);
956        if names.is_empty() {
957            return None;
958        }
959
960        if keyword == "extends" {
961            self.current_parents.extend(names);
962        } else {
963            self.current_roles.extend(names);
964        }
965
966        Some(2)
967    }
968
969    /// Extract `Class::Accessor` generated methods from `mk_*_accessors` calls.
970    fn try_extract_class_accessor_methods(
971        &mut self,
972        statements: &[Node],
973        idx: usize,
974    ) -> Option<usize> {
975        let first = &statements[idx];
976
977        let NodeKind::ExpressionStatement { expression } = &first.kind else {
978            return None;
979        };
980
981        let NodeKind::MethodCall { object, method, args } = &expression.kind else {
982            return None;
983        };
984
985        let accessor_mode = match method.as_str() {
986            "mk_accessors" => ClassAccessorMode::Rw,
987            "mk_ro_accessors" => ClassAccessorMode::Ro,
988            "mk_wo_accessors" => ClassAccessorMode::Wo,
989            _ => return None,
990        };
991
992        if !self.class_accessor_target_matches_current_package(object) {
993            return None;
994        }
995
996        let mut accessor_names = Vec::new();
997        let mut seen = HashSet::new();
998        for arg in args {
999            for name in collect_accessor_names(arg) {
1000                if seen.insert(name.clone()) {
1001                    accessor_names.push(name);
1002                }
1003            }
1004        }
1005
1006        if accessor_names.is_empty() {
1007            return None;
1008        }
1009
1010        for name in accessor_names {
1011            self.current_methods.push(MethodInfo::synthetic(
1012                name,
1013                first.location,
1014                Some(accessor_mode),
1015            ));
1016        }
1017
1018        Some(1)
1019    }
1020
1021    /// Extract Object::Pad field declarations and `ADJUST` blocks.
1022    fn try_extract_object_pad_constructs(
1023        &mut self,
1024        statements: &[Node],
1025        idx: usize,
1026    ) -> Option<usize> {
1027        let statement = &statements[idx];
1028
1029        if let Some(field) = Self::object_pad_field_from_statement(statement) {
1030            let location = field.location;
1031            let field_name = field.name.clone();
1032            let traits = field.attributes.clone();
1033
1034            self.current_fields.push(field);
1035
1036            if let Some(reader) = Self::object_pad_reader_name(&field_name, &traits) {
1037                self.current_methods.push(MethodInfo::synthetic(reader, location, None));
1038            }
1039            if let Some(writer) = Self::object_pad_writer_name(&field_name, &traits) {
1040                self.current_methods.push(MethodInfo::synthetic(writer, location, None));
1041            }
1042            if let Some(accessor) = Self::object_pad_accessor_name(&field_name, &traits) {
1043                self.current_methods.push(MethodInfo::synthetic(accessor, location, None));
1044            }
1045            if let Some(mutator) = Self::object_pad_mutator_name(&field_name, &traits) {
1046                self.current_methods.push(MethodInfo::synthetic(mutator, location, None));
1047            }
1048
1049            return Some(1);
1050        }
1051
1052        match &statement.kind {
1053            NodeKind::Method { name, body, .. } if name == "ADJUST" => {
1054                self.record_object_pad_adjust(statement.location);
1055                self.visit_node(body);
1056                return Some(1);
1057            }
1058            NodeKind::Subroutine { name, body, .. } if name.as_deref() == Some("ADJUST") => {
1059                self.record_object_pad_adjust(statement.location);
1060                self.visit_node(body);
1061                return Some(1);
1062            }
1063            _ => {}
1064        }
1065
1066        None
1067    }
1068
1069    fn record_object_pad_adjust(&mut self, location: SourceLocation) {
1070        self.current_adjusts.push(MethodInfo::synthetic("ADJUST".to_string(), location, None));
1071    }
1072
1073    fn object_pad_field_from_statement(statement: &Node) -> Option<FieldInfo> {
1074        let NodeKind::VariableDeclaration { declarator, variable, attributes, initializer } =
1075            &statement.kind
1076        else {
1077            return None;
1078        };
1079        if declarator != "field" {
1080            return None;
1081        }
1082
1083        let NodeKind::Variable { sigil, name } = &variable.kind else {
1084            return None;
1085        };
1086        if sigil != "$" {
1087            return None;
1088        }
1089
1090        let mut param = false;
1091        let mut traits = Vec::new();
1092        for attr in attributes {
1093            let attr_name = attr.trim().to_string();
1094            if attr_name == "param" {
1095                param = true;
1096            }
1097            traits.push(attr_name);
1098        }
1099
1100        let mut field = FieldInfo {
1101            name: name.clone(),
1102            location: statement.location,
1103            attributes: traits,
1104            param,
1105            reader: None,
1106            writer: None,
1107            accessor: None,
1108            mutator: None,
1109            default: initializer.as_ref().map(|node| Self::value_summary(node)),
1110        };
1111
1112        field.reader = Self::object_pad_reader_name(&field.name, &field.attributes);
1113        field.writer = Self::object_pad_writer_name(&field.name, &field.attributes);
1114        field.accessor = Self::object_pad_accessor_name(&field.name, &field.attributes);
1115        field.mutator = Self::object_pad_mutator_name(&field.name, &field.attributes);
1116
1117        Some(field)
1118    }
1119
1120    fn object_pad_reader_name(field_name: &str, traits: &[String]) -> Option<String> {
1121        if traits.iter().any(|trait_name| trait_name == "reader") {
1122            Some(Self::object_pad_public_name(field_name).to_string())
1123        } else {
1124            None
1125        }
1126    }
1127
1128    fn object_pad_writer_name(field_name: &str, traits: &[String]) -> Option<String> {
1129        if traits.iter().any(|trait_name| trait_name == "writer") {
1130            Some(format!("set_{}", Self::object_pad_public_name(field_name)))
1131        } else {
1132            None
1133        }
1134    }
1135
1136    fn object_pad_accessor_name(field_name: &str, traits: &[String]) -> Option<String> {
1137        if traits.iter().any(|trait_name| trait_name == "accessor") {
1138            Some(Self::object_pad_public_name(field_name).to_string())
1139        } else {
1140            None
1141        }
1142    }
1143
1144    fn object_pad_mutator_name(field_name: &str, traits: &[String]) -> Option<String> {
1145        if traits.iter().any(|trait_name| trait_name == "mutator") {
1146            Some(Self::object_pad_public_name(field_name).to_string())
1147        } else {
1148            None
1149        }
1150    }
1151
1152    fn object_pad_public_name(field_name: &str) -> &str {
1153        field_name.strip_prefix('_').unwrap_or(field_name)
1154    }
1155
1156    /// Return true when a `Class::Accessor` call targets the current package.
1157    fn class_accessor_target_matches_current_package(&self, object: &Node) -> bool {
1158        match &object.kind {
1159            NodeKind::Identifier { name } => name == "__PACKAGE__" || name == &self.current_package,
1160            NodeKind::String { value, .. } => {
1161                normalize_symbol_name(value).is_some_and(|name| name == self.current_package)
1162            }
1163            NodeKind::Variable { sigil, name } if sigil == "$" => {
1164                self.current_package_aliases.contains(name)
1165            }
1166            _ => false,
1167        }
1168    }
1169
1170    /// Extract parent class names from an `@ISA` RHS node (ArrayLiteral or qw-word-list).
1171    fn extract_isa_from_node(&mut self, node: &Node) {
1172        let parents = collect_symbol_names(node);
1173        if !parents.is_empty() {
1174            if parents.iter().any(|parent| parent == "Exporter") {
1175                self.current_uses_exporter = true;
1176            }
1177            // Promote to PlainOO if no stronger framework is already set
1178            if self.current_framework == Framework::None {
1179                self.current_framework = Framework::PlainOO;
1180                self.framework_map.insert(self.current_package.clone(), Framework::PlainOO);
1181            }
1182            self.current_parents.extend(parents);
1183        }
1184    }
1185
1186    fn build_exporter_metadata_for_current_package(&mut self) -> Option<ExporterMetadata> {
1187        if !self.current_uses_exporter {
1188            self.current_export_tags.clear();
1189            return None;
1190        }
1191
1192        let method_map: HashMap<&str, SourceLocation> = self
1193            .current_methods
1194            .iter()
1195            .map(|method| (method.name.as_str(), method.location))
1196            .collect();
1197
1198        let (exports, unresolved_exports) = resolve_exports(&self.current_exports, &method_map);
1199        let (export_ok, unresolved_export_ok) =
1200            resolve_exports(&self.current_export_ok, &method_map);
1201
1202        let mut export_tags: HashMap<String, Vec<ResolvedExport>> = HashMap::new();
1203        let mut unresolved = unresolved_exports;
1204        unresolved.extend(unresolved_export_ok);
1205
1206        for (tag, names) in std::mem::take(&mut self.current_export_tags) {
1207            let (resolved, unresolved_names) = resolve_exports(&names, &method_map);
1208            if !resolved.is_empty() {
1209                export_tags.insert(tag, resolved);
1210            }
1211            unresolved.extend(unresolved_names);
1212        }
1213
1214        dedupe_preserve_order(&mut unresolved);
1215
1216        Some(ExporterMetadata { exports, export_ok, export_tags, unresolved })
1217    }
1218
1219    /// Return true if a variable initializer resolves to the current package name.
1220    fn initializer_is_current_package(&self, node: &Node) -> bool {
1221        match &node.kind {
1222            NodeKind::Identifier { name } => name == "__PACKAGE__" || name == &self.current_package,
1223            NodeKind::FunctionCall { name, args } if name == "__PACKAGE__" && args.is_empty() => {
1224                true
1225            }
1226            NodeKind::String { value, .. } => {
1227                normalize_symbol_name(value).is_some_and(|name| name == self.current_package)
1228            }
1229            _ => false,
1230        }
1231    }
1232
1233    fn value_summary(node: &Node) -> String {
1234        match &node.kind {
1235            NodeKind::String { value, .. } => {
1236                normalize_symbol_name(value).unwrap_or_else(|| value.clone())
1237            }
1238            NodeKind::Identifier { name } => name.clone(),
1239            NodeKind::Number { value } => value.clone(),
1240            NodeKind::Undef => "undef".to_string(),
1241            NodeKind::Variable { sigil, name } => format!("{sigil}{name}"),
1242            _ => "expr".to_string(),
1243        }
1244    }
1245}
1246
1247// ---- Helper functions (parallel to SymbolExtractor's private helpers) ----
1248
1249fn collect_symbol_names(node: &Node) -> Vec<String> {
1250    match &node.kind {
1251        NodeKind::String { value, .. } => normalize_symbol_name(value).into_iter().collect(),
1252        NodeKind::Identifier { name } => normalize_symbol_name(name).into_iter().collect(),
1253        NodeKind::ArrayLiteral { elements } => {
1254            elements.iter().flat_map(collect_symbol_names).collect()
1255        }
1256        _ => Vec::new(),
1257    }
1258}
1259
1260fn collect_export_tags(node: &Node) -> HashMap<String, Vec<String>> {
1261    let mut tags: HashMap<String, Vec<String>> = HashMap::new();
1262    match &node.kind {
1263        NodeKind::HashLiteral { pairs } => {
1264            for (key, value) in pairs {
1265                let Some(tag_name) = collect_single_symbol_name(key) else { continue };
1266                let Some(symbols) = collect_static_symbol_names(value) else { continue };
1267                if symbols.is_empty() {
1268                    continue;
1269                }
1270                tags.entry(tag_name).or_default().extend(symbols);
1271            }
1272        }
1273        NodeKind::ArrayLiteral { elements } => {
1274            for pair in elements.chunks_exact(2) {
1275                let Some(tag_name) = collect_single_symbol_name(&pair[0]) else { continue };
1276                let Some(symbols) = collect_static_symbol_names(&pair[1]) else { continue };
1277                if symbols.is_empty() {
1278                    continue;
1279                }
1280                tags.entry(tag_name).or_default().extend(symbols);
1281            }
1282        }
1283        NodeKind::Binary { op, .. } if op == "," => {
1284            let mut flattened = Vec::new();
1285            flatten_comma_expression(node, &mut flattened);
1286            for element in flattened {
1287                if let NodeKind::Binary { op, left, right } = &element.kind
1288                    && op == "=>"
1289                {
1290                    let Some(tag_name) = collect_single_symbol_name(left) else { continue };
1291                    let Some(symbols) = collect_static_symbol_names(right) else { continue };
1292                    if symbols.is_empty() {
1293                        continue;
1294                    }
1295                    tags.entry(tag_name).or_default().extend(symbols);
1296                }
1297            }
1298        }
1299        _ => {}
1300    }
1301
1302    for symbols in tags.values_mut() {
1303        dedupe_preserve_order(symbols);
1304    }
1305    tags
1306}
1307
1308fn collect_single_symbol_name(node: &Node) -> Option<String> {
1309    let mut names = collect_static_symbol_names(node)?;
1310    dedupe_preserve_order(&mut names);
1311    names.into_iter().next()
1312}
1313
1314fn collect_static_symbol_names(node: &Node) -> Option<Vec<String>> {
1315    match &node.kind {
1316        NodeKind::String { value, .. } => Some(expand_export_name_list(value)),
1317        NodeKind::Identifier { name } => Some(expand_export_name_list(name)),
1318        NodeKind::ArrayLiteral { elements } => {
1319            let mut names = Vec::new();
1320            for element in elements {
1321                let mut element_names = collect_static_symbol_names(element)?;
1322                names.append(&mut element_names);
1323            }
1324            Some(names)
1325        }
1326        NodeKind::Binary { op, left, right } if op == "," => {
1327            let mut names = collect_static_symbol_names(left)?;
1328            let mut right_names = collect_static_symbol_names(right)?;
1329            names.append(&mut right_names);
1330            Some(names)
1331        }
1332        _ => None,
1333    }
1334}
1335
1336fn resolve_exports(
1337    names: &[String],
1338    method_map: &HashMap<&str, SourceLocation>,
1339) -> (Vec<ResolvedExport>, Vec<String>) {
1340    let mut resolved = Vec::new();
1341    let mut unresolved = Vec::new();
1342    for raw_name in names {
1343        for name in expand_export_name_list(raw_name) {
1344            if let Some(location) = method_map.get(name.as_str()) {
1345                resolved.push(ResolvedExport { name, location: *location });
1346            } else {
1347                unresolved.push(name);
1348            }
1349        }
1350    }
1351    dedupe_resolved_exports(&mut resolved);
1352    dedupe_preserve_order(&mut unresolved);
1353    (resolved, unresolved)
1354}
1355
1356fn dedupe_resolved_exports(exports: &mut Vec<ResolvedExport>) {
1357    let mut seen = HashSet::new();
1358    exports.retain(|item| seen.insert(item.name.clone()));
1359}
1360
1361fn dedupe_preserve_order(items: &mut Vec<String>) {
1362    let mut seen = HashSet::new();
1363    items.retain(|item| seen.insert(item.clone()));
1364}
1365
1366fn merge_export_tags(
1367    target: &mut HashMap<String, Vec<String>>,
1368    updates: HashMap<String, Vec<String>>,
1369) {
1370    for (tag, mut symbols) in updates {
1371        target.entry(tag).or_default().append(&mut symbols);
1372    }
1373}
1374
1375fn flatten_comma_expression<'a>(node: &'a Node, out: &mut Vec<&'a Node>) {
1376    if let NodeKind::Binary { op, left, right } = &node.kind
1377        && op == ","
1378    {
1379        flatten_comma_expression(left, out);
1380        flatten_comma_expression(right, out);
1381    } else {
1382        out.push(node);
1383    }
1384}
1385
1386fn expand_export_name_list(raw: &str) -> Vec<String> {
1387    expand_symbol_list(raw).into_iter().filter_map(|name| normalize_export_name(&name)).collect()
1388}
1389
1390fn normalize_export_name(raw: &str) -> Option<String> {
1391    let normalized = normalize_symbol_name(raw)?;
1392    let stripped = normalized
1393        .strip_prefix('&')
1394        .or_else(|| normalized.strip_prefix('$'))
1395        .or_else(|| normalized.strip_prefix('@'))
1396        .or_else(|| normalized.strip_prefix('%'))
1397        .unwrap_or(&normalized)
1398        .to_string();
1399    if stripped.is_empty() { None } else { Some(stripped) }
1400}
1401
1402fn collect_accessor_names(node: &Node) -> Vec<String> {
1403    match &node.kind {
1404        NodeKind::String { value, .. } => expand_symbol_list(value),
1405        NodeKind::Identifier { name } => expand_symbol_list(name),
1406        NodeKind::ArrayLiteral { elements } => {
1407            elements.iter().flat_map(collect_accessor_names).collect()
1408        }
1409        _ => Vec::new(),
1410    }
1411}
1412
1413fn modifier_kind_from_name(name: &str) -> Option<ModifierKind> {
1414    match name {
1415        "before" => Some(ModifierKind::Before),
1416        "after" => Some(ModifierKind::After),
1417        "around" => Some(ModifierKind::Around),
1418        "override" => Some(ModifierKind::Override),
1419        "augment" => Some(ModifierKind::Augment),
1420        _ => None,
1421    }
1422}
1423
1424fn normalize_symbol_name(raw: &str) -> Option<String> {
1425    let trimmed = raw.trim().trim_matches('\'').trim_matches('"').trim();
1426    if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }
1427}
1428
1429fn normalize_attribute_name(raw: &str) -> Option<String> {
1430    let trimmed = raw.trim();
1431    let without_override_prefix = trimmed.strip_prefix('+').unwrap_or(trimmed);
1432    normalize_symbol_name(without_override_prefix)
1433}
1434
1435fn expand_symbol_list(raw: &str) -> Vec<String> {
1436    let raw = raw.trim();
1437
1438    if raw.starts_with("qw(") && raw.ends_with(')') {
1439        let content = &raw[3..raw.len() - 1];
1440        return content
1441            .split_whitespace()
1442            .filter(|s| !s.is_empty())
1443            .map(|s| s.to_string())
1444            .collect();
1445    }
1446
1447    if raw.starts_with("qw") && raw.len() > 2 {
1448        let open = raw.chars().nth(2).unwrap_or(' ');
1449        let close = match open {
1450            '(' => ')',
1451            '{' => '}',
1452            '[' => ']',
1453            '<' => '>',
1454            c => c,
1455        };
1456        if let (Some(start), Some(end)) = (raw.find(open), raw.rfind(close))
1457            && start < end
1458        {
1459            let content = &raw[start + 1..end];
1460            return content
1461                .split_whitespace()
1462                .filter(|s| !s.is_empty())
1463                .map(|s| s.to_string())
1464                .collect();
1465        }
1466    }
1467
1468    normalize_symbol_name(raw).into_iter().collect()
1469}
1470
1471/// Expand a single `use parent`/`use base` arg string into individual class names.
1472///
1473/// The parser stores qw-lists as a single string like `"qw(Base1 Base2)"`.
1474/// This function splits those into `["Base1", "Base2"]`.
1475/// Plain quoted strings like `"'Parent'"` return `["Parent"]`.
1476fn expand_arg_to_names(arg: &str) -> Vec<String> {
1477    let arg = arg.trim();
1478    // qw(...) — any delimiter variant that the parser normalised to qw(...)
1479    if arg.starts_with("qw(") && arg.ends_with(')') {
1480        let content = &arg[3..arg.len() - 1];
1481        return content
1482            .split_whitespace()
1483            .filter(|s| !s.is_empty())
1484            .map(|s| s.to_string())
1485            .collect();
1486    }
1487    // Other qw variants: qw{...}, qw[...], qw/.../ etc.
1488    if arg.starts_with("qw") && arg.len() > 2 {
1489        let open = arg.chars().nth(2).unwrap_or(' ');
1490        let close = match open {
1491            '(' => ')',
1492            '{' => '}',
1493            '[' => ']',
1494            '<' => '>',
1495            c => c,
1496        };
1497        if let (Some(start), Some(end)) = (arg.find(open), arg.rfind(close)) {
1498            if start < end {
1499                let content = &arg[start + 1..end];
1500                return content
1501                    .split_whitespace()
1502                    .filter(|s| !s.is_empty())
1503                    .map(|s| s.to_string())
1504                    .collect();
1505            }
1506        }
1507    }
1508    // Quoted string or bare identifier
1509    normalize_symbol_name(arg).into_iter().collect()
1510}
1511
1512fn extract_hash_options(pairs: &[(Node, Node)]) -> HashMap<String, String> {
1513    let mut options = HashMap::new();
1514    for (key_node, value_node) in pairs {
1515        let Some(key_name) = collect_symbol_names(key_node).into_iter().next() else {
1516            continue;
1517        };
1518        let value_text = value_summary(value_node);
1519        options.insert(key_name, value_text);
1520    }
1521    options
1522}
1523
1524fn value_summary(node: &Node) -> String {
1525    match &node.kind {
1526        NodeKind::String { value, .. } => {
1527            normalize_symbol_name(value).unwrap_or_else(|| value.clone())
1528        }
1529        NodeKind::Identifier { name } => name.clone(),
1530        NodeKind::Number { value } => value.clone(),
1531        _ => "expr".to_string(),
1532    }
1533}
1534
1535#[cfg(test)]
1536mod tests {
1537    use super::*;
1538    use crate::parser::Parser;
1539    use perl_tdd_support::must;
1540    use std::collections::HashSet;
1541
1542    fn build_models(code: &str) -> Vec<ClassModel> {
1543        let mut parser = Parser::new(code);
1544        let ast = must(parser.parse());
1545        ClassModelBuilder::new().build(&ast)
1546    }
1547
1548    fn find_model<'a>(models: &'a [ClassModel], name: &str) -> Option<&'a ClassModel> {
1549        models.iter().find(|m| m.name == name)
1550    }
1551
1552    fn has_method(
1553        model: &ClassModel,
1554        name: &str,
1555        synthetic: bool,
1556        accessor_mode: Option<ClassAccessorMode>,
1557    ) -> bool {
1558        model.methods.iter().any(|method| {
1559            method.name == name
1560                && method.synthetic == synthetic
1561                && method.accessor_mode == accessor_mode
1562        })
1563    }
1564
1565    #[test]
1566    fn basic_moo_class() {
1567        let models = build_models(
1568            r#"
1569package MyApp::User;
1570use Moo;
1571
1572has 'name' => (is => 'ro', isa => 'Str');
1573has 'age' => (is => 'rw', required => 1);
1574
1575sub greet { }
1576"#,
1577        );
1578
1579        let model = find_model(&models, "MyApp::User");
1580        assert!(model.is_some(), "expected ClassModel for MyApp::User");
1581        let model = model.unwrap();
1582
1583        assert_eq!(model.framework, Framework::Moo);
1584        assert_eq!(model.attributes.len(), 2);
1585
1586        let name_attr = model.attributes.iter().find(|a| a.name == "name");
1587        assert!(name_attr.is_some());
1588        let name_attr = name_attr.unwrap();
1589        assert_eq!(name_attr.is, Some(AccessorType::Ro));
1590        assert_eq!(name_attr.isa.as_deref(), Some("Str"));
1591        assert!(!name_attr.required);
1592        assert_eq!(name_attr.accessor_name, "name");
1593
1594        let age_attr = model.attributes.iter().find(|a| a.name == "age");
1595        assert!(age_attr.is_some());
1596        let age_attr = age_attr.unwrap();
1597        assert_eq!(age_attr.is, Some(AccessorType::Rw));
1598        assert!(age_attr.required);
1599
1600        assert!(model.methods.iter().any(|m| m.name == "greet"));
1601    }
1602
1603    #[test]
1604    fn moose_extends_and_with() {
1605        let models = build_models(
1606            r#"
1607package MyApp::Admin;
1608use Moose;
1609extends 'MyApp::User';
1610with 'MyApp::Printable', 'MyApp::Serializable';
1611
1612has 'level' => (is => 'ro');
1613"#,
1614        );
1615
1616        let model = find_model(&models, "MyApp::Admin");
1617        assert!(model.is_some());
1618        let model = model.unwrap();
1619
1620        assert_eq!(model.framework, Framework::Moose);
1621        assert!(model.parents.contains(&"MyApp::User".to_string()));
1622        assert_eq!(model.roles, vec!["MyApp::Printable", "MyApp::Serializable"]);
1623        assert_eq!(model.attributes.len(), 1);
1624    }
1625
1626    #[test]
1627    fn mro_pragma_tracks_c3_and_reset() {
1628        let models = build_models(
1629            r#"
1630package Example::Child;
1631use parent 'Example::Base';
1632use mro 'c3';
1633sub greet { }
1634
1635package Example::Sibling;
1636use parent 'Example::Base';
1637no mro;
1638sub greet { }
1639"#,
1640        );
1641
1642        let child = find_model(&models, "Example::Child").expect("expected ClassModel for Child");
1643        assert_eq!(child.mro, MethodResolutionOrder::C3);
1644
1645        let sibling =
1646            find_model(&models, "Example::Sibling").expect("expected ClassModel for Sibling");
1647        assert_eq!(sibling.mro, MethodResolutionOrder::Dfs);
1648    }
1649
1650    #[test]
1651    fn method_modifiers() {
1652        let models = build_models(
1653            r#"
1654package MyApp::User;
1655use Moo;
1656before 'save' => sub { };
1657after 'save' => sub { };
1658around 'validate' => sub { };
1659override 'dispatch' => sub { super(); };
1660augment 'serialize' => sub { inner(); };
1661"#,
1662        );
1663
1664        let model = find_model(&models, "MyApp::User");
1665        assert!(model.is_some());
1666        let model = model.unwrap();
1667
1668        assert_eq!(model.modifiers.len(), 5);
1669        assert!(
1670            model
1671                .modifiers
1672                .iter()
1673                .any(|m| m.kind == ModifierKind::Before && m.method_name == "save")
1674        );
1675        assert!(
1676            model
1677                .modifiers
1678                .iter()
1679                .any(|m| m.kind == ModifierKind::After && m.method_name == "save")
1680        );
1681        assert!(
1682            model
1683                .modifiers
1684                .iter()
1685                .any(|m| m.kind == ModifierKind::Around && m.method_name == "validate")
1686        );
1687        assert!(
1688            model
1689                .modifiers
1690                .iter()
1691                .any(|m| m.kind == ModifierKind::Override && m.method_name == "dispatch")
1692        );
1693        assert!(
1694            model
1695                .modifiers
1696                .iter()
1697                .any(|m| m.kind == ModifierKind::Augment && m.method_name == "serialize")
1698        );
1699    }
1700
1701    #[test]
1702    fn class_accessor_generates_synthetic_methods_for_all_variants() {
1703        let models = build_models(
1704            r#"
1705package Example::Accessors;
1706use parent 'Class::Accessor';
1707__PACKAGE__->mk_accessors(qw(foo bar));
1708sub other_method { }
1709
1710package Example::ReadOnly;
1711use parent 'Class::Accessor';
1712my $package = __PACKAGE__;
1713$package->mk_ro_accessors('id');
1714
1715package Example::WriteOnly;
1716use parent 'Class::Accessor';
1717my $package = 'Example::WriteOnly';
1718$package->mk_wo_accessors([qw(token)]);
1719"#,
1720        );
1721
1722        let accessors = find_model(&models, "Example::Accessors")
1723            .expect("expected ClassModel for Example::Accessors");
1724        assert!(has_method(accessors, "foo", true, Some(ClassAccessorMode::Rw)));
1725        assert!(has_method(accessors, "bar", true, Some(ClassAccessorMode::Rw)));
1726        assert!(has_method(accessors, "other_method", false, None));
1727
1728        let read_only = find_model(&models, "Example::ReadOnly")
1729            .expect("expected ClassModel for Example::ReadOnly");
1730        assert!(has_method(read_only, "id", true, Some(ClassAccessorMode::Ro)));
1731
1732        let write_only = find_model(&models, "Example::WriteOnly")
1733            .expect("expected ClassModel for Example::WriteOnly");
1734        assert!(has_method(write_only, "token", true, Some(ClassAccessorMode::Wo)));
1735    }
1736
1737    #[test]
1738    fn no_model_for_plain_package() {
1739        let models = build_models(
1740            r#"
1741package MyApp::Utils;
1742sub helper { 1 }
1743"#,
1744        );
1745
1746        assert!(
1747            find_model(&models, "MyApp::Utils").is_none(),
1748            "plain package should not produce a ClassModel"
1749        );
1750    }
1751
1752    #[test]
1753    fn multiple_packages() {
1754        let models = build_models(
1755            r#"
1756package MyApp::User;
1757use Moo;
1758has 'name' => (is => 'ro');
1759
1760package MyApp::Admin;
1761use Moose;
1762extends 'MyApp::User';
1763has 'level' => (is => 'rw');
1764
1765package MyApp::Utils;
1766sub helper { 1 }
1767"#,
1768        );
1769
1770        assert_eq!(models.len(), 2, "expected 2 ClassModels (User + Admin, not Utils)");
1771        assert!(find_model(&models, "MyApp::User").is_some());
1772        assert!(find_model(&models, "MyApp::Admin").is_some());
1773        assert!(find_model(&models, "MyApp::Utils").is_none());
1774    }
1775
1776    #[test]
1777    fn qw_attribute_list() {
1778        let models = build_models(
1779            r#"
1780use Moo;
1781has [qw(first_name last_name)] => (is => 'ro');
1782"#,
1783        );
1784
1785        assert_eq!(models.len(), 1);
1786        let model = &models[0];
1787        assert_eq!(model.attributes.len(), 2);
1788
1789        let names: HashSet<_> = model.attributes.iter().map(|a| a.name.as_str()).collect();
1790        assert!(names.contains("first_name"));
1791        assert!(names.contains("last_name"));
1792    }
1793
1794    #[test]
1795    fn has_framework_helper() {
1796        let models = build_models(
1797            r#"
1798package MyApp::User;
1799use Moo;
1800has 'name' => (is => 'ro');
1801"#,
1802        );
1803
1804        let model = find_model(&models, "MyApp::User").unwrap();
1805        assert!(model.has_framework());
1806    }
1807
1808    #[test]
1809    fn accessor_type_lazy() {
1810        let models = build_models(
1811            r#"
1812use Moo;
1813has 'config' => (is => 'lazy');
1814"#,
1815        );
1816
1817        let model = &models[0];
1818        assert_eq!(model.attributes[0].is, Some(AccessorType::Lazy));
1819        assert!(model.attributes[0].default, "lazy implies default");
1820    }
1821
1822    #[test]
1823    fn explicit_accessor_name() {
1824        let models = build_models(
1825            r#"
1826use Moo;
1827has 'name' => (is => 'ro', reader => 'get_name');
1828"#,
1829        );
1830
1831        let model = &models[0];
1832        assert_eq!(model.attributes[0].accessor_name, "get_name");
1833    }
1834
1835    #[test]
1836    fn inherited_attribute_override_strips_plus_prefix() {
1837        let models = build_models(
1838            r#"
1839use Moo;
1840has '+name' => (is => 'ro', builder => 1, predicate => 1, clearer => 1);
1841"#,
1842        );
1843
1844        let model = &models[0];
1845        let attr = &model.attributes[0];
1846        assert_eq!(attr.name, "name");
1847        assert_eq!(attr.accessor_name, "name");
1848        assert_eq!(attr.builder.as_deref(), Some("_build_name"));
1849        assert_eq!(attr.predicate.as_deref(), Some("has_name"));
1850        assert_eq!(attr.clearer.as_deref(), Some("clear_name"));
1851    }
1852
1853    #[test]
1854    fn default_via_builder_option() {
1855        let models = build_models(
1856            r#"
1857use Moo;
1858has 'config' => (is => 'ro', builder => 1);
1859"#,
1860        );
1861
1862        let model = &models[0];
1863        assert!(model.attributes[0].default, "builder option implies default");
1864    }
1865
1866    #[test]
1867    fn lazy_builder_with_string_name() {
1868        let models = build_models(
1869            r#"
1870use Moo;
1871has 'config' => (is => 'ro', lazy => 1, builder => '_build_config');
1872"#,
1873        );
1874
1875        let model = &models[0];
1876        let attr = &model.attributes[0];
1877        assert_eq!(
1878            attr.builder.as_deref(),
1879            Some("_build_config"),
1880            "builder string should be captured"
1881        );
1882        assert!(attr.default, "named builder implies default");
1883    }
1884
1885    #[test]
1886    fn lazy_builder_with_numeric_one_generates_default_name() {
1887        let models = build_models(
1888            r#"
1889use Moo;
1890has 'profile' => (is => 'ro', builder => 1);
1891"#,
1892        );
1893
1894        let model = &models[0];
1895        let attr = &model.attributes[0];
1896        assert_eq!(
1897            attr.builder.as_deref(),
1898            Some("_build_profile"),
1899            "builder => 1 should derive builder name as '_build_<attr>'"
1900        );
1901    }
1902
1903    #[test]
1904    fn predicate_with_string_name() {
1905        let models = build_models(
1906            r#"
1907use Moo;
1908has 'name' => (is => 'ro', predicate => 'has_name');
1909"#,
1910        );
1911
1912        let model = &models[0];
1913        let attr = &model.attributes[0];
1914        assert_eq!(
1915            attr.predicate.as_deref(),
1916            Some("has_name"),
1917            "predicate string name should be captured"
1918        );
1919    }
1920
1921    #[test]
1922    fn predicate_with_numeric_one_generates_default_name() {
1923        let models = build_models(
1924            r#"
1925use Moo;
1926has 'name' => (is => 'ro', predicate => 1);
1927"#,
1928        );
1929
1930        let model = &models[0];
1931        let attr = &model.attributes[0];
1932        assert_eq!(
1933            attr.predicate.as_deref(),
1934            Some("has_name"),
1935            "predicate => 1 should derive predicate name as 'has_<attr>'"
1936        );
1937    }
1938
1939    #[test]
1940    fn clearer_with_string_name() {
1941        let models = build_models(
1942            r#"
1943use Moo;
1944has 'name' => (is => 'rw', clearer => 'clear_name');
1945"#,
1946        );
1947
1948        let model = &models[0];
1949        let attr = &model.attributes[0];
1950        assert_eq!(
1951            attr.clearer.as_deref(),
1952            Some("clear_name"),
1953            "clearer string name should be captured"
1954        );
1955    }
1956
1957    #[test]
1958    fn clearer_with_numeric_one_generates_default_name() {
1959        let models = build_models(
1960            r#"
1961use Moo;
1962has 'name' => (is => 'rw', clearer => 1);
1963"#,
1964        );
1965
1966        let model = &models[0];
1967        let attr = &model.attributes[0];
1968        assert_eq!(
1969            attr.clearer.as_deref(),
1970            Some("clear_name"),
1971            "clearer => 1 should derive clearer name as 'clear_<attr>'"
1972        );
1973    }
1974
1975    #[test]
1976    fn coerce_flag_true() {
1977        let models = build_models(
1978            r#"
1979use Moose;
1980has 'age' => (is => 'rw', isa => 'Int', coerce => 1);
1981"#,
1982        );
1983
1984        let model = &models[0];
1985        let attr = &model.attributes[0];
1986        assert!(attr.coerce, "coerce => 1 should set coerce flag");
1987    }
1988
1989    #[test]
1990    fn coerce_flag_false_when_absent() {
1991        let models = build_models(
1992            r#"
1993use Moose;
1994has 'age' => (is => 'rw', isa => 'Int');
1995"#,
1996        );
1997
1998        let model = &models[0];
1999        let attr = &model.attributes[0];
2000        assert!(!attr.coerce, "coerce should be false when not specified");
2001    }
2002
2003    #[test]
2004    fn trigger_flag_true() {
2005        let models = build_models(
2006            r#"
2007use Moose;
2008has 'name' => (is => 'rw', trigger => \&_on_name_change);
2009"#,
2010        );
2011
2012        let model = &models[0];
2013        let attr = &model.attributes[0];
2014        assert!(attr.trigger, "trigger option should set trigger flag");
2015    }
2016
2017    #[test]
2018    fn trigger_flag_false_when_absent() {
2019        let models = build_models(
2020            r#"
2021use Moose;
2022has 'name' => (is => 'rw');
2023"#,
2024        );
2025
2026        let model = &models[0];
2027        let attr = &model.attributes[0];
2028        assert!(!attr.trigger, "trigger should be false when not specified");
2029    }
2030
2031    // ── Bug 4 tests: NativeClass framework (must fail before fix) ─────────
2032
2033    #[test]
2034    fn native_class_produces_model() {
2035        let models = build_models(
2036            r#"
2037class MyApp::Point {
2038    field $x :param = 0;
2039    field $y :param = 0;
2040    method get_x { return $x; }
2041    method get_y { return $y; }
2042}
2043"#,
2044        );
2045        assert_eq!(models.len(), 1, "expected one ClassModel for MyApp::Point");
2046        let model = &models[0];
2047        assert_eq!(model.name, "MyApp::Point");
2048        assert_eq!(model.framework, Framework::NativeClass);
2049        assert_eq!(model.methods.len(), 2);
2050        assert!(model.methods.iter().any(|m| m.name == "get_x"));
2051        assert!(model.methods.iter().any(|m| m.name == "get_y"));
2052    }
2053
2054    #[test]
2055    fn native_class_and_moo_class_do_not_interfere() {
2056        let models = build_models(
2057            r#"
2058class Native::Point {
2059    field $x :param = 0;
2060    method get_x { return $x; }
2061}
2062
2063package Moo::User;
2064use Moo;
2065has 'name' => (is => 'ro');
2066"#,
2067        );
2068        assert_eq!(models.len(), 2, "expected 2 ClassModels: Native::Point and Moo::User");
2069        let native = models.iter().find(|m| m.name == "Native::Point");
2070        assert!(native.is_some(), "expected Native::Point model");
2071        let native = native.unwrap();
2072        assert_eq!(native.framework, Framework::NativeClass);
2073        let moo = models.iter().find(|m| m.name == "Moo::User");
2074        assert!(moo.is_some(), "expected Moo::User model");
2075        let moo = moo.unwrap();
2076        assert_eq!(moo.framework, Framework::Moo);
2077    }
2078
2079    #[test]
2080    fn object_pad_fields_and_accessors_are_tracked() {
2081        let models = build_models(
2082            r#"
2083use Object::Pad;
2084
2085class Point {
2086    field $x :param :reader = 0;
2087    field $y :param :writer = 1;
2088
2089    method move { }
2090}
2091"#,
2092        );
2093
2094        assert!(
2095            !models.is_empty(),
2096            "expected at least one model, got {:?}",
2097            models.iter().map(|m| (&m.name, m.framework)).collect::<Vec<_>>()
2098        );
2099        let model = find_model(&models, "Point").expect("Point model");
2100        assert_eq!(model.framework, Framework::ObjectPad);
2101        assert_eq!(model.fields.len(), 2);
2102        assert!(has_method(model, "x", true, None));
2103        assert!(has_method(model, "set_y", true, None));
2104        assert!(has_method(model, "move", false, None));
2105
2106        let x = model.fields.iter().find(|field| field.name == "x").unwrap();
2107        assert!(x.param);
2108        assert_eq!(x.reader.as_deref(), Some("x"));
2109        assert_eq!(x.default.as_deref(), Some("0"));
2110
2111        let y = model.fields.iter().find(|field| field.name == "y").unwrap();
2112        assert!(y.param);
2113        assert_eq!(y.writer.as_deref(), Some("set_y"));
2114        assert_eq!(y.default.as_deref(), Some("1"));
2115
2116        let param_names: Vec<_> = model.object_pad_param_field_names().collect();
2117        assert_eq!(param_names, vec!["x", "y"]);
2118    }
2119
2120    #[test]
2121    fn object_pad_adjust_blocks_are_tracked() {
2122        let models = build_models(
2123            r#"
2124use Object::Pad;
2125
2126class Config {
2127    ADJUST {
2128        my $tmp = 1;
2129    }
2130}
2131"#,
2132        );
2133
2134        let model = find_model(&models, "Config").expect("Config model");
2135        assert_eq!(model.framework, Framework::ObjectPad);
2136        assert_eq!(model.adjusts.len(), 1, "expected one ADJUST block");
2137        assert_eq!(model.adjusts[0].name, "ADJUST");
2138        assert!(model.adjusts[0].synthetic, "ADJUST should be modeled as synthetic");
2139    }
2140
2141    #[test]
2142    fn object_pad_param_field_names_exclude_non_param_fields() {
2143        let models = build_models(
2144            r#"
2145use Object::Pad;
2146
2147class Config {
2148    field $name :param;
2149    field $cache = 1;
2150}
2151"#,
2152        );
2153
2154        let model = find_model(&models, "Config").expect("Config model");
2155        let param_names: Vec<_> = model.object_pad_param_field_names().collect();
2156        assert_eq!(param_names, vec!["name"]);
2157    }
2158
2159    #[test]
2160    fn object_pad_generated_names_follow_documented_defaults() {
2161        let models = build_models(
2162            r#"
2163use Object::Pad;
2164
2165class Defaults {
2166    field $_secret :reader :writer :accessor :mutator;
2167}
2168"#,
2169        );
2170
2171        let model = find_model(&models, "Defaults").expect("Defaults model");
2172        let field = model.fields.iter().find(|field| field.name == "_secret").unwrap();
2173
2174        assert_eq!(field.reader.as_deref(), Some("secret"));
2175        assert_eq!(field.writer.as_deref(), Some("set_secret"));
2176        assert_eq!(field.accessor.as_deref(), Some("secret"));
2177        assert_eq!(field.mutator.as_deref(), Some("secret"));
2178
2179        assert!(has_method(model, "secret", true, None));
2180        assert!(has_method(model, "set_secret", true, None));
2181    }
2182
2183    #[test]
2184    fn all_advanced_options_together() {
2185        let models = build_models(
2186            r#"
2187use Moo;
2188has 'status' => (
2189    is        => 'rw',
2190    isa       => 'Str',
2191    builder   => '_build_status',
2192    coerce    => 1,
2193    predicate => 'has_status',
2194    clearer   => 'clear_status',
2195    trigger   => \&_on_status_change,
2196);
2197"#,
2198        );
2199
2200        let model = &models[0];
2201        let attr = &model.attributes[0];
2202        assert_eq!(attr.builder.as_deref(), Some("_build_status"));
2203        assert!(attr.coerce);
2204        assert_eq!(attr.predicate.as_deref(), Some("has_status"));
2205        assert_eq!(attr.clearer.as_deref(), Some("clear_status"));
2206        assert!(attr.trigger);
2207    }
2208
2209    // ── Phase 1 tests: plain OO inheritance detection ─────────────────────
2210
2211    #[test]
2212    fn use_parent_plain_oo() {
2213        let code = "package Child; use parent 'Parent'; sub greet { } 1;";
2214        let models = build_models(code);
2215        let model = find_model(&models, "Child").expect("Child model");
2216        assert_eq!(model.framework, Framework::PlainOO);
2217        assert!(model.parents.contains(&"Parent".to_string()), "parents should contain 'Parent'");
2218    }
2219
2220    #[test]
2221    fn use_parent_multiple() {
2222        let code = "package Child; use parent qw(Base1 Base2); 1;";
2223        let models = build_models(code);
2224        let model = find_model(&models, "Child").expect("Child model");
2225        assert_eq!(model.framework, Framework::PlainOO);
2226        assert!(model.parents.contains(&"Base1".to_string()), "parents should contain Base1");
2227        assert!(model.parents.contains(&"Base2".to_string()), "parents should contain Base2");
2228    }
2229
2230    #[test]
2231    fn isa_array_assignment() {
2232        let code = "package Child; our @ISA = qw(Parent); sub greet { } 1;";
2233        let models = build_models(code);
2234        let model = find_model(&models, "Child").expect("Child model");
2235        assert!(
2236            model.parents.contains(&"Parent".to_string()),
2237            "parents should contain 'Parent' from @ISA"
2238        );
2239    }
2240
2241    #[test]
2242    fn use_parent_norequire() {
2243        // use parent -norequire, 'Base' — the -norequire flag must not prevent parent capture
2244        let code = "package Child; use parent -norequire, 'Base'; 1;";
2245        let models = build_models(code);
2246        let model = find_model(&models, "Child").expect("Child model");
2247        assert!(
2248            model.parents.contains(&"Base".to_string()),
2249            "parents should contain 'Base' even with -norequire"
2250        );
2251    }
2252
2253    #[test]
2254    fn use_base_plain_oo() {
2255        let code = "package Child; use base 'Parent'; sub greet { } 1;";
2256        let models = build_models(code);
2257        let model = find_model(&models, "Child").expect("Child model");
2258        assert_eq!(model.framework, Framework::PlainOO);
2259        assert!(
2260            model.parents.contains(&"Parent".to_string()),
2261            "parents should contain 'Parent' from use base"
2262        );
2263    }
2264
2265    #[test]
2266    fn plain_oo_does_not_regress_moose_extends() {
2267        // Moose classes should still work and use parent field from extends
2268        let models = build_models(
2269            r#"
2270package MyApp::Admin;
2271use Moose;
2272extends 'MyApp::User';
2273has 'level' => (is => 'ro');
2274"#,
2275        );
2276        let model = find_model(&models, "MyApp::Admin").expect("Admin model");
2277        assert_eq!(model.framework, Framework::Moose);
2278        assert!(
2279            model.parents.contains(&"MyApp::User".to_string()),
2280            "Moose extends should still populate parents"
2281        );
2282    }
2283
2284    // ---- Gap 1: Export surface ----
2285
2286    #[test]
2287    fn export_array_captured() {
2288        let code = "package MyUtils;\nour @EXPORT = qw(foo bar);\nour @EXPORT_OK = qw(baz);\nsub foo {}\nsub bar {}\nsub baz {}\n1;";
2289        let models = build_models(code);
2290        let model = find_model(&models, "MyUtils").expect("MyUtils model");
2291        assert_eq!(model.exports, vec!["foo".to_string(), "bar".to_string()]);
2292        assert_eq!(model.export_ok, vec!["baz".to_string()]);
2293    }
2294
2295    #[test]
2296    fn export_non_oo_package_produces_model() {
2297        let code = "package MyUtils;\nour @EXPORT = qw(helper);\nsub helper { 1 }\n1;";
2298        let models = build_models(code);
2299        assert!(
2300            find_model(&models, "MyUtils").is_some(),
2301            "export-only package must produce a model"
2302        );
2303    }
2304
2305    #[test]
2306    fn export_ok_assignment_without_our() {
2307        let code = "package MyLib;\n@EXPORT_OK = qw(util_a util_b);\n1;";
2308        let models = build_models(code);
2309        let model = find_model(&models, "MyLib").expect("MyLib model");
2310        assert_eq!(model.export_ok, vec!["util_a".to_string(), "util_b".to_string()]);
2311    }
2312
2313    #[test]
2314    fn export_assignment_without_our() {
2315        // Symmetric to export_ok_assignment_without_our: bare `@EXPORT = qw(...)` form.
2316        let code = "package MyLib;\n@EXPORT = qw(func_a func_b);\n1;";
2317        let models = build_models(code);
2318        let model = find_model(&models, "MyLib").expect("MyLib model");
2319        assert_eq!(model.exports, vec!["func_a".to_string(), "func_b".to_string()]);
2320    }
2321
2322    #[test]
2323    fn exporter_metadata_resolves_export_and_export_ok() {
2324        let code = r#"
2325package MyUtils;
2326use Exporter 'import';
2327our @EXPORT = qw(foo missing_default);
2328our @EXPORT_OK = ('bar', "missing_ok");
2329sub foo { 1 }
2330sub bar { 1 }
23311;
2332"#;
2333        let models = build_models(code);
2334        let model = find_model(&models, "MyUtils").expect("MyUtils model");
2335        let metadata = model.exporter_metadata.as_ref().expect("exporter metadata");
2336
2337        assert_eq!(
2338            metadata.exports.iter().map(|item| item.name.as_str()).collect::<Vec<_>>(),
2339            vec!["foo"]
2340        );
2341        assert_eq!(
2342            metadata.export_ok.iter().map(|item| item.name.as_str()).collect::<Vec<_>>(),
2343            vec!["bar"]
2344        );
2345        assert!(metadata.unresolved.contains(&"missing_default".to_string()));
2346        assert!(metadata.unresolved.contains(&"missing_ok".to_string()));
2347    }
2348
2349    #[test]
2350    fn exporter_metadata_resolves_export_tags() {
2351        let code = r#"
2352package MyTags;
2353use parent 'Exporter';
2354our %EXPORT_TAGS = (
2355    util => [qw(one two missing)],
2356    misc => ['three'],
2357);
2358sub one { 1 }
2359sub two { 1 }
2360sub three { 1 }
23611;
2362"#;
2363        let models = build_models(code);
2364        let model = find_model(&models, "MyTags").expect("MyTags model");
2365        let metadata = model.exporter_metadata.as_ref().expect("exporter metadata");
2366
2367        let util_names = metadata
2368            .export_tags
2369            .get("util")
2370            .expect("util tag")
2371            .iter()
2372            .map(|item| item.name.as_str())
2373            .collect::<Vec<_>>();
2374        let misc_names = metadata
2375            .export_tags
2376            .get("misc")
2377            .expect("misc tag")
2378            .iter()
2379            .map(|item| item.name.as_str())
2380            .collect::<Vec<_>>();
2381
2382        assert_eq!(util_names, vec!["one", "two"]);
2383        assert_eq!(misc_names, vec!["three"]);
2384        assert!(metadata.unresolved.contains(&"missing".to_string()));
2385    }
2386
2387    #[test]
2388    fn export_lists_without_exporter_usage_do_not_produce_exporter_metadata() {
2389        let code = r#"
2390package NoExporter;
2391our @EXPORT = qw(foo);
2392our @EXPORT_OK = qw(bar);
2393our %EXPORT_TAGS = (all => [qw(foo bar)]);
2394sub foo { 1 }
2395sub bar { 1 }
23961;
2397"#;
2398        let models = build_models(code);
2399        let model = find_model(&models, "NoExporter").expect("NoExporter model");
2400        assert!(model.exporter_metadata.is_none());
2401    }
2402
2403    // ---- Gap 2: push @ISA ----
2404
2405    #[test]
2406    fn push_isa_single_parent() {
2407        let code = "package Child;\npush @ISA, 'Parent';\n1;";
2408        let models = build_models(code);
2409        let model = find_model(&models, "Child").expect("Child model");
2410        assert!(model.parents.contains(&"Parent".to_string()), "push @ISA must capture parent");
2411        assert_eq!(model.framework, Framework::PlainOO);
2412    }
2413
2414    #[test]
2415    fn push_isa_multiple_parents() {
2416        let code = "package Child;\npush @ISA, 'Base1', 'Base2';\n1;";
2417        let models = build_models(code);
2418        let model = find_model(&models, "Child").expect("Child model");
2419        assert!(model.parents.contains(&"Base1".to_string()));
2420        assert!(model.parents.contains(&"Base2".to_string()));
2421    }
2422
2423    #[test]
2424    fn push_isa_does_not_downgrade_moose_framework() {
2425        // If a package already uses Moose, push @ISA must not overwrite to PlainOO.
2426        let code = "package Child;\nuse Moose;\nextends 'Base';\npush @ISA, 'Extra';\n1;";
2427        let models = build_models(code);
2428        let model = find_model(&models, "Child").expect("Child model");
2429        assert_eq!(model.framework, Framework::Moose, "Moose must not be downgraded to PlainOO");
2430        assert!(
2431            model.parents.contains(&"Extra".to_string()),
2432            "push @ISA parent must still be captured"
2433        );
2434    }
2435
2436    // ── Issue #3540: Native Perl 5.38 class :isa(Parent) inheritance ──────────
2437
2438    #[test]
2439    fn native_class_with_isa_has_correct_parent() {
2440        let models = build_models(
2441            r#"
2442class Point3D :isa(Point) {
2443    field $z :param = 0;
2444    method get_z { return $z; }
2445}
2446"#,
2447        );
2448        assert_eq!(models.len(), 1, "expected one ClassModel for Point3D");
2449        let model = &models[0];
2450        assert_eq!(model.name, "Point3D");
2451        assert_eq!(model.framework, Framework::NativeClass);
2452        assert!(
2453            model.parents.contains(&"Point".to_string()),
2454            "native class :isa(Point) must populate parents, got {:?}",
2455            model.parents
2456        );
2457    }
2458
2459    #[test]
2460    fn native_class_with_multiple_isa_has_all_parents() {
2461        let models = build_models(
2462            r#"
2463class Shape3D :isa(Shape) :isa(Printable) {
2464    field $z :param = 0;
2465}
2466"#,
2467        );
2468        assert_eq!(models.len(), 1, "expected one ClassModel for Shape3D");
2469        let model = &models[0];
2470        assert_eq!(model.framework, Framework::NativeClass);
2471        assert!(
2472            model.parents.contains(&"Shape".to_string()),
2473            "expected 'Shape' in parents, got {:?}",
2474            model.parents
2475        );
2476        assert!(
2477            model.parents.contains(&"Printable".to_string()),
2478            "expected 'Printable' in parents, got {:?}",
2479            model.parents
2480        );
2481    }
2482
2483    #[test]
2484    fn native_class_without_isa_has_no_parents() {
2485        let models = build_models(
2486            r#"
2487class Point {
2488    field $x :param = 0;
2489    field $y :param = 0;
2490}
2491"#,
2492        );
2493        assert_eq!(models.len(), 1);
2494        let model = &models[0];
2495        assert_eq!(model.framework, Framework::NativeClass);
2496        assert!(
2497            model.parents.is_empty(),
2498            "class without :isa must have no parents, got {:?}",
2499            model.parents
2500        );
2501    }
2502
2503    #[test]
2504    fn native_class_with_qualified_isa_has_qualified_parent() {
2505        let models = build_models(
2506            r#"
2507class MyApp::Point3D :isa(MyApp::Point) {
2508    field $z :param = 0;
2509}
2510"#,
2511        );
2512        assert_eq!(models.len(), 1);
2513        let model = &models[0];
2514        assert_eq!(model.name, "MyApp::Point3D");
2515        assert!(
2516            model.parents.contains(&"MyApp::Point".to_string()),
2517            "qualified :isa must preserve qualified name, got {:?}",
2518            model.parents
2519        );
2520    }
2521
2522    #[test]
2523    fn second_class_without_isa_does_not_inherit_first_class_parents() {
2524        // Regression guard: parents from the first class must not bleed into the second.
2525        // flush_current_package() uses mem::take so current_parents is reset between classes.
2526        let models = build_models(
2527            r#"
2528class Point3D :isa(Point) {
2529    field $z :param = 0;
2530}
2531class Standalone {
2532    field $x :param = 0;
2533}
2534"#,
2535        );
2536        let standalone = models.iter().find(|m| m.name == "Standalone").expect("Standalone model");
2537        assert!(
2538            standalone.parents.is_empty(),
2539            "Standalone class must have no parents, but got {:?}",
2540            standalone.parents
2541        );
2542    }
2543}