Skip to main content

perl_semantic_analyzer/analysis/
symbol.rs

1//! Symbol extraction and symbol table for IDE features
2//!
3//! This module provides symbol extraction from the AST, building a symbol table
4//! that tracks definitions, references, and scopes for IDE features like
5//! go-to-definition, find-all-references, and semantic highlighting.
6//!
7//! # Related Modules
8//!
9//! See also [`crate::workspace_index`] for workspace-wide indexing and
10//! cross-file reference resolution.
11//!
12//! # Usage Examples
13//!
14//! ```no_run
15//! use perl_semantic_analyzer::{Parser, symbol::SymbolExtractor};
16//!
17//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
18//! let mut parser = Parser::new("sub hello { my $x = 1; }");
19//! let ast = parser.parse()?;
20//! let extractor = SymbolExtractor::new();
21//! let table = extractor.extract(&ast);
22//! assert!(table.symbols.contains_key("hello"));
23//! # Ok(())
24//! # }
25//! ```
26
27use crate::SourceLocation;
28use crate::ast::{Node, NodeKind};
29use regex::Regex;
30use std::collections::{HashMap, HashSet};
31use std::sync::OnceLock;
32
33const UNIVERSAL_METHODS: [&str; 4] = ["can", "isa", "DOES", "VERSION"];
34
35// Re-export the unified symbol types from perl-symbol
36/// Symbol kind enums used during Index/Analyze workflows.
37pub use perl_symbol::{SymbolKind, VarKind};
38
39#[derive(Debug, Clone)]
40/// A symbol definition in Perl code with comprehensive metadata for Index/Navigate workflows.
41///
42/// Represents a symbol definition with full context including scope,
43/// package qualification, and documentation for LSP features like
44/// go-to-definition, hover, and workspace symbols.
45///
46/// # Performance Characteristics
47/// - Memory: ~128 bytes per symbol (optimized for large codebases)
48/// - Lookup time: O(1) via hash table indexing
49/// - Scope resolution: O(log n) with scope hierarchy
50///
51/// # Perl Language Semantics
52/// - Package qualification: `Package::symbol` vs bare `symbol`
53/// - Scope rules: Lexical (`my`), package (`our`), dynamic (`local`), persistent (`state`)
54/// - Symbol types: Variables (`$`, `@`, `%`), subroutines, packages, constants
55/// - Attribute parsing: `:shared`, `:method`, `:lvalue` and custom attributes
56pub struct Symbol {
57    /// Symbol name (without sigil for variables)
58    pub name: String,
59    /// Fully qualified name with package prefix
60    pub qualified_name: String,
61    /// Classification of symbol type
62    pub kind: SymbolKind,
63    /// Source location of symbol definition
64    pub location: SourceLocation,
65    /// Lexical scope identifier for visibility rules
66    pub scope_id: ScopeId,
67    /// Variable declaration type (my, our, local, state)
68    pub declaration: Option<String>,
69    /// Extracted POD or comment documentation
70    pub documentation: Option<String>,
71    /// Perl attributes applied to the symbol
72    pub attributes: Vec<String>,
73}
74
75#[derive(Debug, Clone)]
76/// A reference to a symbol with usage context for Navigate/Analyze workflows.
77///
78/// Tracks symbol usage sites for features like find-all-references,
79/// rename refactoring, and unused symbol detection with precise
80/// scope and context information.
81///
82/// # Performance Characteristics
83/// - Memory: ~64 bytes per reference
84/// - Collection: O(n) during AST traversal
85/// - Query time: O(log n) with spatial indexing
86///
87/// # LSP Integration
88/// Essential for:
89/// - Find references: Locate all usage sites
90/// - Rename refactoring: Update all references atomically
91/// - Unused detection: Identify unreferenced symbols
92/// - Call hierarchy: Build caller/callee relationships
93pub struct SymbolReference {
94    /// Symbol name (without sigil for variables)
95    pub name: String,
96    /// Symbol type inferred from usage context
97    pub kind: SymbolKind,
98    /// Source location of the reference
99    pub location: SourceLocation,
100    /// Lexical scope where reference occurs
101    pub scope_id: ScopeId,
102    /// Whether this is a write reference (assignment)
103    pub is_write: bool,
104}
105
106/// Unique identifier for a scope used during Index/Analyze workflows.
107pub type ScopeId = usize;
108
109#[derive(Debug, Clone)]
110/// A lexical scope in Perl code with hierarchical symbol visibility for Parse/Analyze stages.
111///
112/// Represents a lexical scope boundary (subroutine, block, package) with
113/// symbol visibility rules according to Perl's lexical scoping semantics.
114///
115/// # Performance Characteristics
116/// - Scope lookup: O(log n) with parent chain traversal
117/// - Symbol resolution: O(1) per scope level
118/// - Memory: ~64 bytes per scope + symbol set
119///
120/// # Perl Scoping Rules
121/// - Global scope: File-level and package symbols
122/// - Package scope: Package-qualified symbols
123/// - Subroutine scope: Local variables and parameters
124/// - Block scope: Lexical variables in control structures
125/// - Lexical precedence: Inner scopes shadow outer scopes
126///
127/// Workflow: Parse/Analyze scope tracking for symbol resolution.
128pub struct Scope {
129    /// Unique scope identifier for reference tracking
130    pub id: ScopeId,
131    /// Parent scope for hierarchical lookup (None for global)
132    pub parent: Option<ScopeId>,
133    /// Classification of scope type
134    pub kind: ScopeKind,
135    /// Source location where scope begins
136    pub location: SourceLocation,
137    /// Set of symbol names defined in this scope
138    pub symbols: HashSet<String>,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142/// Classification of lexical scope types in Perl for Parse/Analyze workflows.
143///
144/// Defines different scope boundaries with specific symbol visibility
145/// and resolution rules according to Perl language semantics.
146///
147/// # Scope Hierarchy
148/// - Global: File-level symbols and imports
149/// - Package: Package-qualified namespace
150/// - Subroutine: Function parameters and local variables
151/// - Block: Control structure lexical variables
152/// - Eval: Dynamic evaluation context
153///
154/// Workflow: Parse/Analyze scope classification.
155pub enum ScopeKind {
156    /// Global/file scope
157    Global,
158    /// Package scope
159    Package,
160    /// Subroutine scope
161    Subroutine,
162    /// Block scope (if, while, for, etc.)
163    Block,
164    /// Eval scope
165    Eval,
166}
167
168#[derive(Debug, Default)]
169/// Comprehensive symbol table for Perl code analysis and LSP features in Index/Analyze stages.
170///
171/// Central data structure containing all symbols, references, and scopes
172/// with efficient indexing for LSP operations like go-to-definition,
173/// find-references, and workspace symbols.
174///
175/// # Performance Characteristics
176/// - Symbol lookup: O(1) average, O(n) worst case for overloaded names
177/// - Reference queries: O(log n) with spatial indexing
178/// - Memory usage: ~500KB per 10K lines of Perl code
179/// - Construction time: O(n) single-pass AST traversal
180///
181/// # LSP Integration
182/// Core data structure for:
183/// - Symbol resolution: Package-qualified and bare name lookup
184/// - Reference tracking: All usage sites with context
185/// - Scope analysis: Lexical visibility and shadowing
186/// - Completion: Context-aware symbol suggestions
187/// - Workspace indexing: Cross-file symbol registry
188///
189/// # Perl Language Support
190/// - Package qualification: `Package::symbol` resolution
191/// - Lexical scoping: `my`, `our`, `local`, `state` variable semantics
192/// - Symbol overloading: Multiple definitions with scope precedence
193/// - Context sensitivity: Scalar/array/hash context resolution
194pub struct SymbolTable {
195    /// Symbols indexed by name with multiple definitions support
196    pub symbols: HashMap<String, Vec<Symbol>>,
197    /// References indexed by name for find-all-references
198    pub references: HashMap<String, Vec<SymbolReference>>,
199    /// Scopes indexed by ID for hierarchical lookup
200    pub scopes: HashMap<ScopeId, Scope>,
201    /// Scope stack maintained during AST traversal
202    scope_stack: Vec<ScopeId>,
203    /// Monotonic scope ID generator
204    next_scope_id: ScopeId,
205    /// Current package context for symbol qualification
206    current_package: String,
207}
208
209/// Return `true` if the method is one of Perl's always-available `UNIVERSAL` methods.
210///
211/// Used in analyze/index workflow stages to keep method lookup behavior
212/// consistent across parser and LSP navigation flows.
213pub fn is_universal_method(method_name: &str) -> bool {
214    UNIVERSAL_METHODS.contains(&method_name)
215}
216
217impl SymbolTable {
218    /// Create a new symbol table for Index/Analyze workflows.
219    pub fn new() -> Self {
220        let mut table = SymbolTable {
221            symbols: HashMap::new(),
222            references: HashMap::new(),
223            scopes: HashMap::new(),
224            scope_stack: vec![0],
225            next_scope_id: 1,
226            current_package: "main".to_string(),
227        };
228
229        // Create global scope
230        table.scopes.insert(
231            0,
232            Scope {
233                id: 0,
234                parent: None,
235                kind: ScopeKind::Global,
236                location: SourceLocation { start: 0, end: 0 },
237                symbols: HashSet::new(),
238            },
239        );
240
241        table
242    }
243
244    /// Get the current scope ID
245    fn current_scope(&self) -> ScopeId {
246        *self.scope_stack.last().unwrap_or(&0)
247    }
248
249    /// Push a new scope
250    fn push_scope(&mut self, kind: ScopeKind, location: SourceLocation) -> ScopeId {
251        let parent = self.current_scope();
252        let scope_id = self.next_scope_id;
253        self.next_scope_id += 1;
254
255        let scope =
256            Scope { id: scope_id, parent: Some(parent), kind, location, symbols: HashSet::new() };
257
258        self.scopes.insert(scope_id, scope);
259        self.scope_stack.push(scope_id);
260        scope_id
261    }
262
263    /// Pop the current scope
264    fn pop_scope(&mut self) {
265        self.scope_stack.pop();
266    }
267
268    /// Add a symbol definition
269    fn add_symbol(&mut self, symbol: Symbol) {
270        if symbol.name.is_empty() {
271            return;
272        }
273        let name = symbol.name.clone();
274        if let Some(scope) = self.scopes.get_mut(&symbol.scope_id) {
275            scope.symbols.insert(name.clone());
276        }
277        self.symbols.entry(name).or_default().push(symbol);
278    }
279
280    /// Add a symbol reference
281    fn add_reference(&mut self, reference: SymbolReference) {
282        if reference.name.is_empty() {
283            return;
284        }
285        let name = reference.name.clone();
286        self.references.entry(name).or_default().push(reference);
287    }
288
289    /// Find symbol definitions visible from a given scope for Navigate/Analyze workflows.
290    pub fn find_symbol(&self, name: &str, from_scope: ScopeId, kind: SymbolKind) -> Vec<&Symbol> {
291        let mut results = Vec::new();
292        let mut current_scope_id = Some(from_scope);
293
294        // Walk up the scope chain
295        while let Some(scope_id) = current_scope_id {
296            if let Some(scope) = self.scopes.get(&scope_id) {
297                // Check if symbol is defined in this scope
298                if scope.symbols.contains(name) {
299                    if let Some(symbols) = self.symbols.get(name) {
300                        for symbol in symbols {
301                            if symbol.scope_id == scope_id && symbol.kind == kind {
302                                results.push(symbol);
303                            }
304                        }
305                    }
306                }
307
308                // For 'our' variables, also check package scope
309                if scope.kind != ScopeKind::Package {
310                    if let Some(symbols) = self.symbols.get(name) {
311                        for symbol in symbols {
312                            if symbol.declaration.as_deref() == Some("our") && symbol.kind == kind {
313                                results.push(symbol);
314                            }
315                        }
316                    }
317                }
318
319                current_scope_id = scope.parent;
320            } else {
321                break;
322            }
323        }
324
325        results
326    }
327
328    /// Get all references to a symbol for Navigate/Analyze workflows.
329    pub fn find_references(&self, symbol: &Symbol) -> Vec<&SymbolReference> {
330        self.references
331            .get(&symbol.name)
332            .map(|refs| refs.iter().filter(|r| r.kind == symbol.kind).collect())
333            .unwrap_or_default()
334    }
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
338/// Classification of Moo/Moose framework variant detected via `use` statements during Parse/Analyze workflows.
339pub enum FrameworkKind {
340    /// `use Moo;`
341    Moo,
342    /// `use Moo::Role;`
343    MooRole,
344    /// `use Moose;`
345    Moose,
346    /// `use Moose::Role;`
347    MooseRole,
348    /// `use Role::Tiny;` — the package is a role
349    RoleTiny,
350    /// `use Role::Tiny::With;` — the package consumes roles
351    RoleTinyWith,
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq)]
355/// Web framework variant detected via `use` statements during Parse/Analyze workflows.
356pub enum WebFrameworkKind {
357    /// `use Dancer;`
358    Dancer,
359    /// `use Dancer2;` or `use Dancer2::Core;`
360    Dancer2,
361    /// `use Mojolicious::Lite;`
362    MojoliciousLite,
363    /// `use Plack::Builder;`
364    PlackBuilder,
365}
366
367#[derive(Debug, Clone, Copy, PartialEq, Eq)]
368/// Async framework variant detected via `use` statements during Parse/Analyze workflows.
369pub enum AsyncFrameworkKind {
370    /// `use AnyEvent;`
371    AnyEvent,
372    /// `use EV;`
373    EV,
374    /// `use Future;`
375    Future,
376    /// `use Future::XS;`
377    FutureXS,
378    /// `use Promise;`
379    Promise,
380    /// `use Promise::XS;`
381    PromiseXS,
382    /// `use POE;`
383    POE,
384    /// `use IO::Async;`
385    IOAsync,
386    /// `use Mojo::Redis;`
387    MojoRedis,
388    /// `use Mojo::Pg;`
389    MojoPg,
390}
391
392#[derive(Debug, Clone, Default)]
393/// Per-package framework detection flags used in Parse/Analyze workflows.
394pub struct FrameworkFlags {
395    /// Moo/Moose framework variant, if any.
396    pub moo: bool,
397    /// Class::Accessor style generated accessors.
398    pub class_accessor: bool,
399    /// Which specific Moo/Moose variant was detected.
400    pub kind: Option<FrameworkKind>,
401    /// Web framework variant, if any (Dancer, Dancer2, Mojolicious::Lite).
402    pub web_framework: Option<WebFrameworkKind>,
403    /// Async framework variant, if any (IO::Async).
404    pub async_framework: Option<AsyncFrameworkKind>,
405    /// Catalyst controller/package marker used for action synthesis.
406    pub catalyst_controller: bool,
407}
408
409/// Extract symbols from an AST for Parse/Index workflows.
410pub struct SymbolExtractor {
411    table: SymbolTable,
412    /// Source code for comment extraction
413    source: String,
414    /// Per-package framework detection flags, keyed by package name.
415    framework_flags: HashMap<String, FrameworkFlags>,
416    /// Whether `use Const::Fast` has been seen in the current compilation unit.
417    const_fast_enabled: bool,
418    /// Whether `use Readonly` has been seen in the current compilation unit.
419    readonly_enabled: bool,
420}
421
422impl Default for SymbolExtractor {
423    fn default() -> Self {
424        Self::new()
425    }
426}
427
428impl SymbolExtractor {
429    /// Create a new symbol extractor without source (no documentation extraction).
430    ///
431    /// Used during Parse/Index stages when only symbols are required.
432    pub fn new() -> Self {
433        SymbolExtractor {
434            table: SymbolTable::new(),
435            source: String::new(),
436            framework_flags: HashMap::new(),
437            const_fast_enabled: false,
438            readonly_enabled: false,
439        }
440    }
441
442    /// Create a symbol extractor with source text for documentation extraction.
443    ///
444    /// Used during Parse/Analyze stages to attach documentation metadata.
445    pub fn new_with_source(source: &str) -> Self {
446        SymbolExtractor {
447            table: SymbolTable::new(),
448            source: source.to_string(),
449            framework_flags: HashMap::new(),
450            const_fast_enabled: false,
451            readonly_enabled: false,
452        }
453    }
454
455    /// Extract symbols from an AST node for Index/Analyze workflows.
456    pub fn extract(mut self, node: &Node) -> SymbolTable {
457        self.visit_node(node);
458        self.upgrade_package_symbols_from_framework_flags();
459        self.table
460    }
461
462    /// Post-processing: upgrade `SymbolKind::Package` to `Class` or `Role`
463    /// based on the framework flags discovered during traversal.
464    fn upgrade_package_symbols_from_framework_flags(&mut self) {
465        for (pkg_name, flags) in &self.framework_flags {
466            let Some(kind) = flags.kind else {
467                continue;
468            };
469            let new_kind = match kind {
470                FrameworkKind::Moo | FrameworkKind::Moose | FrameworkKind::RoleTinyWith => {
471                    SymbolKind::Class
472                }
473                FrameworkKind::MooRole | FrameworkKind::MooseRole | FrameworkKind::RoleTiny => {
474                    SymbolKind::Role
475                }
476            };
477            if let Some(symbols) = self.table.symbols.get_mut(pkg_name) {
478                for symbol in symbols.iter_mut() {
479                    if symbol.kind == SymbolKind::Package {
480                        symbol.kind = new_kind;
481                    }
482                }
483            }
484        }
485    }
486
487    /// Visit a node and extract symbols
488    fn visit_node(&mut self, node: &Node) {
489        match &node.kind {
490            NodeKind::Program { statements } => {
491                self.visit_statement_list(statements);
492            }
493
494            NodeKind::VariableDeclaration { declarator, variable, attributes, initializer } => {
495                let doc = self.extract_leading_comment(node.location.start);
496                self.handle_variable_declaration(
497                    declarator,
498                    variable,
499                    attributes,
500                    variable.location,
501                    doc,
502                );
503                if let Some(init) = initializer {
504                    self.visit_node(init);
505                }
506            }
507
508            NodeKind::VariableListDeclaration {
509                declarator,
510                variables,
511                attributes,
512                initializer,
513            } => {
514                let doc = self.extract_leading_comment(node.location.start);
515                for var in variables {
516                    self.handle_variable_declaration(
517                        declarator,
518                        var,
519                        attributes,
520                        var.location,
521                        doc.clone(),
522                    );
523                }
524                if let Some(init) = initializer {
525                    self.visit_node(init);
526                }
527            }
528
529            NodeKind::Variable { sigil, name } => {
530                let kind = match sigil.as_str() {
531                    "$" => SymbolKind::scalar(),
532                    "@" => SymbolKind::array(),
533                    "%" => SymbolKind::hash(),
534                    _ => return,
535                };
536
537                let reference = SymbolReference {
538                    name: name.clone(),
539                    kind,
540                    location: node.location,
541                    scope_id: self.table.current_scope(),
542                    is_write: false, // Will be updated based on context
543                };
544
545                self.table.add_reference(reference);
546            }
547
548            NodeKind::Subroutine {
549                name,
550                prototype: _,
551                signature,
552                attributes,
553                body,
554                name_span: _,
555            } => {
556                let sub_name =
557                    name.as_ref().map(|n| n.to_string()).unwrap_or_else(|| "<anon>".to_string());
558
559                if name.is_some() {
560                    let documentation = self.extract_leading_comment(node.location.start);
561                    let mut symbol_attributes = attributes.clone();
562                    let documentation = if self.current_package_is_catalyst_controller()
563                        && let Some((action_kind, action_details)) =
564                            Self::catalyst_action_metadata(attributes)
565                    {
566                        symbol_attributes.push("framework=Catalyst".to_string());
567                        symbol_attributes.push("catalyst_controller=true".to_string());
568                        symbol_attributes.push("catalyst_action=true".to_string());
569                        symbol_attributes.push(format!("catalyst_action_kind={action_kind}"));
570                        if !action_details.is_empty() {
571                            symbol_attributes.push(format!(
572                                "catalyst_action_attributes={}",
573                                action_details.join(", ")
574                            ));
575                        }
576
577                        let action_doc = if action_details.is_empty() {
578                            format!("Catalyst action ({action_kind})")
579                        } else {
580                            format!(
581                                "Catalyst action ({action_kind}; {})",
582                                action_details.join(", ")
583                            )
584                        };
585                        match documentation {
586                            Some(doc) => Some(format!("{doc}\n{action_doc}")),
587                            None => Some(action_doc),
588                        }
589                    } else {
590                        documentation
591                    };
592                    let symbol = Symbol {
593                        name: sub_name.clone(),
594                        qualified_name: format!("{}::{}", self.table.current_package, sub_name),
595                        kind: SymbolKind::Subroutine,
596                        location: node.location,
597                        scope_id: self.table.current_scope(),
598                        declaration: None,
599                        documentation,
600                        attributes: symbol_attributes,
601                    };
602
603                    self.table.add_symbol(symbol);
604                }
605
606                // Create subroutine scope
607                self.table.push_scope(ScopeKind::Subroutine, node.location);
608
609                // Register signature parameters as implicit `my` declarations
610                if let Some(sig) = signature {
611                    self.register_signature_params(sig);
612                }
613
614                self.visit_node(body);
615
616                self.table.pop_scope();
617            }
618
619            NodeKind::Package { name, block, name_span: _ } => {
620                let old_package = self.table.current_package.clone();
621                self.table.current_package = name.clone();
622                if Self::is_catalyst_controller_package_name(name) {
623                    self.mark_catalyst_controller_package(name);
624                }
625
626                let documentation = self.extract_package_documentation(name, node.location);
627                let symbol = Symbol {
628                    name: name.clone(),
629                    qualified_name: name.clone(),
630                    kind: SymbolKind::Package,
631                    location: node.location,
632                    scope_id: self.table.current_scope(),
633                    declaration: None,
634                    documentation,
635                    attributes: vec![],
636                };
637
638                self.table.add_symbol(symbol);
639
640                if let Some(block_node) = block {
641                    // Package with block - create a new scope
642                    self.table.push_scope(ScopeKind::Package, node.location);
643                    self.visit_node(block_node);
644                    self.table.pop_scope();
645                    self.table.current_package = old_package;
646                }
647                // If no block, package declaration affects rest of file
648                // Don't change scope or restore package name
649            }
650
651            NodeKind::Block { statements } => {
652                self.table.push_scope(ScopeKind::Block, node.location);
653                self.visit_statement_list(statements);
654                self.table.pop_scope();
655            }
656
657            NodeKind::If { condition, then_branch, elsif_branches: _, else_branch } => {
658                self.visit_node(condition);
659
660                self.table.push_scope(ScopeKind::Block, then_branch.location);
661                self.visit_node(then_branch);
662                self.table.pop_scope();
663
664                if let Some(else_node) = else_branch {
665                    self.table.push_scope(ScopeKind::Block, else_node.location);
666                    self.visit_node(else_node);
667                    self.table.pop_scope();
668                }
669            }
670
671            NodeKind::While { condition, body, continue_block: _ } => {
672                self.visit_node(condition);
673
674                self.table.push_scope(ScopeKind::Block, body.location);
675                self.visit_node(body);
676                self.table.pop_scope();
677            }
678
679            NodeKind::For { init, condition, update, body, .. } => {
680                self.table.push_scope(ScopeKind::Block, node.location);
681
682                if let Some(init_node) = init {
683                    self.visit_node(init_node);
684                }
685                if let Some(cond_node) = condition {
686                    self.visit_node(cond_node);
687                }
688                if let Some(update_node) = update {
689                    self.visit_node(update_node);
690                }
691                self.visit_node(body);
692
693                self.table.pop_scope();
694            }
695
696            NodeKind::Foreach { variable, list, body, continue_block: _ } => {
697                self.table.push_scope(ScopeKind::Block, node.location);
698
699                // The loop variable is implicitly declared
700                self.handle_variable_declaration("my", variable, &[], variable.location, None);
701                self.visit_node(list);
702                self.visit_node(body);
703
704                self.table.pop_scope();
705            }
706
707            // Handle other node types by visiting children
708            NodeKind::Assignment { lhs, rhs, .. } => {
709                // Mark LHS as write reference
710                self.mark_write_reference(lhs);
711                self.visit_node(lhs);
712                self.visit_node(rhs);
713            }
714
715            NodeKind::Binary { left, right, .. } => {
716                self.visit_node(left);
717                self.visit_node(right);
718            }
719
720            NodeKind::Unary { operand, .. } => {
721                self.visit_node(operand);
722            }
723
724            NodeKind::FunctionCall { name, args } => {
725                if self.const_fast_enabled
726                    && name == "const"
727                    && self.try_extract_const_fast_declaration(args)
728                {
729                    return;
730                }
731                if self.readonly_enabled
732                    && name == "Readonly"
733                    && self.try_extract_readonly_declaration(args)
734                {
735                    return;
736                }
737
738                // Track function call as a reference
739                let reference = SymbolReference {
740                    name: name.clone(),
741                    kind: SymbolKind::Subroutine,
742                    location: node.location,
743                    scope_id: self.table.current_scope(),
744                    is_write: false,
745                };
746                self.table.add_reference(reference);
747
748                self.synthesize_plack_builder_symbols(name, args);
749                self.synthesize_ev_symbols(name, node.location);
750
751                for arg in args {
752                    self.visit_node(arg);
753                }
754            }
755
756            NodeKind::MethodCall { object, method, args } => {
757                // Track method call sites so semantic definition/hover can resolve generated
758                // accessors (Moo/Moose/Class::Accessor) from usage points.
759                let location = self.method_reference_location(node, object, method);
760                self.table.add_reference(SymbolReference {
761                    name: method.clone(),
762                    kind: SymbolKind::Subroutine,
763                    location,
764                    scope_id: self.table.current_scope(),
765                    is_write: false,
766                });
767
768                self.synthesize_async_framework_class_symbol(object);
769                self.synthesize_future_api_symbols(object, method, node.location);
770                self.visit_node(object);
771                for arg in args {
772                    self.visit_node(arg);
773                }
774            }
775
776            // ArrayRef and HashRef are handled as Binary operations with [] or {}
777            NodeKind::ArrayLiteral { elements } => {
778                for elem in elements {
779                    self.visit_node(elem);
780                }
781            }
782
783            NodeKind::HashLiteral { pairs } => {
784                for (key, value) in pairs {
785                    self.visit_node(key);
786                    self.visit_node(value);
787                }
788            }
789
790            NodeKind::Ternary { condition, then_expr, else_expr } => {
791                self.visit_node(condition);
792                self.visit_node(then_expr);
793                self.visit_node(else_expr);
794            }
795
796            NodeKind::LabeledStatement { label, statement } => {
797                let symbol = Symbol {
798                    name: label.clone(),
799                    qualified_name: label.clone(),
800                    kind: SymbolKind::Label,
801                    location: node.location,
802                    scope_id: self.table.current_scope(),
803                    declaration: None,
804                    documentation: None,
805                    attributes: vec![],
806                };
807
808                self.table.add_symbol(symbol);
809
810                {
811                    self.visit_node(statement);
812                }
813            }
814
815            // Handle interpolated strings specially to extract variable references
816            NodeKind::String { value, interpolated } => {
817                if *interpolated {
818                    // Extract variable references from interpolated strings
819                    self.extract_vars_from_string(value, node.location);
820                }
821            }
822
823            NodeKind::Use { module, args, .. } => {
824                self.update_framework_context(module, args);
825                if module == "Const::Fast" {
826                    self.const_fast_enabled = true;
827                }
828                if module == "Readonly" {
829                    self.readonly_enabled = true;
830                }
831                if module == "EV" {
832                    self.synthesize_ev_framework_symbol(node.location);
833                }
834                if module == "constant" {
835                    self.synthesize_use_constant_symbols(args, node.location);
836                }
837            }
838
839            NodeKind::No { module: _, args: _, .. } => {
840                // We don't currently track framework deactivation via `no`.
841            }
842
843            NodeKind::PhaseBlock { phase, phase_span: _, block } => {
844                // BEGIN, END, CHECK, INIT, UNITCHECK blocks — expose as named symbols
845                // so they appear in document outline / Outline View (#3464).
846                let symbol = Symbol {
847                    name: phase.clone(),
848                    qualified_name: format!("{}::{}", self.table.current_package, phase),
849                    kind: SymbolKind::Subroutine,
850                    location: node.location,
851                    scope_id: self.table.current_scope(),
852                    declaration: None,
853                    documentation: None,
854                    attributes: vec![],
855                };
856                self.table.add_symbol(symbol);
857
858                self.table.push_scope(ScopeKind::Block, node.location);
859                self.visit_node(block);
860                self.table.pop_scope();
861            }
862
863            NodeKind::StatementModifier { statement, modifier: _, condition } => {
864                self.visit_node(statement);
865                self.visit_node(condition);
866            }
867
868            NodeKind::Do { block } | NodeKind::Eval { block } | NodeKind::Defer { block } => {
869                self.visit_node(block);
870            }
871
872            NodeKind::Try { body, catch_blocks, finally_block } => {
873                self.visit_node(body);
874                for (catch_var, catch_block) in catch_blocks {
875                    self.table.push_scope(ScopeKind::Block, catch_block.location);
876                    if let Some(full_name) = catch_var.as_deref() {
877                        self.register_catch_variable(full_name, catch_block.location);
878                    }
879                    self.visit_node(catch_block);
880                    self.table.pop_scope();
881                }
882                if let Some(finally) = finally_block {
883                    self.visit_node(finally);
884                }
885            }
886
887            NodeKind::Given { expr, body } => {
888                self.visit_node(expr);
889                self.visit_node(body);
890            }
891
892            NodeKind::When { condition, body } => {
893                self.visit_node(condition);
894                self.visit_node(body);
895            }
896
897            NodeKind::Default { body } => {
898                self.visit_node(body);
899            }
900
901            NodeKind::Class { name, parents, body } => {
902                let documentation = self.extract_leading_comment(node.location.start);
903                if Self::is_catalyst_controller_package_name(name)
904                    || parents.iter().any(|parent| parent == "Catalyst::Controller")
905                {
906                    self.mark_catalyst_controller_package(name);
907                }
908                let symbol = Symbol {
909                    name: name.clone(),
910                    qualified_name: name.clone(),
911                    kind: SymbolKind::Package, // Classes are like packages
912                    location: node.location,
913                    scope_id: self.table.current_scope(),
914                    declaration: None,
915                    documentation,
916                    attributes: vec![],
917                };
918                self.table.add_symbol(symbol);
919
920                self.table.push_scope(ScopeKind::Package, node.location);
921                self.visit_node(body);
922                self.table.pop_scope();
923            }
924
925            NodeKind::Method { name, signature, attributes, body } => {
926                let documentation = self.extract_leading_comment(node.location.start);
927                let mut symbol_attributes = Vec::with_capacity(attributes.len() + 1);
928                symbol_attributes.push("method".to_string());
929                symbol_attributes.extend(attributes.iter().cloned());
930                let symbol = Symbol {
931                    name: name.clone(),
932                    qualified_name: format!("{}::{}", self.table.current_package, name),
933                    kind: SymbolKind::Method,
934                    location: node.location,
935                    scope_id: self.table.current_scope(),
936                    declaration: None,
937                    documentation,
938                    attributes: symbol_attributes,
939                };
940                self.table.add_symbol(symbol);
941
942                self.table.push_scope(ScopeKind::Subroutine, node.location);
943
944                // Register signature parameters as implicit `my` declarations
945                if let Some(sig) = signature {
946                    self.register_signature_params(sig);
947                }
948
949                self.visit_node(body);
950                self.table.pop_scope();
951            }
952
953            NodeKind::Format { name, body: _ } => {
954                let symbol = Symbol {
955                    name: name.clone(),
956                    qualified_name: format!("{}::{}", self.table.current_package, name),
957                    kind: SymbolKind::Format,
958                    location: node.location,
959                    scope_id: self.table.current_scope(),
960                    declaration: None,
961                    documentation: None,
962                    attributes: vec![],
963                };
964                self.table.add_symbol(symbol);
965            }
966
967            NodeKind::Return { value } => {
968                if let Some(val) = value {
969                    self.visit_node(val);
970                }
971            }
972
973            NodeKind::Tie { variable, package, args } => {
974                self.visit_node(variable);
975                self.visit_node(package);
976                for arg in args {
977                    self.visit_node(arg);
978                }
979            }
980
981            NodeKind::Untie { variable } => {
982                self.visit_node(variable);
983            }
984
985            NodeKind::Goto { target } => match &target.kind {
986                NodeKind::Identifier { name } => {
987                    self.table.add_reference(SymbolReference {
988                        name: name.clone(),
989                        kind: SymbolKind::Label,
990                        location: target.location,
991                        scope_id: self.table.current_scope(),
992                        is_write: false,
993                    });
994                }
995                NodeKind::Variable { sigil, name } if sigil == "&" => {
996                    self.table.add_reference(SymbolReference {
997                        name: name.clone(),
998                        kind: SymbolKind::Subroutine,
999                        location: target.location,
1000                        scope_id: self.table.current_scope(),
1001                        is_write: false,
1002                    });
1003                }
1004                _ => self.visit_node(target),
1005            },
1006
1007            // Regex related nodes - we recurse into expression
1008            NodeKind::Regex { .. } => {}
1009            NodeKind::Match { expr, .. } => {
1010                self.visit_node(expr);
1011            }
1012            NodeKind::Substitution { expr, .. } => {
1013                self.visit_node(expr);
1014            }
1015            NodeKind::Transliteration { expr, .. } => {
1016                self.visit_node(expr);
1017            }
1018
1019            NodeKind::IndirectCall { method, object, args } => {
1020                self.table.add_reference(SymbolReference {
1021                    name: method.clone(),
1022                    kind: SymbolKind::Subroutine,
1023                    location: node.location,
1024                    scope_id: self.table.current_scope(),
1025                    is_write: false,
1026                });
1027
1028                self.visit_node(object);
1029                for arg in args {
1030                    self.visit_node(arg);
1031                }
1032            }
1033
1034            NodeKind::ExpressionStatement { expression } => {
1035                // Visit the inner expression to extract symbols
1036                self.visit_node(expression);
1037            }
1038
1039            // Leaf nodes - no children to visit
1040            NodeKind::Number { .. }
1041            | NodeKind::Heredoc { .. }
1042            | NodeKind::Undef
1043            | NodeKind::Diamond
1044            | NodeKind::Ellipsis
1045            | NodeKind::Glob { .. }
1046            | NodeKind::Readline { .. }
1047            | NodeKind::Identifier { .. }
1048            | NodeKind::Typeglob { .. }
1049            | NodeKind::DataSection { .. }
1050            | NodeKind::LoopControl { .. }
1051            | NodeKind::MissingExpression
1052            | NodeKind::MissingStatement
1053            | NodeKind::MissingIdentifier
1054            | NodeKind::MissingBlock
1055            | NodeKind::UnknownRest => {
1056                // No symbols to extract
1057            }
1058
1059            NodeKind::Error { partial, .. } => {
1060                // Descend into the partial sub-tree if present. The parser stores
1061                // the partially-parsed node inside Error when it managed to build
1062                // some structure before failing (e.g. a variable expression whose
1063                // postfix chain was truncated). Visiting it keeps symbol.rs in
1064                // parity with every other traversal in the codebase (semantic
1065                // tokens, class model, scope analyzer via children()) that already
1066                // descends into partial.
1067                if let Some(partial_node) = partial {
1068                    self.visit_node(partial_node);
1069                }
1070            }
1071
1072            _ => {
1073                // For any unhandled node types, log a warning
1074                tracing::warn!(kind = ?node.kind, "Unhandled node type in symbol extractor");
1075            }
1076        }
1077    }
1078
1079    /// Visit a statement list with framework-aware declaration synthesis.
1080    ///
1081    /// This handles idiomatic Perl framework declarations that are not represented
1082    /// as native declaration nodes in the AST (for example Moo `has` and
1083    /// Class::Accessor `mk_accessors` patterns).
1084    fn visit_statement_list(&mut self, statements: &[Node]) {
1085        let mut idx = 0;
1086        while idx < statements.len() {
1087            if let Some(consumed) = self.try_extract_framework_declarations(statements, idx) {
1088                idx += consumed;
1089                continue;
1090            }
1091
1092            self.visit_node(&statements[idx]);
1093            idx += 1;
1094        }
1095    }
1096
1097    /// Detect and synthesize framework declarations from statement patterns.
1098    ///
1099    /// Returns the number of statements consumed when a pattern is handled.
1100    fn try_extract_framework_declarations(
1101        &mut self,
1102        statements: &[Node],
1103        idx: usize,
1104    ) -> Option<usize> {
1105        let flags = self.framework_flags.get(&self.table.current_package).cloned();
1106        let flags = flags.as_ref();
1107
1108        let is_moo = flags.is_some_and(|f| f.moo);
1109
1110        if is_moo {
1111            if let Some(consumed) = self.try_extract_moo_has_declaration(statements, idx) {
1112                return Some(consumed);
1113            }
1114            if let Some(consumed) = self.try_extract_method_modifier(statements, idx) {
1115                return Some(consumed);
1116            }
1117            if let Some(consumed) = self.try_extract_extends_with(statements, idx) {
1118                return Some(consumed);
1119            }
1120            if let Some(consumed) = self.try_extract_role_requires(statements, idx) {
1121                return Some(consumed);
1122            }
1123        }
1124
1125        if flags.is_some_and(|f| f.class_accessor)
1126            && self.try_extract_class_accessor_declaration(&statements[idx])
1127        {
1128            // Keep regular traversal for argument expressions (for example defaults).
1129            self.visit_node(&statements[idx]);
1130            return Some(1);
1131        }
1132
1133        if flags.is_some_and(|f| f.web_framework.is_some()) {
1134            if let Some(consumed) = self.try_extract_web_route_declaration(statements, idx) {
1135                return Some(consumed);
1136            }
1137        }
1138
1139        None
1140    }
1141
1142    /// Extract Moo/Moose `has` declarations represented as:
1143    /// 1. `ExpressionStatement(Identifier("has"))`
1144    /// 2. `ExpressionStatement(HashLiteral(...))`
1145    fn try_extract_moo_has_declaration(
1146        &mut self,
1147        statements: &[Node],
1148        idx: usize,
1149    ) -> Option<usize> {
1150        let first = &statements[idx];
1151
1152        // Form A:
1153        // 1) ExpressionStatement(Identifier("has"))
1154        // 2) ExpressionStatement(HashLiteral(...))
1155        // OR
1156        // 1) ExpressionStatement(Identifier("has"))
1157        // 2) ExpressionStatement(ArrayLiteral([..., HashLiteral]))
1158        if idx + 1 < statements.len() {
1159            let second = &statements[idx + 1];
1160            let is_has_marker = matches!(
1161                &first.kind,
1162                NodeKind::ExpressionStatement { expression }
1163                    if matches!(&expression.kind, NodeKind::Identifier { name } if name == "has")
1164            );
1165
1166            if is_has_marker {
1167                if let NodeKind::ExpressionStatement { expression } = &second.kind {
1168                    let has_location =
1169                        SourceLocation { start: first.location.start, end: second.location.end };
1170
1171                    match &expression.kind {
1172                        NodeKind::HashLiteral { pairs } => {
1173                            self.synthesize_moo_has_pairs(pairs, has_location, false);
1174                            self.visit_node(second);
1175                            return Some(2);
1176                        }
1177                        NodeKind::ArrayLiteral { elements } => {
1178                            if let Some(Node { kind: NodeKind::HashLiteral { pairs }, .. }) =
1179                                elements.last()
1180                            {
1181                                // Extract the names from the preceding elements
1182                                let mut names = Vec::new();
1183                                for el in elements.iter().take(elements.len() - 1) {
1184                                    names.extend(Self::collect_symbol_names(el));
1185                                }
1186                                if !names.is_empty() {
1187                                    self.synthesize_moo_has_attrs_with_options(
1188                                        &names,
1189                                        pairs,
1190                                        has_location,
1191                                    );
1192                                    self.visit_node(second);
1193                                    return Some(2);
1194                                }
1195                            }
1196                        }
1197                        _ => {}
1198                    }
1199                }
1200            }
1201        }
1202
1203        // Form B:
1204        // ExpressionStatement(HashLiteral((Binary("[]", Identifier("has"), attr_expr), options)))
1205        if let NodeKind::ExpressionStatement { expression } = &first.kind
1206            && let NodeKind::HashLiteral { pairs } = &expression.kind
1207        {
1208            let has_embedded_marker = pairs.iter().any(|(key_node, _)| {
1209                matches!(
1210                    &key_node.kind,
1211                    NodeKind::Binary { op, left, .. }
1212                        if op == "[]" && matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
1213                )
1214            });
1215
1216            if has_embedded_marker {
1217                self.synthesize_moo_has_pairs(pairs, first.location, true);
1218                self.visit_node(first);
1219                return Some(1);
1220            }
1221        }
1222
1223        // Form C: FunctionCall { name: "has", args: [name_expr, HashLiteral { ... }] }
1224        // Produced when the parser recognises `has 'name' => (is => 'ro', ...)` as a bare call.
1225        if let NodeKind::ExpressionStatement { expression } = &first.kind
1226            && let NodeKind::FunctionCall { name, args } = &expression.kind
1227            && name == "has"
1228            && !args.is_empty()
1229        {
1230            let options_hash_idx =
1231                args.iter().rposition(|a| matches!(a.kind, NodeKind::HashLiteral { .. }));
1232            if let Some(opts_idx) = options_hash_idx {
1233                if let NodeKind::HashLiteral { pairs } = &args[opts_idx].kind {
1234                    let names: Vec<String> =
1235                        args[..opts_idx].iter().flat_map(Self::collect_symbol_names).collect();
1236                    if !names.is_empty() {
1237                        self.synthesize_moo_has_attrs_with_options(&names, pairs, first.location);
1238                        self.visit_node(first);
1239                        return Some(1);
1240                    }
1241                }
1242            }
1243        }
1244
1245        None
1246    }
1247
1248    /// Detect Moo/Moose method modifiers (`before`, `after`, `around`, `override`, `augment`).
1249    ///
1250    /// Pattern (two statements):
1251    /// 1. `ExpressionStatement(Identifier("around"))` (or `before`/`after`/`override`/`augment`)
1252    /// 2. `ExpressionStatement(HashLiteral([ (method_name, Subroutine{...}) ]))`
1253    ///
1254    /// Also handles FunctionCall form: `around 'name' => sub { }` (post parser fix).
1255    fn try_extract_method_modifier(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
1256        let first = &statements[idx];
1257
1258        // FunctionCall form: `around 'name' => sub { }` parsed as a bare call.
1259        if let NodeKind::ExpressionStatement { expression } = &first.kind
1260            && let NodeKind::FunctionCall { name, args } = &expression.kind
1261            && Self::is_moose_method_modifier(name)
1262        {
1263            let modifier_name = name.as_str();
1264            let method_names: Vec<String> =
1265                args.iter().flat_map(Self::collect_symbol_names).collect();
1266            if !method_names.is_empty() {
1267                let scope_id = self.table.current_scope();
1268                let package = self.table.current_package.clone();
1269                for method_name in method_names {
1270                    self.table.add_symbol(Symbol {
1271                        name: method_name.clone(),
1272                        qualified_name: format!("{package}::{method_name}"),
1273                        kind: SymbolKind::Subroutine,
1274                        location: first.location,
1275                        scope_id,
1276                        declaration: Some(modifier_name.to_string()),
1277                        documentation: Some(format!(
1278                            "Method modifier `{modifier_name}` for `{method_name}`"
1279                        )),
1280                        attributes: vec![format!("modifier={modifier_name}")],
1281                    });
1282                }
1283                return Some(1);
1284            }
1285        }
1286
1287        if idx + 1 >= statements.len() {
1288            return None;
1289        }
1290
1291        let second = &statements[idx + 1];
1292
1293        // Check: first is ExpressionStatement(Identifier("before"|"after"|"around"|"override"|"augment"))
1294        let modifier_name = match &first.kind {
1295            NodeKind::ExpressionStatement { expression } => match &expression.kind {
1296                NodeKind::Identifier { name } if Self::is_moose_method_modifier(name) => {
1297                    name.as_str()
1298                }
1299                _ => return None,
1300            },
1301            _ => return None,
1302        };
1303
1304        // Check: second is ExpressionStatement(HashLiteral(...)) with method names
1305        let NodeKind::ExpressionStatement { expression } = &second.kind else {
1306            return None;
1307        };
1308        let NodeKind::HashLiteral { pairs } = &expression.kind else {
1309            return None;
1310        };
1311
1312        let modifier_location =
1313            SourceLocation { start: first.location.start, end: second.location.end };
1314        let scope_id = self.table.current_scope();
1315        let package = self.table.current_package.clone();
1316
1317        for (key_node, _value_node) in pairs {
1318            let method_names = Self::collect_symbol_names(key_node);
1319            for method_name in method_names {
1320                self.table.add_symbol(Symbol {
1321                    name: method_name.clone(),
1322                    qualified_name: format!("{package}::{method_name}"),
1323                    kind: SymbolKind::Subroutine,
1324                    location: modifier_location,
1325                    scope_id,
1326                    declaration: Some(modifier_name.to_string()),
1327                    documentation: Some(format!(
1328                        "Method modifier `{modifier_name}` for `{method_name}`"
1329                    )),
1330                    attributes: vec![format!("modifier={modifier_name}")],
1331                });
1332            }
1333        }
1334
1335        // Visit the body of the modifier subroutines
1336        self.visit_node(second);
1337
1338        Some(2)
1339    }
1340
1341    fn is_moose_method_modifier(name: &str) -> bool {
1342        matches!(name, "before" | "after" | "around" | "override" | "augment")
1343    }
1344
1345    /// Detect Moo/Moose `extends 'Parent'` and `with 'Role'` declarations.
1346    ///
1347    /// Pattern (two statements):
1348    /// 1. `ExpressionStatement(Identifier("extends"))` or `ExpressionStatement(Identifier("with"))`
1349    /// 2. `ExpressionStatement(String(...))` or `ExpressionStatement(ArrayLiteral(...))`
1350    ///
1351    /// Also handles FunctionCall form: `extends 'Parent'` (post parser fix).
1352    fn try_extract_extends_with(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
1353        let first = &statements[idx];
1354
1355        // FunctionCall form: `extends 'Parent'` / `with 'Role'` parsed as bare calls.
1356        if let NodeKind::ExpressionStatement { expression } = &first.kind
1357            && let NodeKind::FunctionCall { name, args } = &expression.kind
1358            && matches!(name.as_str(), "extends" | "with")
1359        {
1360            let keyword = name.as_str();
1361            let names: Vec<String> = args.iter().flat_map(Self::collect_symbol_names).collect();
1362            if !names.is_empty() {
1363                if names.iter().any(|name| name == "Catalyst::Controller") {
1364                    let package = self.table.current_package.clone();
1365                    self.mark_catalyst_controller_package(&package);
1366                }
1367                let ref_kind =
1368                    if keyword == "extends" { SymbolKind::Class } else { SymbolKind::Role };
1369                for ref_name in names {
1370                    self.table.add_reference(SymbolReference {
1371                        name: ref_name,
1372                        kind: ref_kind,
1373                        location: first.location,
1374                        scope_id: self.table.current_scope(),
1375                        is_write: false,
1376                    });
1377                }
1378                return Some(1);
1379            }
1380        }
1381
1382        if idx + 1 >= statements.len() {
1383            return None;
1384        }
1385
1386        let second = &statements[idx + 1];
1387
1388        // Check: first is ExpressionStatement(Identifier("extends"|"with"))
1389        let keyword = match &first.kind {
1390            NodeKind::ExpressionStatement { expression } => match &expression.kind {
1391                NodeKind::Identifier { name } if matches!(name.as_str(), "extends" | "with") => {
1392                    name.as_str()
1393                }
1394                _ => return None,
1395            },
1396            _ => return None,
1397        };
1398
1399        // Check: second is ExpressionStatement with name(s)
1400        let NodeKind::ExpressionStatement { expression } = &second.kind else {
1401            return None;
1402        };
1403
1404        let names = Self::collect_symbol_names(expression);
1405        if names.is_empty() {
1406            return None;
1407        }
1408
1409        if names.iter().any(|name| name == "Catalyst::Controller") {
1410            let package = self.table.current_package.clone();
1411            self.mark_catalyst_controller_package(&package);
1412        }
1413
1414        let ref_location = SourceLocation { start: first.location.start, end: second.location.end };
1415
1416        let ref_kind = if keyword == "extends" { SymbolKind::Class } else { SymbolKind::Role };
1417
1418        for name in names {
1419            self.table.add_reference(SymbolReference {
1420                name,
1421                kind: ref_kind,
1422                location: ref_location,
1423                scope_id: self.table.current_scope(),
1424                is_write: false,
1425            });
1426        }
1427
1428        Some(2)
1429    }
1430
1431    /// Detect Moo/Moose `requires 'method'` declarations.
1432    ///
1433    /// Pattern:
1434    /// `ExpressionStatement(Identifier("requires"))` followed by `ExpressionStatement(String(...))` or similar
1435    ///
1436    /// Also handles FunctionCall form: `requires 'method'` (post parser fix).
1437    fn try_extract_role_requires(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
1438        let first = &statements[idx];
1439
1440        // FunctionCall form: `requires 'method'` parsed as a bare call.
1441        if let NodeKind::ExpressionStatement { expression } = &first.kind
1442            && let NodeKind::FunctionCall { name, args } = &expression.kind
1443            && name == "requires"
1444        {
1445            let names: Vec<String> = args.iter().flat_map(Self::collect_symbol_names).collect();
1446            if !names.is_empty() {
1447                let scope_id = self.table.current_scope();
1448                let package = self.table.current_package.clone();
1449                for method_name in names {
1450                    self.table.add_symbol(Symbol {
1451                        name: method_name.clone(),
1452                        qualified_name: format!("{package}::{method_name}"),
1453                        kind: SymbolKind::Subroutine,
1454                        location: first.location,
1455                        scope_id,
1456                        declaration: Some("requires".to_string()),
1457                        documentation: Some(format!("Required method `{method_name}` from role")),
1458                        attributes: vec!["requires=true".to_string()],
1459                    });
1460                }
1461                return Some(1);
1462            }
1463        }
1464
1465        if idx + 1 >= statements.len() {
1466            return None;
1467        }
1468
1469        let second = &statements[idx + 1];
1470
1471        // Check: first is ExpressionStatement(Identifier("requires"))
1472        let is_requires = match &first.kind {
1473            NodeKind::ExpressionStatement { expression } => {
1474                matches!(&expression.kind, NodeKind::Identifier { name } if name == "requires")
1475            }
1476            _ => false,
1477        };
1478
1479        if !is_requires {
1480            return None;
1481        }
1482
1483        let NodeKind::ExpressionStatement { expression } = &second.kind else {
1484            return None;
1485        };
1486
1487        let names = Self::collect_symbol_names(expression);
1488        if names.is_empty() {
1489            return None;
1490        }
1491
1492        let location = SourceLocation { start: first.location.start, end: second.location.end };
1493        let scope_id = self.table.current_scope();
1494        let package = self.table.current_package.clone();
1495
1496        for name in names {
1497            self.table.add_symbol(Symbol {
1498                name: name.clone(),
1499                qualified_name: format!("{package}::{name}"),
1500                kind: SymbolKind::Subroutine,
1501                location,
1502                scope_id,
1503                declaration: Some("requires".to_string()),
1504                documentation: Some(format!("Required method `{name}` from role")),
1505                attributes: vec!["requires=true".to_string()],
1506            });
1507        }
1508
1509        Some(2)
1510    }
1511
1512    /// Synthesize symbols from parsed `has` key/value pairs.
1513    fn synthesize_moo_has_pairs(
1514        &mut self,
1515        pairs: &[(Node, Node)],
1516        has_location: SourceLocation,
1517        require_embedded_marker: bool,
1518    ) {
1519        for (attr_expr, options_expr) in pairs {
1520            let Some(attr_expr) = Self::moo_attribute_expr(attr_expr, require_embedded_marker)
1521            else {
1522                continue;
1523            };
1524
1525            let attribute_names = Self::collect_symbol_names(attr_expr);
1526            if attribute_names.is_empty() {
1527                continue;
1528            }
1529
1530            if let NodeKind::HashLiteral { pairs: option_pairs } = &options_expr.kind {
1531                self.synthesize_moo_has_attrs_with_options(
1532                    &attribute_names,
1533                    option_pairs,
1534                    has_location,
1535                );
1536            }
1537        }
1538    }
1539
1540    /// Synthesize Moo symbols for a known list of attributes and options.
1541    fn synthesize_moo_has_attrs_with_options(
1542        &mut self,
1543        attribute_names: &[String],
1544        option_pairs: &[(Node, Node)],
1545        has_location: SourceLocation,
1546    ) {
1547        let scope_id = self.table.current_scope();
1548        let package = self.table.current_package.clone();
1549
1550        // Create a dummy options_expr Node to pass to existing helpers
1551        // (a bit hacky, but avoids rewriting the helpers that take Node)
1552        let options_expr = Node {
1553            kind: NodeKind::HashLiteral { pairs: option_pairs.to_vec() },
1554            location: has_location,
1555        };
1556
1557        let option_map = Self::extract_hash_options(&options_expr);
1558        let metadata = Self::attribute_metadata(&option_map);
1559        let generated_methods =
1560            Self::moo_accessor_names(attribute_names, &option_map, &options_expr);
1561
1562        for attribute_name in attribute_names {
1563            self.table.add_symbol(Symbol {
1564                name: attribute_name.clone(),
1565                qualified_name: format!("{package}::{attribute_name}"),
1566                kind: SymbolKind::scalar(),
1567                location: has_location,
1568                scope_id,
1569                declaration: Some("has".to_string()),
1570                documentation: Some(format!("Moo/Moose attribute `{attribute_name}`")),
1571                attributes: metadata.clone(),
1572            });
1573        }
1574
1575        // Build accessor documentation that includes the isa type when available.
1576        let accessor_doc = Self::moo_accessor_doc(&option_map);
1577
1578        for method_name in generated_methods {
1579            self.table.add_symbol(Symbol {
1580                name: method_name.clone(),
1581                qualified_name: format!("{package}::{method_name}"),
1582                kind: SymbolKind::Subroutine,
1583                location: has_location,
1584                scope_id,
1585                declaration: Some("has".to_string()),
1586                documentation: Some(accessor_doc.clone()),
1587                attributes: metadata.clone(),
1588            });
1589        }
1590    }
1591
1592    /// Resolve the attribute-expression node used in a parsed `has` declaration pair.
1593    fn moo_attribute_expr(attr_expr: &Node, require_embedded_marker: bool) -> Option<&Node> {
1594        if let NodeKind::Binary { op, left, right } = &attr_expr.kind
1595            && op == "[]"
1596            && matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
1597        {
1598            return Some(right.as_ref());
1599        }
1600
1601        if require_embedded_marker { None } else { Some(attr_expr) }
1602    }
1603
1604    /// Detect Dancer/Dancer2/Mojolicious::Lite route declarations and synthesize route symbols.
1605    ///
1606    /// Pattern (two statements):
1607    /// 1. `ExpressionStatement(Identifier("get"|"post"|"put"|"del"|"patch"|"any"))`
1608    /// 2. `ExpressionStatement(HashLiteral([ (String("/path"), Subroutine{...}) ]))`
1609    ///
1610    /// Synthesizes a `Subroutine` symbol named by the route path with
1611    /// `http_method=<METHOD>` in attributes and a human-readable documentation string.
1612    fn try_extract_web_route_declaration(
1613        &mut self,
1614        statements: &[Node],
1615        idx: usize,
1616    ) -> Option<usize> {
1617        let web_framework = self
1618            .framework_flags
1619            .get(&self.table.current_package)
1620            .and_then(|flags| flags.web_framework);
1621        let first = &statements[idx];
1622
1623        // FunctionCall form: `get '/path' => sub { }` parsed as a bare call.
1624        if let NodeKind::ExpressionStatement { expression } = &first.kind
1625            && let NodeKind::FunctionCall { name, args } = &expression.kind
1626            && matches!(name.as_str(), "get" | "post" | "put" | "del" | "delete" | "patch" | "any")
1627        {
1628            let method_name = name.as_str();
1629            // args[0] is the route path (String), rest is the handler
1630            if let Some(path_node) = args.first() {
1631                if let NodeKind::String { value, .. } = &path_node.kind {
1632                    if let Some(path) = Self::normalize_symbol_name(value) {
1633                        let http_method = match method_name {
1634                            "get" => "GET",
1635                            "post" => "POST",
1636                            "put" => "PUT",
1637                            "del" | "delete" => "DELETE",
1638                            "patch" => "PATCH",
1639                            "any" => "ANY",
1640                            _ => method_name,
1641                        };
1642                        let scope_id = self.table.current_scope();
1643                        self.table.add_symbol(Symbol {
1644                            name: path.clone(),
1645                            qualified_name: path.clone(),
1646                            kind: SymbolKind::Subroutine,
1647                            location: first.location,
1648                            scope_id,
1649                            declaration: Some(method_name.to_string()),
1650                            documentation: Some(format!("{http_method} {path}")),
1651                            attributes: vec![format!("http_method={http_method}")],
1652                        });
1653
1654                        if matches!(
1655                            web_framework,
1656                            Some(WebFrameworkKind::Dancer | WebFrameworkKind::Dancer2)
1657                        ) && let Some(target_node) = args.get(1)
1658                        {
1659                            if let Some(target_name) =
1660                                Self::collect_symbol_names(target_node).first().cloned()
1661                            {
1662                                self.table.add_reference(SymbolReference {
1663                                    name: target_name,
1664                                    kind: SymbolKind::Subroutine,
1665                                    location: target_node.location,
1666                                    scope_id: self.table.current_scope(),
1667                                    is_write: false,
1668                                });
1669                            }
1670                        }
1671
1672                        self.visit_node(first);
1673                        return Some(1);
1674                    }
1675                }
1676            }
1677        }
1678
1679        if idx + 1 >= statements.len() {
1680            return None;
1681        }
1682
1683        let second = &statements[idx + 1];
1684
1685        // First statement must be ExpressionStatement(Identifier(<route_method>))
1686        let method_name = match &first.kind {
1687            NodeKind::ExpressionStatement { expression } => match &expression.kind {
1688                NodeKind::Identifier { name }
1689                    if matches!(
1690                        name.as_str(),
1691                        "get" | "post" | "put" | "del" | "delete" | "patch" | "any"
1692                    ) =>
1693                {
1694                    name.as_str()
1695                }
1696                _ => return None,
1697            },
1698            _ => return None,
1699        };
1700
1701        // Second statement must be ExpressionStatement(HashLiteral([ (path, handler) ]))
1702        let NodeKind::ExpressionStatement { expression } = &second.kind else {
1703            return None;
1704        };
1705        let NodeKind::HashLiteral { pairs } = &expression.kind else {
1706            return None;
1707        };
1708
1709        // Extract route path from the first key in the hash literal (strip surrounding quotes)
1710        let (path_node, _handler_node) = pairs.first()?;
1711        let path = match &path_node.kind {
1712            NodeKind::String { value, .. } => Self::normalize_symbol_name(value)?,
1713            _ => return None,
1714        };
1715
1716        let http_method = match method_name {
1717            "get" => "GET",
1718            "post" => "POST",
1719            "put" => "PUT",
1720            "del" | "delete" => "DELETE",
1721            "patch" => "PATCH",
1722            "any" => "ANY",
1723            _ => method_name,
1724        };
1725
1726        let route_location =
1727            SourceLocation { start: first.location.start, end: second.location.end };
1728        let scope_id = self.table.current_scope();
1729
1730        self.table.add_symbol(Symbol {
1731            name: path.clone(),
1732            qualified_name: path.clone(),
1733            kind: SymbolKind::Subroutine,
1734            location: route_location,
1735            scope_id,
1736            declaration: Some(method_name.to_string()),
1737            documentation: Some(format!("{http_method} {path}")),
1738            attributes: vec![format!("http_method={http_method}")],
1739        });
1740
1741        // Visit the handler body so variables inside the sub are still indexed
1742        self.visit_node(second);
1743
1744        Some(2)
1745    }
1746
1747    /// Synthesize Plack::Builder middleware and mount symbols from a builder block.
1748    fn synthesize_plack_builder_symbols(&mut self, name: &str, args: &[Node]) {
1749        let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
1750            return;
1751        };
1752        if flags.web_framework != Some(WebFrameworkKind::PlackBuilder) || name != "builder" {
1753            return;
1754        }
1755
1756        let Some(block) = args.first() else {
1757            return;
1758        };
1759        let NodeKind::Block { statements } = &block.kind else {
1760            return;
1761        };
1762
1763        let scope_id = self.table.current_scope();
1764        let package = self.table.current_package.clone();
1765
1766        for statement in statements {
1767            let NodeKind::ExpressionStatement { expression } = &statement.kind else {
1768                continue;
1769            };
1770            let NodeKind::FunctionCall { name: stmt_name, args: stmt_args } = &expression.kind
1771            else {
1772                continue;
1773            };
1774
1775            match stmt_name.as_str() {
1776                "enable" => {
1777                    self.synthesize_plack_enable_symbol(statement, stmt_args, scope_id, &package);
1778                }
1779                "mount" => {
1780                    self.synthesize_plack_mount_symbol(statement, stmt_args, scope_id, &package);
1781                }
1782                _ => {}
1783            }
1784        }
1785    }
1786
1787    fn synthesize_plack_enable_symbol(
1788        &mut self,
1789        statement: &Node,
1790        args: &[Node],
1791        scope_id: ScopeId,
1792        _package: &str,
1793    ) {
1794        let Some(first) = args.first() else {
1795            return;
1796        };
1797        let Some(raw_name) = Self::single_symbol_name(first) else {
1798            return;
1799        };
1800        let middleware_name = if raw_name.contains("::") {
1801            raw_name
1802        } else {
1803            format!("Plack::Middleware::{raw_name}")
1804        };
1805        if middleware_name.is_empty() {
1806            return;
1807        }
1808
1809        if self.table.symbols.get(&middleware_name).is_some_and(|symbols| {
1810            symbols.iter().any(|symbol| {
1811                symbol.kind == SymbolKind::Package
1812                    && symbol.declaration.as_deref() == Some("enable")
1813                    && symbol
1814                        .attributes
1815                        .iter()
1816                        .any(|attr| attr == &format!("middleware={middleware_name}"))
1817            })
1818        }) {
1819            return;
1820        }
1821
1822        self.table.add_symbol(Symbol {
1823            name: middleware_name.clone(),
1824            qualified_name: middleware_name.clone(),
1825            kind: SymbolKind::Package,
1826            location: statement.location,
1827            scope_id,
1828            declaration: Some("enable".to_string()),
1829            documentation: Some(format!("PSGI middleware {middleware_name}")),
1830            attributes: vec![
1831                "framework=Plack::Builder".to_string(),
1832                format!("middleware={middleware_name}"),
1833            ],
1834        });
1835    }
1836
1837    fn synthesize_plack_mount_symbol(
1838        &mut self,
1839        statement: &Node,
1840        args: &[Node],
1841        scope_id: ScopeId,
1842        _package: &str,
1843    ) {
1844        let Some(path_node) = args.first() else {
1845            return;
1846        };
1847        let Some(path) = Self::single_symbol_name(path_node) else {
1848            return;
1849        };
1850        if path.is_empty() {
1851            return;
1852        }
1853
1854        let target = args
1855            .get(1)
1856            .map(Self::value_summary)
1857            .filter(|s| !s.is_empty())
1858            .unwrap_or_else(|| "$app".to_string());
1859
1860        if self.table.symbols.get(&path).is_some_and(|symbols| {
1861            symbols.iter().any(|symbol| {
1862                symbol.kind == SymbolKind::Subroutine
1863                    && symbol.declaration.as_deref() == Some("mount")
1864                    && symbol.attributes.iter().any(|attr| attr == &format!("mount_path={path}"))
1865            })
1866        }) {
1867            return;
1868        }
1869
1870        self.table.add_symbol(Symbol {
1871            name: path.clone(),
1872            qualified_name: path.clone(),
1873            kind: SymbolKind::Subroutine,
1874            location: statement.location,
1875            scope_id,
1876            declaration: Some("mount".to_string()),
1877            documentation: Some(format!("PSGI mount {path} -> {target}")),
1878            attributes: vec![
1879                "framework=Plack::Builder".to_string(),
1880                format!("mount_path={path}"),
1881                format!("mount_target={target}"),
1882            ],
1883        });
1884    }
1885
1886    /// Extract Class::Accessor generated accessors from `mk_*_accessors` calls.
1887    fn try_extract_class_accessor_declaration(&mut self, statement: &Node) -> bool {
1888        let NodeKind::ExpressionStatement { expression } = &statement.kind else {
1889            return false;
1890        };
1891
1892        let NodeKind::MethodCall { method, args, .. } = &expression.kind else {
1893            return false;
1894        };
1895
1896        let is_accessor_generator = matches!(
1897            method.as_str(),
1898            "mk_accessors" | "mk_ro_accessors" | "mk_rw_accessors" | "mk_wo_accessors"
1899        );
1900        if !is_accessor_generator {
1901            return false;
1902        }
1903
1904        let mut accessor_names = Vec::new();
1905        for arg in args {
1906            accessor_names.extend(Self::collect_symbol_names(arg));
1907        }
1908        if accessor_names.is_empty() {
1909            return false;
1910        }
1911
1912        let mut seen = HashSet::new();
1913        let scope_id = self.table.current_scope();
1914        let package = self.table.current_package.clone();
1915
1916        for accessor_name in accessor_names {
1917            if !seen.insert(accessor_name.clone()) {
1918                continue;
1919            }
1920
1921            self.table.add_symbol(Symbol {
1922                name: accessor_name.clone(),
1923                qualified_name: format!("{package}::{accessor_name}"),
1924                kind: SymbolKind::Subroutine,
1925                location: statement.location,
1926                scope_id,
1927                declaration: Some(method.clone()),
1928                documentation: Some("Generated accessor (Class::Accessor)".to_string()),
1929                attributes: vec!["framework=Class::Accessor".to_string()],
1930            });
1931        }
1932
1933        true
1934    }
1935
1936    /// Synthesize class symbols for async framework namespaces used in method-call form.
1937    fn synthesize_async_framework_class_symbol(&mut self, object: &Node) -> bool {
1938        let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
1939            return false;
1940        };
1941
1942        let (module_name, framework_name, exact_match) = match flags.async_framework {
1943            Some(AsyncFrameworkKind::AnyEvent) => ("AnyEvent", "AnyEvent", false),
1944            Some(AsyncFrameworkKind::EV) => ("EV", "EV", true),
1945            Some(AsyncFrameworkKind::Future) => ("Future", "Future", true),
1946            Some(AsyncFrameworkKind::FutureXS) => ("Future::XS", "Future::XS", true),
1947            Some(AsyncFrameworkKind::Promise) => ("Promise", "Promise", true),
1948            Some(AsyncFrameworkKind::PromiseXS) => ("Promise::XS", "Promise::XS", true),
1949            Some(AsyncFrameworkKind::POE) => ("POE", "POE", false),
1950            Some(AsyncFrameworkKind::IOAsync) => ("IO::Async", "IO::Async", false),
1951            Some(AsyncFrameworkKind::MojoRedis) => ("Mojo::Redis", "Mojo::Redis", true),
1952            Some(AsyncFrameworkKind::MojoPg) => ("Mojo::Pg", "Mojo::Pg", true),
1953            None => return false,
1954        };
1955
1956        let Some(name) = Self::single_symbol_name(object) else {
1957            return false;
1958        };
1959        if flags.async_framework == Some(AsyncFrameworkKind::AnyEvent) {
1960            if !matches!(
1961                name.as_str(),
1962                "AnyEvent" | "AnyEvent::CondVar" | "AnyEvent::Timer" | "AnyEvent::IO"
1963            ) {
1964                return false;
1965            }
1966        } else if exact_match {
1967            if name != module_name {
1968                return false;
1969            }
1970        } else if !name.starts_with(&format!("{module_name}::")) {
1971            return false;
1972        }
1973
1974        let already_synthesized = self.table.symbols.get(&name).is_some_and(|symbols| {
1975            symbols.iter().any(|symbol| {
1976                symbol.kind == SymbolKind::Class
1977                    && symbol.declaration.as_deref() == Some(&format!("framework={framework_name}"))
1978            })
1979        });
1980        if already_synthesized {
1981            return true;
1982        }
1983
1984        let framework_attr = format!("framework={framework_name}");
1985
1986        self.table.add_symbol(Symbol {
1987            name: name.clone(),
1988            qualified_name: name.clone(),
1989            kind: SymbolKind::Class,
1990            location: object.location,
1991            scope_id: self.table.current_scope(),
1992            declaration: Some(framework_attr.clone()),
1993            documentation: Some(format!("Synthetic {framework_name} class")),
1994            attributes: vec![framework_attr],
1995        });
1996
1997        true
1998    }
1999
2000    /// Synthesize the `EV` namespace symbol when the framework is imported.
2001    fn synthesize_ev_framework_symbol(&mut self, location: SourceLocation) {
2002        let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
2003            return;
2004        };
2005        if flags.async_framework != Some(AsyncFrameworkKind::EV) {
2006            return;
2007        }
2008
2009        let name = "EV";
2010        if self.table.symbols.get(name).is_some_and(|symbols| {
2011            symbols.iter().any(|symbol| {
2012                symbol.kind == SymbolKind::Class
2013                    && symbol.declaration.as_deref() == Some("framework=EV")
2014            })
2015        }) {
2016            return;
2017        }
2018
2019        self.table.add_symbol(Symbol {
2020            name: name.to_string(),
2021            qualified_name: name.to_string(),
2022            kind: SymbolKind::Class,
2023            location,
2024            scope_id: self.table.current_scope(),
2025            declaration: Some("framework=EV".to_string()),
2026            documentation: Some("Synthetic EV namespace".to_string()),
2027            attributes: vec!["framework=EV".to_string()],
2028        });
2029    }
2030
2031    /// Synthesize narrow EV watcher / loop API symbols used in function-call form.
2032    fn synthesize_ev_symbols(&mut self, name: &str, location: SourceLocation) -> bool {
2033        let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
2034            return false;
2035        };
2036        if flags.async_framework != Some(AsyncFrameworkKind::EV) {
2037            return false;
2038        }
2039
2040        let Some(ev_suffix) = name.strip_prefix("EV::") else {
2041            return false;
2042        };
2043        if !matches!(ev_suffix, "timer" | "io" | "signal" | "idle") {
2044            return false;
2045        }
2046
2047        let already_synthesized = self.table.symbols.get(name).is_some_and(|symbols| {
2048            symbols.iter().any(|symbol| {
2049                symbol.kind == SymbolKind::Subroutine
2050                    && symbol.declaration.as_deref() == Some("framework=EV")
2051            })
2052        });
2053        if already_synthesized {
2054            return true;
2055        }
2056
2057        self.table.add_symbol(Symbol {
2058            name: name.to_string(),
2059            qualified_name: name.to_string(),
2060            kind: SymbolKind::Subroutine,
2061            location,
2062            scope_id: self.table.current_scope(),
2063            declaration: Some("framework=EV".to_string()),
2064            documentation: Some(format!("Synthetic EV API `{ev_suffix}`")),
2065            attributes: vec!["framework=EV".to_string(), format!("ev_api={ev_suffix}")],
2066        });
2067
2068        true
2069    }
2070
2071    /// Synthesize a narrow async framework API surface for common entrypoints.
2072    ///
2073    /// This intentionally avoids type inference. It only exposes the canonical
2074    /// constructor / class methods and the common chain methods that are most
2075    /// useful for navigation and references when a file opts into an async
2076    /// framework such as Future or Promise.
2077    fn synthesize_future_api_symbols(
2078        &mut self,
2079        object: &Node,
2080        method: &str,
2081        location: SourceLocation,
2082    ) -> bool {
2083        let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
2084            return false;
2085        };
2086
2087        let (framework_name, root_name, chain_methods, class_entrypoints) =
2088            match flags.async_framework {
2089                Some(AsyncFrameworkKind::Future) => (
2090                    "Future",
2091                    "Future",
2092                    vec!["then", "catch", "finally", "get", "is_done", "is_ready"],
2093                    vec!["new", "done", "fail", "wait_all", "needs_all", "needs_any"],
2094                ),
2095                Some(AsyncFrameworkKind::FutureXS) => (
2096                    "Future::XS",
2097                    "Future::XS",
2098                    vec!["then", "catch", "finally", "get", "is_done", "is_ready"],
2099                    vec!["new", "done", "fail", "wait_all", "needs_all", "needs_any"],
2100                ),
2101                Some(AsyncFrameworkKind::Promise) => (
2102                    "Promise",
2103                    "Promise",
2104                    vec!["then", "catch", "finally", "resolve", "reject"],
2105                    vec!["new", "all", "race", "any"],
2106                ),
2107                Some(AsyncFrameworkKind::PromiseXS) => (
2108                    "Promise::XS",
2109                    "Promise::XS",
2110                    vec!["then", "catch", "finally", "resolve", "reject"],
2111                    vec!["new", "all", "race", "any"],
2112                ),
2113                _ => return false,
2114            };
2115
2116        let object_name = Self::single_symbol_name(object);
2117
2118        let should_synthesize = if chain_methods.contains(&method) {
2119            true
2120        } else if class_entrypoints.contains(&method) {
2121            object_name.is_some_and(|name| name == root_name)
2122        } else {
2123            false
2124        };
2125        if !should_synthesize {
2126            return false;
2127        }
2128
2129        let already_synthesized = self.table.symbols.get(method).is_some_and(|symbols| {
2130            symbols.iter().any(|symbol| {
2131                symbol.kind == SymbolKind::Subroutine
2132                    && symbol.declaration.as_deref() == Some(&format!("framework={framework_name}"))
2133                    && symbol.attributes.iter().any(|attr| attr == &format!("future_api={method}"))
2134            })
2135        });
2136        if already_synthesized {
2137            return true;
2138        }
2139
2140        self.table.add_symbol(Symbol {
2141            name: method.to_string(),
2142            qualified_name: format!("{framework_name}::{method}"),
2143            kind: SymbolKind::Subroutine,
2144            location,
2145            scope_id: self.table.current_scope(),
2146            declaration: Some(format!("framework={framework_name}")),
2147            documentation: Some(format!("Synthetic {framework_name} API `{method}`")),
2148            attributes: vec![format!("framework={framework_name}"), format!("future_api={method}")],
2149        });
2150
2151        true
2152    }
2153
2154    /// Update framework detection state from `use` statements.
2155    fn update_framework_context(&mut self, module: &str, args: &[String]) {
2156        let pkg = self.table.current_package.clone();
2157
2158        let framework_kind = match module {
2159            "Moo" | "Mouse" => Some(FrameworkKind::Moo),
2160            "Moo::Role" | "Mouse::Role" => Some(FrameworkKind::MooRole),
2161            "Moose" => Some(FrameworkKind::Moose),
2162            "Moose::Role" => Some(FrameworkKind::MooseRole),
2163            "Role::Tiny" => Some(FrameworkKind::RoleTiny),
2164            "Role::Tiny::With" => Some(FrameworkKind::RoleTinyWith),
2165            _ => None,
2166        };
2167
2168        if let Some(kind) = framework_kind {
2169            let flags = self.framework_flags.entry(pkg.clone()).or_default();
2170            flags.moo = true;
2171            flags.kind = Some(kind);
2172            return;
2173        }
2174
2175        if module == "Class::Accessor" {
2176            self.framework_flags.entry(pkg.clone()).or_default().class_accessor = true;
2177            return;
2178        }
2179
2180        let web_kind = match module {
2181            "Dancer" => Some(WebFrameworkKind::Dancer),
2182            "Dancer2" | "Dancer2::Core" => Some(WebFrameworkKind::Dancer2),
2183            "Mojolicious::Lite" => Some(WebFrameworkKind::MojoliciousLite),
2184            "Plack::Builder" => Some(WebFrameworkKind::PlackBuilder),
2185            _ => None,
2186        };
2187        if let Some(kind) = web_kind {
2188            self.framework_flags.entry(pkg.clone()).or_default().web_framework = Some(kind);
2189            return;
2190        }
2191
2192        if module == "IO::Async" || module.starts_with("IO::Async::") {
2193            self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2194                Some(AsyncFrameworkKind::IOAsync);
2195            return;
2196        }
2197
2198        if module == "AnyEvent" {
2199            self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2200                Some(AsyncFrameworkKind::AnyEvent);
2201            return;
2202        }
2203
2204        if module == "EV" {
2205            self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2206                Some(AsyncFrameworkKind::EV);
2207            return;
2208        }
2209
2210        if module == "Future" {
2211            self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2212                Some(AsyncFrameworkKind::Future);
2213            return;
2214        }
2215
2216        if module == "Future::XS" {
2217            self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2218                Some(AsyncFrameworkKind::FutureXS);
2219            return;
2220        }
2221
2222        if module == "Promise" {
2223            self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2224                Some(AsyncFrameworkKind::Promise);
2225            return;
2226        }
2227
2228        if module == "Promise::XS" {
2229            self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2230                Some(AsyncFrameworkKind::PromiseXS);
2231            return;
2232        }
2233
2234        if module == "POE" || module.starts_with("POE::") {
2235            self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2236                Some(AsyncFrameworkKind::POE);
2237            return;
2238        }
2239
2240        if module == "Mojo::Redis" {
2241            self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2242                Some(AsyncFrameworkKind::MojoRedis);
2243            return;
2244        }
2245
2246        if module == "Mojo::Pg" {
2247            self.framework_flags.entry(pkg.clone()).or_default().async_framework =
2248                Some(AsyncFrameworkKind::MojoPg);
2249            return;
2250        }
2251
2252        if matches!(module, "base" | "parent") {
2253            let has_class_accessor_parent = args
2254                .iter()
2255                .filter_map(|arg| Self::normalize_symbol_name(arg))
2256                .any(|arg| arg == "Class::Accessor");
2257            if has_class_accessor_parent {
2258                self.framework_flags.entry(pkg.clone()).or_default().class_accessor = true;
2259            }
2260            let has_catalyst_controller_parent = args
2261                .iter()
2262                .filter_map(|arg| Self::normalize_symbol_name(arg))
2263                .any(|arg| arg == "Catalyst::Controller");
2264            if has_catalyst_controller_parent {
2265                self.mark_catalyst_controller_package(&pkg);
2266            }
2267        }
2268    }
2269
2270    fn mark_catalyst_controller_package(&mut self, package: &str) {
2271        self.framework_flags.entry(package.to_string()).or_default().catalyst_controller = true;
2272    }
2273
2274    fn current_package_is_catalyst_controller(&self) -> bool {
2275        self.framework_flags
2276            .get(&self.table.current_package)
2277            .is_some_and(|flags| flags.catalyst_controller)
2278            || Self::is_catalyst_controller_package_name(&self.table.current_package)
2279    }
2280
2281    fn is_catalyst_controller_package_name(package: &str) -> bool {
2282        package.contains("::Controller::") || package.ends_with("::Controller")
2283    }
2284
2285    fn catalyst_action_metadata(attributes: &[String]) -> Option<(String, Vec<String>)> {
2286        let mut kind = None;
2287        let mut details = Vec::new();
2288        let mut seen = HashSet::new();
2289
2290        for attr in attributes {
2291            let attr_name = Self::attribute_base_name(attr);
2292            if !Self::is_catalyst_action_attribute(&attr_name) {
2293                continue;
2294            }
2295
2296            if kind.is_none()
2297                || matches!(kind.as_deref(), Some("Args" | "CaptureArgs" | "PathPart"))
2298            {
2299                if matches!(attr_name.as_str(), "Path" | "Local" | "Global" | "Regex" | "Chained") {
2300                    kind = Some(attr_name.clone());
2301                } else if kind.is_none() {
2302                    kind = Some(attr_name.clone());
2303                }
2304            }
2305
2306            if seen.insert(attr.clone()) {
2307                details.push(attr.clone());
2308            }
2309        }
2310
2311        if let Some(action_kind) = kind.as_deref()
2312            && matches!(action_kind, "Path" | "Local" | "Global" | "Regex" | "Chained")
2313        {
2314            details.retain(|attr| Self::attribute_base_name(attr) != action_kind);
2315        }
2316
2317        kind.map(|kind| (kind, details))
2318    }
2319
2320    fn is_catalyst_action_attribute(attr_name: &str) -> bool {
2321        matches!(
2322            attr_name,
2323            "Path" | "Local" | "Global" | "Regex" | "Chained" | "PathPart" | "Args" | "CaptureArgs"
2324        )
2325    }
2326
2327    fn attribute_base_name(attr: &str) -> String {
2328        attr.trim_start_matches(':')
2329            .split(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == ':'))
2330            .next()
2331            .unwrap_or("")
2332            .to_string()
2333    }
2334
2335    /// Parse attribute metadata from Moo/Moose option hashes.
2336    fn extract_hash_options(node: &Node) -> HashMap<String, String> {
2337        let mut options = HashMap::new();
2338        let NodeKind::HashLiteral { pairs } = &node.kind else {
2339            return options;
2340        };
2341
2342        for (key_node, value_node) in pairs {
2343            let Some(key_name) = Self::single_symbol_name(key_node) else {
2344                continue;
2345            };
2346            let value_text = Self::value_summary(value_node);
2347            options.insert(key_name, value_text);
2348        }
2349
2350        options
2351    }
2352
2353    /// Convert option metadata into hover-friendly key/value tags.
2354    fn attribute_metadata(option_map: &HashMap<String, String>) -> Vec<String> {
2355        let preferred_order = [
2356            "is",
2357            "isa",
2358            "required",
2359            "lazy",
2360            "builder",
2361            "default",
2362            "reader",
2363            "writer",
2364            "accessor",
2365            "predicate",
2366            "clearer",
2367            "handles",
2368        ];
2369
2370        let mut metadata = Vec::new();
2371        for key in preferred_order {
2372            if let Some(value) = option_map.get(key) {
2373                metadata.push(format!("{key}={value}"));
2374            }
2375        }
2376        metadata
2377    }
2378
2379    /// Build a documentation string for a generated Moo/Moose accessor method.
2380    ///
2381    /// Includes the `isa` type constraint and access mode when present in the
2382    /// option map, producing hover-friendly documentation such as:
2383    ///
2384    /// ```text
2385    /// Moo/Moose accessor (isa: Str, ro)
2386    /// ```
2387    fn moo_accessor_doc(option_map: &HashMap<String, String>) -> String {
2388        let mut parts = Vec::new();
2389
2390        if let Some(isa) = option_map.get("isa") {
2391            parts.push(format!("isa: {isa}"));
2392        }
2393        if let Some(is) = option_map.get("is") {
2394            parts.push(is.clone());
2395        }
2396
2397        if parts.is_empty() {
2398            "Generated accessor from Moo/Moose `has`".to_string()
2399        } else {
2400            format!("Moo/Moose accessor ({})", parts.join(", "))
2401        }
2402    }
2403
2404    /// Compute accessor method names for a Moo/Moose `has` declaration.
2405    fn moo_accessor_names(
2406        attribute_names: &[String],
2407        option_map: &HashMap<String, String>,
2408        options_expr: &Node,
2409    ) -> Vec<String> {
2410        let mut methods = Vec::new();
2411        let mut seen = HashSet::new();
2412
2413        for key in ["accessor", "reader", "writer", "predicate", "clearer", "builder"] {
2414            for name in Self::option_method_names(options_expr, key, attribute_names) {
2415                if seen.insert(name.clone()) {
2416                    methods.push(name);
2417                }
2418            }
2419        }
2420
2421        for name in Self::handles_method_names(options_expr) {
2422            if seen.insert(name.clone()) {
2423                methods.push(name);
2424            }
2425        }
2426
2427        // Default accessor when explicit reader/writer/accessor isn't provided.
2428        let has_explicit_accessor = option_map.contains_key("accessor")
2429            || option_map.contains_key("reader")
2430            || option_map.contains_key("writer");
2431        if !has_explicit_accessor {
2432            for attribute_name in attribute_names {
2433                if seen.insert(attribute_name.clone()) {
2434                    methods.push(attribute_name.clone());
2435                }
2436            }
2437        }
2438
2439        methods
2440    }
2441
2442    /// Find an option value node inside a hash-literal options list.
2443    fn find_hash_option_value<'a>(options_expr: &'a Node, key: &str) -> Option<&'a Node> {
2444        let NodeKind::HashLiteral { pairs } = &options_expr.kind else {
2445            return None;
2446        };
2447
2448        for (key_node, value_node) in pairs {
2449            if Self::single_symbol_name(key_node).as_deref() == Some(key) {
2450                return Some(value_node);
2451            }
2452        }
2453
2454        None
2455    }
2456
2457    /// Compute method names from a single Moo/Moose option key.
2458    fn option_method_names(
2459        options_expr: &Node,
2460        key: &str,
2461        attribute_names: &[String],
2462    ) -> Vec<String> {
2463        let Some(value_node) = Self::find_hash_option_value(options_expr, key) else {
2464            return Vec::new();
2465        };
2466
2467        let mut names = Self::collect_symbol_names(value_node);
2468        if !names.is_empty() {
2469            names.sort();
2470            names.dedup();
2471            return names;
2472        }
2473
2474        // Moo/Moose shorthand: `predicate => 1`, `clearer => 1`, `builder => 1`.
2475        if !Self::is_truthy_shorthand(value_node) {
2476            return Vec::new();
2477        }
2478
2479        match key {
2480            "predicate" => attribute_names.iter().map(|name| format!("has_{name}")).collect(),
2481            "clearer" => attribute_names.iter().map(|name| format!("clear_{name}")).collect(),
2482            "builder" => attribute_names.iter().map(|name| format!("_build_{name}")).collect(),
2483            _ => Vec::new(),
2484        }
2485    }
2486
2487    /// Determine if an option node is a static truthy shorthand literal (`1`, `true`, `'1'`).
2488    fn is_truthy_shorthand(node: &Node) -> bool {
2489        match &node.kind {
2490            NodeKind::Number { value } => value.trim() == "1",
2491            NodeKind::Identifier { name } => {
2492                let lower = name.trim().to_ascii_lowercase();
2493                lower == "1" || lower == "true"
2494            }
2495            NodeKind::String { value, .. } => {
2496                Self::normalize_symbol_name(value).is_some_and(|value| {
2497                    let lower = value.to_ascii_lowercase();
2498                    value == "1" || lower == "true"
2499                })
2500            }
2501            _ => false,
2502        }
2503    }
2504
2505    /// Extract delegated method names from a Moo/Moose `handles` option.
2506    fn handles_method_names(options_expr: &Node) -> Vec<String> {
2507        let Some(handles_node) = Self::find_hash_option_value(options_expr, "handles") else {
2508            return Vec::new();
2509        };
2510
2511        let mut names = Vec::new();
2512        match &handles_node.kind {
2513            NodeKind::HashLiteral { pairs } => {
2514                for (key_node, _) in pairs {
2515                    names.extend(Self::collect_symbol_names(key_node));
2516                }
2517            }
2518            _ => {
2519                names.extend(Self::collect_symbol_names(handles_node));
2520            }
2521        }
2522
2523        names.sort();
2524        names.dedup();
2525        names
2526    }
2527
2528    /// Extract one or more symbol names from a framework declaration expression.
2529    fn collect_symbol_names(node: &Node) -> Vec<String> {
2530        match &node.kind {
2531            NodeKind::String { value, .. } => {
2532                Self::normalize_symbol_name(value).into_iter().collect()
2533            }
2534            NodeKind::Identifier { name } => {
2535                Self::normalize_symbol_name(name).into_iter().collect()
2536            }
2537            NodeKind::ArrayLiteral { elements } => {
2538                let mut names = Vec::new();
2539                for element in elements {
2540                    names.extend(Self::collect_symbol_names(element));
2541                }
2542                names
2543            }
2544            _ => Vec::new(),
2545        }
2546    }
2547
2548    /// Extract a single symbol name from a key/value expression.
2549    fn single_symbol_name(node: &Node) -> Option<String> {
2550        Self::collect_symbol_names(node).into_iter().next()
2551    }
2552
2553    /// Normalize a symbol-like literal into a plain name.
2554    fn normalize_symbol_name(raw: &str) -> Option<String> {
2555        let trimmed = raw.trim().trim_matches('\'').trim_matches('"').trim();
2556        if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }
2557    }
2558
2559    /// Produce a short textual value summary for hover metadata.
2560    fn value_summary(node: &Node) -> String {
2561        match &node.kind {
2562            NodeKind::String { value, .. } => {
2563                Self::normalize_symbol_name(value).unwrap_or_else(|| value.clone())
2564            }
2565            NodeKind::Identifier { name } => name.clone(),
2566            NodeKind::Variable { sigil, name } => format!("{sigil}{name}"),
2567            NodeKind::Number { value } => value.clone(),
2568            NodeKind::ArrayLiteral { elements } => {
2569                let mut entries = Vec::new();
2570                for element in elements {
2571                    entries.extend(Self::collect_symbol_names(element));
2572                }
2573                entries.sort();
2574                entries.dedup();
2575                if entries.is_empty() {
2576                    "array".to_string()
2577                } else {
2578                    format!("[{}]", entries.join(","))
2579                }
2580            }
2581            NodeKind::HashLiteral { pairs } => {
2582                let mut entries = Vec::new();
2583                for (key_node, value_node) in pairs {
2584                    let Some(key_name) = Self::single_symbol_name(key_node) else {
2585                        continue;
2586                    };
2587                    if let Some(value_name) = Self::single_symbol_name(value_node) {
2588                        entries.push(format!("{key_name}->{value_name}"));
2589                    } else {
2590                        entries.push(key_name);
2591                    }
2592                }
2593                entries.sort();
2594                entries.dedup();
2595                if entries.is_empty() {
2596                    "hash".to_string()
2597                } else {
2598                    format!("{{{}}}", entries.join(","))
2599                }
2600            }
2601            NodeKind::Undef => "undef".to_string(),
2602            _ => "expr".to_string(),
2603        }
2604    }
2605
2606    /// Compute a method token location for method-call references.
2607    ///
2608    /// Some parsed method-call nodes only cover the object span. This helper scans
2609    /// source text after the object to anchor references on the method name token.
2610    fn method_reference_location(
2611        &self,
2612        call_node: &Node,
2613        object: &Node,
2614        method_name: &str,
2615    ) -> SourceLocation {
2616        if self.source.is_empty() {
2617            return call_node.location;
2618        }
2619
2620        let search_start = object.location.end.min(self.source.len());
2621        let search_end = search_start.saturating_add(160).min(self.source.len());
2622        if search_start >= search_end || !self.source.is_char_boundary(search_start) {
2623            return call_node.location;
2624        }
2625
2626        let window = &self.source[search_start..search_end];
2627        let Some(arrow_idx) = window.find("->") else {
2628            return call_node.location;
2629        };
2630
2631        let mut idx = arrow_idx + 2;
2632        while idx < window.len() {
2633            let b = window.as_bytes()[idx];
2634            if b.is_ascii_whitespace() {
2635                idx += 1;
2636            } else {
2637                break;
2638            }
2639        }
2640
2641        let suffix = &window[idx..];
2642        if suffix.starts_with(method_name) {
2643            let method_start = search_start + idx;
2644            return SourceLocation { start: method_start, end: method_start + method_name.len() };
2645        }
2646
2647        if let Some(rel_idx) = suffix.find(method_name) {
2648            let method_start = search_start + idx + rel_idx;
2649            return SourceLocation { start: method_start, end: method_start + method_name.len() };
2650        }
2651
2652        call_node.location
2653    }
2654
2655    /// Extract a block of line comments immediately preceding a declaration
2656    fn extract_leading_comment(&self, start: usize) -> Option<String> {
2657        if self.source.is_empty() || start == 0 {
2658            return None;
2659        }
2660        let mut end = start.min(self.source.len());
2661        let bytes = self.source.as_bytes();
2662        // Trim all preceding whitespace, including newlines, to find the real end of comments.
2663        while end > 0 && bytes[end - 1].is_ascii_whitespace() {
2664            end -= 1;
2665        }
2666
2667        // Ensure we don't break UTF-8 sequences by finding the nearest char boundary
2668        while end > 0 && !self.source.is_char_boundary(end) {
2669            end -= 1;
2670        }
2671
2672        let prefix = &self.source[..end];
2673        let mut lines = prefix.lines().rev();
2674        let mut docs = Vec::new();
2675        for line in &mut lines {
2676            let trimmed = line.trim_start();
2677            if trimmed.starts_with('#') {
2678                // Optimize: avoid string allocation by using string slice references
2679                let content = trimmed.trim_start_matches('#').trim_start();
2680                docs.push(content);
2681            } else {
2682                // Stop at any non-comment line (including empty lines).
2683                break;
2684            }
2685        }
2686        if docs.is_empty() {
2687            None
2688        } else {
2689            docs.reverse();
2690            // Optimize: pre-calculate capacity to avoid reallocations
2691            let total_len: usize =
2692                docs.iter().map(|s| s.len()).sum::<usize>() + docs.len().saturating_sub(1);
2693            let mut result = String::with_capacity(total_len);
2694            for (i, doc) in docs.iter().enumerate() {
2695                if i > 0 {
2696                    result.push('\n');
2697                }
2698                result.push_str(doc);
2699            }
2700            Some(result)
2701        }
2702    }
2703
2704    /// Extract documentation for a package declaration.
2705    ///
2706    /// Looks for:
2707    /// 1. A POD `=head1 NAME` section that mentions the package name
2708    /// 2. Leading comments immediately before the `package` statement
2709    /// 3. An `=head1 DESCRIPTION` section as fallback
2710    fn extract_package_documentation(
2711        &self,
2712        package_name: &str,
2713        location: SourceLocation,
2714    ) -> Option<String> {
2715        // First try leading comments (cheapest check)
2716        let leading = self.extract_leading_comment(location.start);
2717        if leading.is_some() {
2718            return leading;
2719        }
2720
2721        // Then search for POD NAME section in the source text
2722        if self.source.is_empty() {
2723            return None;
2724        }
2725
2726        // Look for =head1 NAME section anywhere in the file
2727        let mut in_name_section = false;
2728        let mut name_lines: Vec<&str> = Vec::new();
2729
2730        for line in self.source.lines() {
2731            let trimmed = line.trim();
2732            if trimmed.starts_with("=head1") {
2733                if in_name_section {
2734                    // We hit the next =head1, stop collecting
2735                    break;
2736                }
2737                let heading = trimmed.strip_prefix("=head1").map(|s| s.trim());
2738                if heading == Some("NAME") {
2739                    in_name_section = true;
2740                    continue;
2741                }
2742            } else if trimmed.starts_with("=cut") && in_name_section {
2743                break;
2744            } else if trimmed.starts_with('=') && in_name_section {
2745                // Any other POD directive ends the NAME section
2746                break;
2747            } else if in_name_section && !trimmed.is_empty() {
2748                name_lines.push(trimmed);
2749            }
2750        }
2751
2752        if !name_lines.is_empty() {
2753            let name_doc = name_lines.join(" ");
2754            // Only return if the NAME section actually references this package
2755            if name_doc.contains(package_name)
2756                || name_doc.contains(&package_name.replace("::", "-"))
2757            {
2758                return Some(name_doc);
2759            }
2760        }
2761
2762        None
2763    }
2764
2765    /// Register signature parameters as implicit `my` variable declarations in the current scope.
2766    ///
2767    /// Handles `MandatoryParameter`, `OptionalParameter`, `SlurpyParameter`, and
2768    /// `NamedParameter` nodes by extracting the inner variable and registering it
2769    /// exactly as if the user had written `my $x` at the top of the subroutine body.
2770    fn register_signature_params(&mut self, sig: &Node) {
2771        let NodeKind::Signature { parameters } = &sig.kind else {
2772            return;
2773        };
2774        for param in parameters {
2775            let variable = match &param.kind {
2776                NodeKind::MandatoryParameter { variable } => variable.as_ref(),
2777                NodeKind::OptionalParameter { variable, .. } => variable.as_ref(),
2778                NodeKind::SlurpyParameter { variable } => variable.as_ref(),
2779                NodeKind::NamedParameter { variable } => variable.as_ref(),
2780                // Unexpected node kind inside a signature — skip gracefully
2781                _ => continue,
2782            };
2783            self.handle_variable_declaration("my", variable, &[], variable.location, None);
2784        }
2785    }
2786
2787    /// Handle variable declaration
2788    fn handle_variable_declaration(
2789        &mut self,
2790        declarator: &str,
2791        variable: &Node,
2792        attributes: &[String],
2793        location: SourceLocation,
2794        documentation: Option<String>,
2795    ) {
2796        if let NodeKind::Variable { sigil, name } = &variable.kind {
2797            let kind = match sigil.as_str() {
2798                "$" => SymbolKind::scalar(),
2799                "@" => SymbolKind::array(),
2800                "%" => SymbolKind::hash(),
2801                _ => return,
2802            };
2803
2804            let symbol = Symbol {
2805                name: name.clone(),
2806                qualified_name: if declarator == "our" {
2807                    format!("{}::{}", self.table.current_package, name)
2808                } else {
2809                    name.clone()
2810                },
2811                kind,
2812                location,
2813                scope_id: self.table.current_scope(),
2814                declaration: Some(declarator.to_string()),
2815                documentation,
2816                attributes: attributes.to_vec(),
2817            };
2818
2819            self.table.add_symbol(symbol);
2820        }
2821    }
2822
2823    fn try_extract_const_fast_declaration(&mut self, args: &[Node]) -> bool {
2824        let mut matched = false;
2825
2826        for arg in args {
2827            match &arg.kind {
2828                NodeKind::VariableDeclaration { declarator, variable, .. } => {
2829                    if self.add_constant_wrapper_symbol(
2830                        variable,
2831                        &[],
2832                        declarator,
2833                        "const",
2834                        "Const::Fast read-only variable",
2835                    ) {
2836                        matched = true;
2837                    }
2838                }
2839                NodeKind::VariableListDeclaration { declarator, variables, attributes, .. } => {
2840                    let mut saw_decl = false;
2841                    for variable in variables {
2842                        if self.add_constant_wrapper_symbol(
2843                            variable,
2844                            attributes,
2845                            declarator,
2846                            "const",
2847                            "Const::Fast read-only variable",
2848                        ) {
2849                            saw_decl = true;
2850                        }
2851                    }
2852                    matched |= saw_decl;
2853                }
2854                _ => self.visit_node(arg),
2855            }
2856        }
2857
2858        matched
2859    }
2860
2861    fn try_extract_readonly_declaration(&mut self, args: &[Node]) -> bool {
2862        let mut matched = false;
2863
2864        for arg in args {
2865            match &arg.kind {
2866                NodeKind::VariableDeclaration { declarator, variable, attributes, .. } => {
2867                    if self.add_constant_wrapper_symbol(
2868                        variable,
2869                        attributes,
2870                        declarator,
2871                        "Readonly",
2872                        "Readonly read-only variable",
2873                    ) {
2874                        matched = true;
2875                    }
2876                }
2877                NodeKind::VariableListDeclaration { declarator, variables, attributes, .. } => {
2878                    let mut saw_decl = false;
2879                    for variable in variables {
2880                        if self.add_constant_wrapper_symbol(
2881                            variable,
2882                            attributes,
2883                            declarator,
2884                            "Readonly",
2885                            "Readonly read-only variable",
2886                        ) {
2887                            saw_decl = true;
2888                        }
2889                    }
2890                    matched |= saw_decl;
2891                }
2892                _ => self.visit_node(arg),
2893            }
2894        }
2895
2896        matched
2897    }
2898
2899    fn add_constant_wrapper_symbol(
2900        &mut self,
2901        variable: &Node,
2902        attributes: &[String],
2903        scope_declarator: &str,
2904        declarator: &str,
2905        documentation: &str,
2906    ) -> bool {
2907        match &variable.kind {
2908            NodeKind::Variable { name, .. } => {
2909                self.table.add_symbol(Symbol {
2910                    name: name.clone(),
2911                    qualified_name: if scope_declarator == "our" {
2912                        format!("{}::{}", self.table.current_package, name)
2913                    } else {
2914                        name.clone()
2915                    },
2916                    kind: SymbolKind::Constant,
2917                    location: variable.location,
2918                    scope_id: self.table.current_scope(),
2919                    declaration: Some(declarator.to_string()),
2920                    documentation: Some(documentation.to_string()),
2921                    attributes: attributes.to_vec(),
2922                });
2923                true
2924            }
2925            NodeKind::VariableWithAttributes { variable, attributes: inner_attributes } => {
2926                let mut merged = attributes.to_vec();
2927                merged.extend(inner_attributes.iter().cloned());
2928                self.add_constant_wrapper_symbol(
2929                    variable,
2930                    &merged,
2931                    scope_declarator,
2932                    declarator,
2933                    documentation,
2934                )
2935            }
2936            _ => false,
2937        }
2938    }
2939
2940    fn synthesize_use_constant_symbols(&mut self, args: &[String], location: SourceLocation) {
2941        let constant_names = extract_constant_names_from_use_args(args);
2942        for name in constant_names {
2943            self.table.add_symbol(Symbol {
2944                name: name.clone(),
2945                qualified_name: format!("{}::{}", self.table.current_package, name),
2946                kind: SymbolKind::Constant,
2947                location,
2948                scope_id: self.table.current_scope(),
2949                declaration: Some("constant".to_string()),
2950                documentation: Some("use constant declaration".to_string()),
2951                attributes: vec![],
2952            });
2953        }
2954    }
2955
2956    fn register_catch_variable(&mut self, full_name: &str, catch_block_location: SourceLocation) {
2957        let (sigil, name) = split_variable_name(full_name);
2958        let kind = match sigil {
2959            "$" => SymbolKind::scalar(),
2960            "@" => SymbolKind::array(),
2961            "%" => SymbolKind::hash(),
2962            _ => return,
2963        };
2964        if name.is_empty() || name.contains("::") {
2965            return;
2966        }
2967
2968        let location = self
2969            .find_catch_variable_location(catch_block_location.start, full_name)
2970            .unwrap_or(SourceLocation {
2971                start: catch_block_location.start,
2972                end: catch_block_location.start,
2973            });
2974
2975        self.table.add_symbol(Symbol {
2976            name: name.to_string(),
2977            qualified_name: name.to_string(),
2978            kind,
2979            location,
2980            scope_id: self.table.current_scope(),
2981            declaration: Some("my".to_string()),
2982            documentation: Some("Exception variable bound by catch".to_string()),
2983            attributes: vec![],
2984        });
2985    }
2986
2987    fn find_catch_variable_location(
2988        &self,
2989        catch_body_start: usize,
2990        full_name: &str,
2991    ) -> Option<SourceLocation> {
2992        if self.source.is_empty()
2993            || full_name.is_empty()
2994            || catch_body_start == 0
2995            || catch_body_start > self.source.len()
2996        {
2997            return None;
2998        }
2999
3000        let window_start = catch_body_start.saturating_sub(256);
3001        let window = self.source.get(window_start..catch_body_start)?;
3002        let catch_start = window.rfind("catch")?;
3003        let search_start = catch_start + "catch".len();
3004        let var_offset = window[search_start..].rfind(full_name)? + search_start;
3005        let start = window_start + var_offset;
3006        let end = start + full_name.len();
3007
3008        Some(SourceLocation { start, end })
3009    }
3010
3011    /// Mark a node as a write reference (used in assignments)
3012    fn mark_write_reference(&mut self, node: &Node) {
3013        // This is a simplified version - in practice we'd need to handle
3014        // more complex LHS patterns like array/hash subscripts
3015        if let NodeKind::Variable { .. } = &node.kind {
3016            // The reference will be marked as write when we visit it
3017            // This would require passing context down through visit_node
3018        }
3019    }
3020
3021    /// Extract variable references from an interpolated string
3022    fn extract_vars_from_string(&mut self, value: &str, string_location: SourceLocation) {
3023        static SCALAR_RE: OnceLock<Result<Regex, regex::Error>> = OnceLock::new();
3024
3025        // Simple regex to find scalar variables in strings
3026        // This handles $var, ${var}, but not arrays/hashes for now
3027        let scalar_re = match SCALAR_RE
3028            .get_or_init(|| {
3029                Regex::new(
3030                    r"\$((?:[a-zA-Z_]\w*(?:::[a-zA-Z_]\w*)*)|\{(?:[a-zA-Z_]\w*(?:::[a-zA-Z_]\w*)*)\})",
3031                )
3032            })
3033            .as_ref()
3034        {
3035            Ok(re) => re,
3036            Err(_) => return, // Skip variable extraction if regex fails
3037        };
3038
3039        // The value includes quotes, so strip them
3040        let content = if value.len() >= 2 { &value[1..value.len() - 1] } else { value };
3041
3042        for cap in scalar_re.captures_iter(content) {
3043            if let Some(m) = cap.get(0) {
3044                let var_name = if m.as_str().starts_with("${") && m.as_str().ends_with("}") {
3045                    // Handle ${var} format
3046                    &m.as_str()[2..m.as_str().len() - 1]
3047                } else {
3048                    // Handle $var format
3049                    &m.as_str()[1..]
3050                };
3051
3052                // Calculate the location within the original string
3053                // This is approximate - in the actual string location
3054                let start_offset = string_location.start + 1 + m.start(); // +1 for opening quote
3055                let end_offset = start_offset + m.len();
3056
3057                let reference = SymbolReference {
3058                    name: var_name.to_string(),
3059                    kind: SymbolKind::scalar(),
3060                    location: SourceLocation { start: start_offset, end: end_offset },
3061                    scope_id: self.table.current_scope(),
3062                    is_write: false,
3063                };
3064
3065                self.table.add_reference(reference);
3066            }
3067        }
3068    }
3069}
3070
3071fn split_variable_name(full_name: &str) -> (&str, &str) {
3072    full_name
3073        .char_indices()
3074        .next()
3075        .map(|(idx, ch)| (&full_name[idx..idx + ch.len_utf8()], &full_name[idx + ch.len_utf8()..]))
3076        .unwrap_or(("", ""))
3077}
3078
3079/// Extract constant names from `NodeKind::Use { module: "constant", args, .. }`.
3080fn extract_constant_names_from_use_args(args: &[String]) -> Vec<String> {
3081    fn push_unique(names: &mut Vec<String>, seen: &mut HashSet<String>, candidate: &str) {
3082        if seen.insert(candidate.to_string()) {
3083            names.push(candidate.to_string());
3084        }
3085    }
3086
3087    fn normalize_constant_name(token: &str) -> Option<&str> {
3088        let stripped = token.trim_matches(|c: char| {
3089            matches!(c, '\'' | '"' | '(' | ')' | '[' | ']' | '{' | '}' | ',' | ';')
3090        });
3091        if stripped.is_empty() || stripped.starts_with('-') {
3092            return None;
3093        }
3094        stripped.chars().all(|c| c.is_alphanumeric() || c == '_').then_some(stripped)
3095    }
3096
3097    let mut names = Vec::new();
3098    let mut seen = HashSet::new();
3099    let Some(first) = args.first().map(String::as_str) else {
3100        return names;
3101    };
3102
3103    if first.starts_with("qw") {
3104        let (qw_words, remainder) = extract_qw_words(first);
3105        if remainder.trim().is_empty() {
3106            for word in qw_words {
3107                if let Some(candidate) = normalize_constant_name(&word) {
3108                    push_unique(&mut names, &mut seen, candidate);
3109                }
3110            }
3111            return names;
3112        }
3113
3114        let content = first.trim_start_matches("qw").trim_start();
3115        let content = content
3116            .trim_start_matches(|c: char| "([{/<|!".contains(c))
3117            .trim_end_matches(|c: char| ")]}/|!>".contains(c));
3118        for word in content.split_whitespace() {
3119            if let Some(candidate) = normalize_constant_name(word) {
3120                push_unique(&mut names, &mut seen, candidate);
3121            }
3122        }
3123        return names;
3124    }
3125
3126    let starts_hash_form = first == "{"
3127        || first == "+{"
3128        || (first == "+" && args.get(1).map(String::as_str) == Some("{"));
3129    if starts_hash_form {
3130        let mut skipped_leading_plus = false;
3131        let mut iter = args.iter().peekable();
3132        while let Some(arg) = iter.next() {
3133            if arg == "+{" {
3134                skipped_leading_plus = true;
3135                continue;
3136            }
3137            if arg == "+" && !skipped_leading_plus {
3138                skipped_leading_plus = true;
3139                continue;
3140            }
3141            if arg == "{" || arg == "}" || arg == "," || arg == "=>" {
3142                continue;
3143            }
3144            if let Some(candidate) = normalize_constant_name(arg)
3145                && iter.peek().map(|s| s.as_str()) == Some("=>")
3146            {
3147                push_unique(&mut names, &mut seen, candidate);
3148            }
3149        }
3150        return names;
3151    }
3152
3153    if let Some(candidate) = normalize_constant_name(first) {
3154        push_unique(&mut names, &mut seen, candidate);
3155    }
3156
3157    names
3158}
3159
3160fn extract_qw_words(input: &str) -> (Vec<String>, String) {
3161    let chars: Vec<char> = input.chars().collect();
3162    let mut i = 0;
3163    let mut words = Vec::new();
3164    let mut remainder = String::new();
3165
3166    while i < chars.len() {
3167        if chars[i] == 'q'
3168            && i + 1 < chars.len()
3169            && chars[i + 1] == 'w'
3170            && (i == 0 || !chars[i - 1].is_alphanumeric())
3171        {
3172            let mut j = i + 2;
3173            while j < chars.len() && chars[j].is_whitespace() {
3174                j += 1;
3175            }
3176            if j >= chars.len() {
3177                remainder.push(chars[i]);
3178                i += 1;
3179                continue;
3180            }
3181
3182            let open = chars[j];
3183            let (close, is_paired_delimiter) = match open {
3184                '(' => (')', true),
3185                '[' => (']', true),
3186                '{' => ('}', true),
3187                '<' => ('>', true),
3188                _ => (open, false),
3189            };
3190            if open.is_alphanumeric() || open == '_' || open == '\'' || open == '"' {
3191                remainder.push(chars[i]);
3192                i += 1;
3193                continue;
3194            }
3195
3196            let mut k = j + 1;
3197            if is_paired_delimiter {
3198                let mut depth = 1usize;
3199                while k < chars.len() && depth > 0 {
3200                    if chars[k] == open {
3201                        depth += 1;
3202                    } else if chars[k] == close {
3203                        depth -= 1;
3204                    }
3205                    k += 1;
3206                }
3207                if depth != 0 {
3208                    remainder.extend(chars[i..].iter());
3209                    break;
3210                }
3211                k -= 1;
3212            } else {
3213                while k < chars.len() && chars[k] != close {
3214                    k += 1;
3215                }
3216                if k >= chars.len() {
3217                    remainder.extend(chars[i..].iter());
3218                    break;
3219                }
3220            }
3221
3222            let content: String = chars[j + 1..k].iter().collect();
3223            for word in content.split_whitespace() {
3224                if !word.is_empty() {
3225                    words.push(word.to_string());
3226                }
3227            }
3228            i = k + 1;
3229            continue;
3230        }
3231
3232        remainder.push(chars[i]);
3233        i += 1;
3234    }
3235
3236    (words, remainder)
3237}
3238
3239#[cfg(test)]
3240mod tests {
3241    use super::*;
3242    use crate::parser::Parser;
3243    use perl_tdd_support::{must, must_some};
3244
3245    #[test]
3246    fn test_symbol_extraction() {
3247        let code = r#"
3248package Foo;
3249
3250my $x = 42;
3251our $y = "hello";
3252
3253sub bar {
3254    my $z = $x + $y;
3255    return $z;
3256}
3257"#;
3258
3259        let mut parser = Parser::new(code);
3260        let ast = must(parser.parse());
3261
3262        let extractor = SymbolExtractor::new_with_source(code);
3263        let table = extractor.extract(&ast);
3264
3265        // Check package symbol
3266        assert!(table.symbols.contains_key("Foo"));
3267        let foo_symbols = &table.symbols["Foo"];
3268        assert_eq!(foo_symbols.len(), 1);
3269        assert_eq!(foo_symbols[0].kind, SymbolKind::Package);
3270
3271        // Check variable symbols
3272        assert!(table.symbols.contains_key("x"));
3273        assert!(table.symbols.contains_key("y"));
3274        assert!(table.symbols.contains_key("z"));
3275
3276        // Check subroutine symbol
3277        assert!(table.symbols.contains_key("bar"));
3278        let bar_symbols = &table.symbols["bar"];
3279        assert_eq!(bar_symbols.len(), 1);
3280        assert_eq!(bar_symbols[0].kind, SymbolKind::Subroutine);
3281    }
3282
3283    // ── Bug 3 test: NodeKind::Method uses SymbolKind::Method not Subroutine ──
3284
3285    #[test]
3286    fn test_method_node_uses_symbol_kind_method() {
3287        let code = r#"
3288class MyClass {
3289    method greet {
3290        return "hello";
3291    }
3292}
3293"#;
3294        let mut parser = Parser::new(code);
3295        let ast = must(parser.parse());
3296
3297        let extractor = SymbolExtractor::new_with_source(code);
3298        let table = extractor.extract(&ast);
3299
3300        assert!(table.symbols.contains_key("greet"), "expected 'greet' in symbol table");
3301        let greet_symbols = &table.symbols["greet"];
3302        assert_eq!(greet_symbols.len(), 1);
3303        assert_eq!(
3304            greet_symbols[0].kind,
3305            SymbolKind::Method,
3306            "NodeKind::Method should produce SymbolKind::Method, not Subroutine"
3307        );
3308        // Also verify the method attribute was pushed
3309        assert!(
3310            greet_symbols[0].attributes.contains(&"method".to_string()),
3311            "method symbol should have 'method' attribute"
3312        );
3313    }
3314
3315    // ── Issue #3361: signature parameters added to symbol table ──
3316
3317    #[test]
3318    fn test_subroutine_mandatory_params_in_symbol_table() {
3319        let code = r#"
3320sub foo ($x, $y) {
3321    return $x + $y;
3322}
3323"#;
3324        let mut parser = Parser::new(code);
3325        let ast = must(parser.parse());
3326
3327        let extractor = SymbolExtractor::new_with_source(code);
3328        let table = extractor.extract(&ast);
3329
3330        assert!(
3331            table.symbols.contains_key("x"),
3332            "mandatory parameter $x should be in the symbol table"
3333        );
3334        assert!(
3335            table.symbols.contains_key("y"),
3336            "mandatory parameter $y should be in the symbol table"
3337        );
3338
3339        let x_symbols = &table.symbols["x"];
3340        assert_eq!(x_symbols.len(), 1);
3341        assert_eq!(
3342            x_symbols[0].declaration,
3343            Some("my".to_string()),
3344            "$x should be declared as 'my'"
3345        );
3346
3347        let y_symbols = &table.symbols["y"];
3348        assert_eq!(y_symbols.len(), 1);
3349        assert_eq!(
3350            y_symbols[0].declaration,
3351            Some("my".to_string()),
3352            "$y should be declared as 'my'"
3353        );
3354    }
3355
3356    #[test]
3357    fn test_subroutine_optional_param_in_symbol_table() {
3358        let code = r#"
3359sub bar ($x, $y = 0) {
3360    return $x + $y;
3361}
3362"#;
3363        let mut parser = Parser::new(code);
3364        let ast = must(parser.parse());
3365
3366        let extractor = SymbolExtractor::new_with_source(code);
3367        let table = extractor.extract(&ast);
3368
3369        assert!(
3370            table.symbols.contains_key("x"),
3371            "mandatory parameter $x should be in the symbol table"
3372        );
3373        assert!(
3374            table.symbols.contains_key("y"),
3375            "optional parameter $y should be in the symbol table"
3376        );
3377        assert_eq!(
3378            table.symbols["y"][0].declaration,
3379            Some("my".to_string()),
3380            "optional parameter $y should be declared as 'my'"
3381        );
3382    }
3383
3384    #[test]
3385    fn test_subroutine_slurpy_param_in_symbol_table() {
3386        let code = r#"
3387sub baz ($x, @rest) {
3388    return scalar @rest;
3389}
3390"#;
3391        let mut parser = Parser::new(code);
3392        let ast = must(parser.parse());
3393
3394        let extractor = SymbolExtractor::new_with_source(code);
3395        let table = extractor.extract(&ast);
3396
3397        assert!(
3398            table.symbols.contains_key("x"),
3399            "mandatory parameter $x should be in the symbol table"
3400        );
3401        assert!(
3402            table.symbols.contains_key("rest"),
3403            "slurpy parameter @rest should be in the symbol table"
3404        );
3405        assert_eq!(
3406            table.symbols["rest"][0].declaration,
3407            Some("my".to_string()),
3408            "slurpy parameter @rest should be declared as 'my'"
3409        );
3410    }
3411
3412    #[test]
3413    fn test_method_signature_params_in_symbol_table() {
3414        let code = r#"
3415class Foo {
3416    method greet ($name) {
3417        return $name;
3418    }
3419}
3420"#;
3421        let mut parser = Parser::new(code);
3422        let ast = must(parser.parse());
3423
3424        let extractor = SymbolExtractor::new_with_source(code);
3425        let table = extractor.extract(&ast);
3426
3427        assert!(
3428            table.symbols.contains_key("name"),
3429            "method signature parameter $name should be in the symbol table"
3430        );
3431        assert_eq!(
3432            table.symbols["name"][0].declaration,
3433            Some("my".to_string()),
3434            "method parameter $name should be declared as 'my'"
3435        );
3436    }
3437
3438    #[test]
3439    fn test_empty_signature_no_crash() {
3440        // Edge case: empty signature `sub foo () { }` — should not crash and
3441        // should leave the symbol table with only the sub itself, not any param.
3442        let code = r#"
3443sub foo () {
3444    return 1;
3445}
3446"#;
3447        let mut parser = Parser::new(code);
3448        let ast = must(parser.parse());
3449
3450        let extractor = SymbolExtractor::new_with_source(code);
3451        let table = extractor.extract(&ast);
3452
3453        // Sub `foo` is registered as a symbol
3454        assert!(table.symbols.contains_key("foo"), "sub foo should be in the symbol table");
3455        // No spurious variable symbols from an empty signature
3456        assert_eq!(
3457            table.symbols.len(),
3458            1,
3459            "only 'foo' should be in the symbol table for an empty-signature sub"
3460        );
3461    }
3462
3463    #[test]
3464    fn test_hash_slurpy_param_in_symbol_table() {
3465        // Edge case: hash slurpy `%opts` — sigil % maps to SymbolKind::hash()
3466        let code = r#"
3467sub configure ($x, %opts) {
3468    return $opts{key};
3469}
3470"#;
3471        let mut parser = Parser::new(code);
3472        let ast = must(parser.parse());
3473
3474        let extractor = SymbolExtractor::new_with_source(code);
3475        let table = extractor.extract(&ast);
3476
3477        assert!(
3478            table.symbols.contains_key("opts"),
3479            "hash slurpy parameter %opts should be in the symbol table"
3480        );
3481        assert_eq!(
3482            table.symbols["opts"][0].declaration,
3483            Some("my".to_string()),
3484            "hash slurpy parameter %opts should be declared as 'my'"
3485        );
3486    }
3487
3488    #[test]
3489    fn test_optional_param_location_is_variable_span() {
3490        // The symbol location for an optional param `$y = 0` should span just
3491        // the variable `$y`, not the entire `$y = 0` expression.  Callers like
3492        // go-to-definition use this span to highlight the declaration site.
3493        let code = "sub bar ($x, $y = 0) { $x + $y }";
3494        let mut parser = Parser::new(code);
3495        let ast = must(parser.parse());
3496
3497        let extractor = SymbolExtractor::new_with_source(code);
3498        let table = extractor.extract(&ast);
3499
3500        // `$y` starts at offset 13 in "sub bar ($x, $y = 0)"
3501        //                                            ^ offset 13
3502        let y_sym = &table.symbols["y"][0];
3503        let span_len = y_sym.location.end - y_sym.location.start;
3504        // The variable node "$y" is 2 bytes; the full param "$y = 0" is 6 bytes.
3505        assert_eq!(
3506            span_len, 2,
3507            "symbol location should cover just '$y' (2 chars), not the full '$y = 0' (6 chars)"
3508        );
3509    }
3510
3511    #[test]
3512    fn test_goto_label_creates_label_reference() {
3513        let code = r#"
3514sub run {
3515    goto FINISH;
3516FINISH:
3517    return 1;
3518}
3519"#;
3520        let mut parser = Parser::new(code);
3521        let ast = must(parser.parse());
3522
3523        let extractor = SymbolExtractor::new_with_source(code);
3524        let table = extractor.extract(&ast);
3525        let references = must_some(table.references.get("FINISH"));
3526
3527        assert!(
3528            references.iter().any(|reference| reference.kind == SymbolKind::Label),
3529            "goto FINISH should produce a label reference"
3530        );
3531    }
3532
3533    #[test]
3534    fn test_goto_ampersand_creates_subroutine_reference() {
3535        let code = r#"
3536sub target { return 42; }
3537sub jump {
3538    goto &target;
3539}
3540"#;
3541        let mut parser = Parser::new(code);
3542        let ast = must(parser.parse());
3543
3544        let extractor = SymbolExtractor::new_with_source(code);
3545        let table = extractor.extract(&ast);
3546        let references = must_some(table.references.get("target"));
3547
3548        assert!(
3549            references.iter().any(|reference| reference.kind == SymbolKind::Subroutine),
3550            "goto &target should produce a subroutine reference"
3551        );
3552    }
3553}