Skip to main content

perl_semantic_analyzer/analysis/semantic/
mod.rs

1//! Semantic analysis for IDE features.
2//!
3//! This module provides semantic analysis on top of the symbol table,
4//! including semantic tokens for syntax highlighting, hover information,
5//! and code intelligence features.
6//!
7//! ## Module layout
8//!
9//! | Sub-module | Contents |
10//! |------------|----------|
11//! | `tokens`   | `SemanticTokenType`, `SemanticTokenModifier`, `SemanticToken` |
12//! | `hover`    | `HoverInfo` |
13//! | `builtins` | `BuiltinDoc`, `get_builtin_documentation`, classification helpers |
14//! | `node_analysis` | `impl SemanticAnalyzer` — AST traversal and source helpers |
15//! | `references`    | `impl SemanticAnalyzer` — reference resolution, `find_all_references` |
16//! | `model`    | `SemanticModel` — stable, query-oriented LSP facade |
17
18mod builtins;
19mod exporter_metadata;
20mod hover;
21mod model;
22mod node_analysis;
23mod query_facade;
24mod references;
25mod tokens;
26
27// Public re-exports — downstream consumers see exactly the same surface.
28pub use builtins::{
29    BuiltinDoc, ExceptionContext, PragmaDoc, get_attribute_documentation,
30    get_builtin_documentation, get_exception_context, get_moose_type_documentation,
31    get_operator_documentation, get_pragma_documentation, is_exception_function,
32};
33pub use exporter_metadata::{ExportedSubroutine, FileExportMetadata, PackageExportMetadata};
34pub use hover::HoverInfo;
35pub use model::SemanticModel;
36pub use query_facade::{
37    DefinitionLocation, EffectivePragmaState, ParentChain, ResolvedSymbol, SemanticQueryFacade,
38    VisibleImport,
39};
40pub use tokens::{SemanticToken, SemanticTokenModifier, SemanticTokenType};
41
42use crate::SourceLocation;
43use crate::analysis::class_model::{ClassModel, ClassModelBuilder, MethodResolutionOrder};
44use crate::analysis::generated_member_extractor::GeneratedMemberExtractor;
45use crate::analysis::package_graph_extractor::PackageGraphExtractor;
46use crate::ast::Node;
47use crate::symbol::{Symbol, SymbolExtractor, SymbolTable, is_universal_method};
48use perl_semantic_facts::{FileId, GeneratedMember, PackageEdge};
49use std::collections::{HashMap, HashSet};
50
51const MAX_MRO_TRAVERSAL_DEPTH: usize = 1024;
52
53#[derive(Debug)]
54/// Semantic analyzer providing comprehensive IDE features for Perl code.
55///
56/// Central component for LSP semantic analysis, combining symbol table
57/// construction, semantic token generation, and hover information extraction
58/// with enterprise-grade performance characteristics.
59///
60/// # Performance Characteristics
61/// - Analysis time: O(n) where n is AST node count
62/// - Memory usage: ~1MB per 10K lines of Perl code
63/// - Incremental updates: ≤1ms for typical changes
64/// - Symbol resolution: <50μs average lookup time
65///
66/// # LSP Workflow Integration
67/// Core pipeline component:
68/// 1. **Parse**: AST generation from Perl source
69/// 2. **Index**: Symbol table and semantic token construction
70/// 3. **Navigate**: Symbol resolution for go-to-definition
71/// 4. **Complete**: Context-aware completion suggestions
72/// 5. **Analyze**: Cross-reference analysis and diagnostics
73///
74/// # Perl Language Support
75/// - Full Perl 5 syntax coverage with modern idioms
76/// - Package-qualified symbol resolution
77/// - Lexical scoping with `my`, `our`, `local`, `state`
78/// - Object-oriented method dispatch
79/// - Regular expression and heredoc analysis
80///
81/// # Examples
82///
83/// ```ignore
84/// use perl_parser::{Parser, SemanticAnalyzer};
85///
86/// let code = "my $greeting = 'hello'; sub say_hi { print $greeting; }";
87/// let mut parser = Parser::new(code);
88/// let ast = parser.parse()?;
89///
90/// let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
91/// let symbols = analyzer.symbol_table();
92/// let tokens = analyzer.semantic_tokens();
93/// ```
94pub struct SemanticAnalyzer {
95    /// Symbol table with scope hierarchy and definitions
96    pub(super) symbol_table: SymbolTable,
97    /// Generated semantic tokens for syntax highlighting
98    pub(super) semantic_tokens: Vec<SemanticToken>,
99    /// Hover information cache for symbol details
100    pub(super) hover_info: HashMap<SourceLocation, HoverInfo>,
101    /// Source code for text extraction and analysis
102    pub(super) source: String,
103    /// Class models extracted from the same file (for same-file inheritance resolution)
104    pub class_models: Vec<ClassModel>,
105    /// Per-file Exporter metadata extracted from statically readable assignments.
106    pub export_metadata: FileExportMetadata,
107    /// Package graph edges extracted from inheritance and role composition forms.
108    package_edges: Vec<PackageEdge>,
109    /// Framework-generated members extracted from accessor declarations.
110    generated_members: Vec<GeneratedMember>,
111}
112
113impl SemanticAnalyzer {
114    /// Create a new semantic analyzer from an AST.
115    ///
116    /// Equivalent to [`analyze_with_source`](Self::analyze_with_source) with an empty
117    /// source string. Use this when you only need symbol-table and token analysis
118    /// without source-text-dependent features like hover documentation.
119    ///
120    /// # Examples
121    ///
122    /// ```ignore
123    /// use perl_parser::{Parser, SemanticAnalyzer};
124    ///
125    /// let mut parser = Parser::new("my $x = 42;");
126    /// let ast = parser.parse()?;
127    /// let analyzer = SemanticAnalyzer::analyze(&ast);
128    /// assert!(!analyzer.symbol_table().symbols.is_empty());
129    /// ```
130    pub fn analyze(ast: &Node) -> Self {
131        Self::analyze_with_source(ast, "")
132    }
133
134    /// Create a new semantic analyzer from an AST and source text.
135    ///
136    /// The source text enables richer analysis including hover documentation
137    /// extraction and precise text-range lookups.
138    ///
139    /// # Examples
140    ///
141    /// ```ignore
142    /// use perl_parser::{Parser, SemanticAnalyzer};
143    ///
144    /// let code = "sub greet { print \"Hello\\n\"; }";
145    /// let mut parser = Parser::new(code);
146    /// let ast = parser.parse()?;
147    ///
148    /// let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
149    /// let tokens = analyzer.semantic_tokens();
150    /// // tokens contains semantic highlighting data for the parsed code
151    /// ```
152    pub fn analyze_with_source(ast: &Node, source: &str) -> Self {
153        let symbol_table = SymbolExtractor::new_with_source(source).extract(ast);
154        let class_models = ClassModelBuilder::new().build(ast);
155        let export_metadata = exporter_metadata::ExportMetadataBuilder::new().build(ast);
156        let package_edges = PackageGraphExtractor::extract(ast, FileId(0));
157        let generated_members =
158            GeneratedMemberExtractor::extract_from_models(&class_models, "main");
159
160        let mut analyzer = SemanticAnalyzer {
161            symbol_table,
162            semantic_tokens: Vec::new(),
163            hover_info: HashMap::new(),
164            source: source.to_string(),
165            class_models,
166            export_metadata,
167            package_edges,
168            generated_members,
169        };
170
171        analyzer.analyze_node(ast, 0);
172        analyzer
173    }
174
175    /// Get the symbol table.
176    pub fn symbol_table(&self) -> &SymbolTable {
177        &self.symbol_table
178    }
179
180    /// Get semantic tokens for syntax highlighting.
181    pub fn semantic_tokens(&self) -> &[SemanticToken] {
182        &self.semantic_tokens
183    }
184
185    /// Get per-file Exporter metadata for statically readable exports.
186    pub fn export_metadata(&self) -> &FileExportMetadata {
187        &self.export_metadata
188    }
189
190    /// Get package graph edges extracted from inheritance and role-composition patterns.
191    pub fn package_edges(&self) -> &[PackageEdge] {
192        &self.package_edges
193    }
194
195    /// Get framework-generated members extracted from accessor declarations.
196    pub fn generated_members(&self) -> &[GeneratedMember] {
197        &self.generated_members
198    }
199
200    /// Get hover information at a location for Navigate/Analyze stages.
201    pub fn hover_at(&self, location: SourceLocation) -> Option<&HoverInfo> {
202        self.hover_info.get(&location)
203    }
204
205    /// Iterate over all hover entries collected during analysis.
206    pub fn all_hover_entries(&self) -> impl Iterator<Item = &HoverInfo> {
207        self.hover_info.values()
208    }
209
210    /// Find the symbol at a given location for Navigate workflows.
211    ///
212    /// Returns the most specific (smallest range) symbol that contains the location.
213    /// This ensures that when hovering inside a subroutine body, we return the
214    /// variable at the cursor rather than the enclosing subroutine.
215    pub fn symbol_at(&self, location: SourceLocation) -> Option<&Symbol> {
216        let mut best: Option<&Symbol> = None;
217        let mut best_span = usize::MAX;
218
219        // Search through all symbols for the most specific one at this location
220        for symbols in self.symbol_table.symbols.values() {
221            for symbol in symbols {
222                if symbol.location.start <= location.start && symbol.location.end >= location.end {
223                    let span = symbol.location.end - symbol.location.start;
224                    if span < best_span {
225                        best = Some(symbol);
226                        best_span = span;
227                    }
228                }
229            }
230        }
231        best
232    }
233
234    /// Find the definition of a symbol at a given position for Navigate workflows.
235    pub fn find_definition(&self, position: usize) -> Option<&Symbol> {
236        // First, find if there's a reference at this position
237        for refs in self.symbol_table.references.values() {
238            for reference in refs {
239                if reference.location.start <= position && reference.location.end >= position {
240                    let symbols = self.resolve_reference_to_symbols(reference);
241                    if let Some(first_symbol) = symbols.first() {
242                        return Some(self.resolve_definition_target(first_symbol));
243                    }
244                }
245            }
246        }
247
248        // If no reference found, check if we're on a definition itself
249        self.symbol_at(SourceLocation { start: position, end: position })
250            .map(|symbol| self.resolve_definition_target(symbol))
251    }
252
253    /// Redirect method modifier definitions to the underlying method they modify.
254    ///
255    /// Method modifiers (`before`, `after`, `around`, `override`, `augment`) are modeled as synthetic
256    /// subroutine symbols so hover/navigation can describe them, but go-to-definition
257    /// should land on the real method declaration when it exists.
258    fn resolve_definition_target<'a>(&'a self, symbol: &'a Symbol) -> &'a Symbol {
259        if let Some(target) = self.resolve_method_modifier_target(symbol) { target } else { symbol }
260    }
261
262    /// If `symbol` is a method modifier target, find the underlying method symbol.
263    fn resolve_method_modifier_target<'a>(&'a self, symbol: &'a Symbol) -> Option<&'a Symbol> {
264        if !matches!(
265            symbol.declaration.as_deref(),
266            Some("before" | "after" | "around" | "override" | "augment")
267        ) {
268            return None;
269        }
270
271        self.symbol_table
272            .find_symbol(&symbol.name, symbol.scope_id, crate::symbol::SymbolKind::Subroutine)
273            .into_iter()
274            .find(|candidate| {
275                candidate.location != symbol.location
276                    && !matches!(
277                        candidate.declaration.as_deref(),
278                        Some("before" | "after" | "around" | "override" | "augment")
279                    )
280            })
281    }
282
283    /// Check if an operator is a file test operator.
284    ///
285    /// File test operators in Perl are unary operators that test file properties:
286    /// -e (exists), -d (directory), -f (file), -r (readable), -w (writable), etc.
287    ///
288    /// Delegates to `builtins::is_file_test_operator`.
289    pub fn is_file_test_operator(op: &str) -> bool {
290        builtins::is_file_test_operator(op)
291    }
292
293    /// Resolve hover info for a method by walking the same-file parent chain.
294    ///
295    /// Given a receiver package name and a method name, walks the `parents` of
296    /// each `ClassModel` in `self.class_models` (BFS) and returns `HoverInfo` for
297    /// the first class in the chain that defines the method.
298    ///
299    /// For packages not in `class_models` (plain packages with no OO indicators),
300    /// falls back to the symbol table looking for `PackageName::method_name`.
301    ///
302    /// Returns `None` when no ancestor defined in the same source file
303    /// defines the method. For cross-file inherited-method navigation, the
304    /// LSP layer (`inherited_method_definition_location` in `navigation.rs`)
305    /// handles workspace index traversal - this method is intentionally
306    /// scoped to the current file's `class_models` only.
307    pub fn resolve_inherited_method_hover(
308        &self,
309        receiver_class: &str,
310        method_name: &str,
311    ) -> Option<HoverInfo> {
312        self.resolve_inherited_method_hover_ordered(receiver_class, method_name)
313    }
314
315    /// Resolve the source location of a method inherited from the current class.
316    ///
317    /// This skips the receiver class itself and searches its ancestors in the
318    /// package's configured method-resolution order.
319    pub fn resolve_inherited_method_location(
320        &self,
321        receiver_class: &str,
322        method_name: &str,
323    ) -> Option<SourceLocation> {
324        let models_by_name: HashMap<&str, &ClassModel> =
325            self.class_models.iter().map(|model| (model.name.as_str(), model)).collect();
326
327        let receiver_model = models_by_name.get(receiver_class).copied()?;
328        let ancestor_order = match receiver_model.mro {
329            MethodResolutionOrder::Dfs => self.dfs_ancestor_order(receiver_class, &models_by_name),
330            MethodResolutionOrder::C3 => self.c3_ancestor_order(receiver_class, &models_by_name),
331        };
332
333        for ancestor in ancestor_order {
334            if let Some(model) = models_by_name.get(ancestor.as_str()).copied()
335                && let Some(location) = self.method_location_in_model(model, method_name)
336            {
337                return Some(location);
338            }
339        }
340
341        if is_universal_method(method_name) {
342            return self
343                .symbol_table
344                .symbols
345                .get(method_name)
346                .and_then(|symbols| {
347                    symbols.iter().find(|symbol| {
348                        symbol.kind == crate::symbol::SymbolKind::Subroutine
349                            && symbol.qualified_name == format!("UNIVERSAL::{method_name}")
350                    })
351                })
352                .map(|symbol| symbol.location);
353        }
354
355        None
356    }
357
358    /// Resolve the ordered parent chain for a class in same-file class models.
359    ///
360    /// Returns ancestors in configured method-resolution order, excluding `receiver_class`.
361    pub fn resolve_parent_chain(&self, receiver_class: &str) -> Option<Vec<String>> {
362        let models_by_name: HashMap<&str, &ClassModel> =
363            self.class_models.iter().map(|model| (model.name.as_str(), model)).collect();
364        let receiver_model = models_by_name.get(receiver_class).copied()?;
365
366        let chain = match receiver_model.mro {
367            MethodResolutionOrder::Dfs => self.dfs_ancestor_order(receiver_class, &models_by_name),
368            MethodResolutionOrder::C3 => self.c3_ancestor_order(receiver_class, &models_by_name),
369        };
370        Some(chain)
371    }
372
373    fn resolve_inherited_method_hover_ordered(
374        &self,
375        receiver_class: &str,
376        method_name: &str,
377    ) -> Option<HoverInfo> {
378        let models_by_name: HashMap<&str, &ClassModel> =
379            self.class_models.iter().map(|model| (model.name.as_str(), model)).collect();
380
381        let Some(receiver_model) = models_by_name.get(receiver_class).copied() else {
382            return self.resolve_plain_package_method_hover(receiver_class, method_name);
383        };
384
385        if let Some(hover) =
386            self.hover_for_model_method(receiver_model, receiver_class, method_name)
387        {
388            return Some(hover);
389        }
390
391        let ancestor_order = match receiver_model.mro {
392            MethodResolutionOrder::Dfs => self.dfs_ancestor_order(receiver_class, &models_by_name),
393            MethodResolutionOrder::C3 => self.c3_ancestor_order(receiver_class, &models_by_name),
394        };
395
396        for ancestor in ancestor_order {
397            if let Some(model) = models_by_name.get(ancestor.as_str()).copied() {
398                if let Some(hover) = self.hover_for_model_method(model, receiver_class, method_name)
399                {
400                    return Some(hover);
401                }
402            } else if let Some(hover) =
403                self.resolve_plain_package_method_hover(&ancestor, method_name)
404            {
405                return Some(hover);
406            }
407        }
408
409        if is_universal_method(method_name) {
410            return Some(HoverInfo {
411                signature: format!("sub UNIVERSAL::{method_name}"),
412                documentation: None,
413                details: vec!["Defined in UNIVERSAL".to_string()],
414            });
415        }
416
417        None
418    }
419
420    fn hover_for_model_method(
421        &self,
422        model: &ClassModel,
423        receiver_class: &str,
424        method_name: &str,
425    ) -> Option<HoverInfo> {
426        if model.methods.iter().any(|m| m.name == method_name) {
427            let is_direct = model.name == receiver_class;
428            let details = if is_direct {
429                vec![format!("Defined in {}", model.name)]
430            } else {
431                vec![format!("Inherited from {}", model.name)]
432            };
433            return Some(HoverInfo {
434                signature: format!("sub {}::{}", model.name, method_name),
435                documentation: None,
436                details,
437            });
438        }
439        if model.methods.iter().any(|m| m.name == "AUTOLOAD") {
440            let is_direct = model.name == receiver_class;
441            let details = if is_direct {
442                vec![
443                    format!("Resolved via AUTOLOAD in {}", model.name),
444                    format!("Requested method: {method_name}"),
445                ]
446            } else {
447                vec![
448                    format!("Resolved via inherited AUTOLOAD from {}", model.name),
449                    format!("Requested method: {method_name}"),
450                ]
451            };
452            return Some(HoverInfo {
453                signature: format!("sub {}::AUTOLOAD", model.name),
454                documentation: None,
455                details,
456            });
457        }
458        None
459    }
460
461    fn method_location_in_model(
462        &self,
463        model: &ClassModel,
464        method_name: &str,
465    ) -> Option<SourceLocation> {
466        model
467            .methods
468            .iter()
469            .find(|method| method.name == method_name)
470            .or_else(|| model.methods.iter().find(|method| method.name == "AUTOLOAD"))
471            .map(|method| method.location)
472    }
473
474    fn resolve_plain_package_method_hover(
475        &self,
476        package_name: &str,
477        method_name: &str,
478    ) -> Option<HoverInfo> {
479        let qualified = format!("{}::{}", package_name, method_name);
480        let found_in_table = self.symbol_table.symbols.get(method_name).is_some_and(|syms| {
481            syms.iter().any(|s| {
482                matches!(s.kind, crate::symbol::SymbolKind::Subroutine)
483                    && s.qualified_name == qualified
484            })
485        }) || self.symbol_table.symbols.contains_key(&qualified);
486
487        if found_in_table {
488            return Some(HoverInfo {
489                signature: format!("sub {}::{}", package_name, method_name),
490                documentation: None,
491                details: vec![format!("Inherited from {}", package_name)],
492            });
493        }
494
495        let qualified_autoload = format!("{}::AUTOLOAD", package_name);
496        let autoload_in_table = self.symbol_table.symbols.get("AUTOLOAD").is_some_and(|syms| {
497            syms.iter().any(|s| {
498                matches!(s.kind, crate::symbol::SymbolKind::Subroutine)
499                    && s.qualified_name == qualified_autoload
500            })
501        }) || self.symbol_table.symbols.contains_key(&qualified_autoload);
502
503        if autoload_in_table {
504            return Some(HoverInfo {
505                signature: format!("sub {}::AUTOLOAD", package_name),
506                documentation: None,
507                details: vec![
508                    format!("Resolved via AUTOLOAD in {}", package_name),
509                    format!("Requested method: {}", method_name),
510                ],
511            });
512        }
513
514        if is_universal_method(method_name) {
515            return Some(HoverInfo {
516                signature: format!("sub UNIVERSAL::{method_name}"),
517                documentation: None,
518                details: vec!["Defined in UNIVERSAL".to_string()],
519            });
520        }
521
522        None
523    }
524
525    fn dfs_ancestor_order(
526        &self,
527        package: &str,
528        models_by_name: &HashMap<&str, &ClassModel>,
529    ) -> Vec<String> {
530        fn walk(
531            package: &str,
532            models_by_name: &HashMap<&str, &ClassModel>,
533            seen: &mut HashSet<String>,
534            out: &mut Vec<String>,
535            depth: usize,
536        ) {
537            if depth >= MAX_MRO_TRAVERSAL_DEPTH {
538                return;
539            }
540
541            let Some(model) = models_by_name.get(package).copied() else {
542                return;
543            };
544
545            for parent in &model.parents {
546                if seen.insert(parent.clone()) {
547                    out.push(parent.clone());
548                    walk(parent, models_by_name, seen, out, depth + 1);
549                }
550            }
551        }
552
553        let mut seen = HashSet::from([package.to_string()]);
554        let mut out = Vec::new();
555        walk(package, models_by_name, &mut seen, &mut out, 0);
556        out
557    }
558
559    fn c3_ancestor_order(
560        &self,
561        package: &str,
562        models_by_name: &HashMap<&str, &ClassModel>,
563    ) -> Vec<String> {
564        fn linearize(
565            package: &str,
566            models_by_name: &HashMap<&str, &ClassModel>,
567            visited: &mut HashSet<String>,
568            depth: usize,
569        ) -> Vec<String> {
570            if depth >= MAX_MRO_TRAVERSAL_DEPTH {
571                return vec![package.to_string()];
572            }
573
574            if !visited.insert(package.to_string()) {
575                return vec![];
576            }
577
578            let Some(model) = models_by_name.get(package).copied() else {
579                return vec![package.to_string()];
580            };
581
582            let parents = model.parents.clone();
583            if parents.is_empty() {
584                return vec![package.to_string()];
585            }
586
587            let mut parent_mros: Vec<Vec<String>> = parents
588                .iter()
589                .map(|parent| linearize(parent, models_by_name, &mut visited.clone(), depth + 1))
590                .collect();
591            parent_mros.push(parents.clone());
592
593            let mut result = vec![package.to_string()];
594            loop {
595                parent_mros.retain(|list| !list.is_empty());
596                if parent_mros.is_empty() {
597                    break;
598                }
599
600                let chosen = parent_mros.iter().find_map(|list| {
601                    let candidate = list.first()?;
602                    let in_tail = parent_mros
603                        .iter()
604                        .any(|other| other.iter().skip(1).any(|name| name == candidate));
605                    if in_tail { None } else { Some(candidate.clone()) }
606                });
607
608                match chosen {
609                    Some(name) => {
610                        if !result.contains(&name) {
611                            result.push(name.clone());
612                        }
613                        for list in &mut parent_mros {
614                            if list.first().is_some_and(|head| head == &name) {
615                                list.remove(0);
616                            }
617                        }
618                    }
619                    None => {
620                        for list in parent_mros {
621                            if let Some(head) = list.first()
622                                && !result.contains(head)
623                            {
624                                result.push(head.clone());
625                            }
626                        }
627                        break;
628                    }
629                }
630            }
631
632            result
633        }
634
635        linearize(package, models_by_name, &mut HashSet::new(), 0).into_iter().skip(1).collect()
636    }
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642    use crate::ast::{Node, NodeKind};
643    use crate::parser::Parser;
644    use crate::symbol::SymbolKind;
645    use perl_semantic_facts::{Confidence, GeneratedMemberKind, PackageEdgeKind, Provenance};
646
647    #[test]
648    fn test_semantic_tokens() -> Result<(), Box<dyn std::error::Error>> {
649        let code = r#"
650my $x = 42;
651print $x;
652"#;
653
654        let mut parser = Parser::new(code);
655        let ast = parser.parse()?;
656
657        let analyzer = SemanticAnalyzer::analyze(&ast);
658        let tokens = analyzer.semantic_tokens();
659
660        // Phase 1 implementation (Issue #188) handles critical AST node types
661        // including VariableListDeclaration, Ternary, ArrayLiteral, HashLiteral,
662        // Try, and PhaseBlock nodes
663
664        // Check first $x is a declaration
665        let x_tokens: Vec<_> = tokens
666            .iter()
667            .filter(|t| {
668                matches!(
669                    t.token_type,
670                    SemanticTokenType::Variable | SemanticTokenType::VariableDeclaration
671                )
672            })
673            .collect();
674        assert!(!x_tokens.is_empty());
675        assert!(x_tokens[0].modifiers.contains(&SemanticTokenModifier::Declaration));
676        Ok(())
677    }
678
679    #[test]
680    fn test_hover_info() -> Result<(), Box<dyn std::error::Error>> {
681        let code = r#"
682sub foo {
683    return 42;
684}
685
686my $result = foo();
687"#;
688
689        let mut parser = Parser::new(code);
690        let ast = parser.parse()?;
691
692        let analyzer = SemanticAnalyzer::analyze(&ast);
693
694        // The hover info would be at specific locations
695        // In practice, we'd look up by position
696        assert!(!analyzer.hover_info.is_empty());
697        Ok(())
698    }
699
700    #[test]
701    fn test_hover_doc_from_pod() -> Result<(), Box<dyn std::error::Error>> {
702        let code = r#"
703# This is foo
704# More docs
705sub foo {
706    return 1;
707}
708"#;
709
710        let mut parser = Parser::new(code);
711        let ast = parser.parse()?;
712
713        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
714
715        // Find the symbol for foo and check its hover documentation
716        let sym = analyzer.symbol_table().symbols.get("foo").ok_or("symbol not found")?[0].clone();
717        let hover = analyzer.hover_at(sym.location).ok_or("hover not found")?;
718        assert!(hover.documentation.as_ref().ok_or("doc not found")?.contains("This is foo"));
719        Ok(())
720    }
721
722    #[test]
723    fn test_comment_doc_extraction() -> Result<(), Box<dyn std::error::Error>> {
724        let code = r#"
725# Adds two numbers
726sub add { 1 }
727"#;
728
729        let mut parser = Parser::new(code);
730        let ast = parser.parse()?;
731
732        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
733
734        let sub_symbols =
735            analyzer.symbol_table().find_symbol("add", 0, crate::symbol::SymbolKind::Subroutine);
736        assert!(!sub_symbols.is_empty());
737        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
738        assert_eq!(hover.documentation.as_deref(), Some("Adds two numbers"));
739        Ok(())
740    }
741
742    #[test]
743    fn test_extract_documentation_with_out_of_bounds_offset()
744    -> Result<(), Box<dyn std::error::Error>> {
745        let code = "sub add { 1 }\n";
746        let mut parser = Parser::new(code);
747        let ast = parser.parse()?;
748        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
749
750        assert_eq!(analyzer.extract_documentation(code.len() + 1), None);
751        Ok(())
752    }
753
754    #[test]
755    fn test_cross_package_navigation() -> Result<(), Box<dyn std::error::Error>> {
756        let code = r#"
757package Foo {
758    # bar sub
759    sub bar { 42 }
760}
761
762package main;
763Foo::bar();
764"#;
765
766        let mut parser = Parser::new(code);
767        let ast = parser.parse()?;
768        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
769        let pos = code.find("Foo::bar").ok_or("Foo::bar not found")? + 5; // position within "bar"
770        let def = analyzer.find_definition(pos).ok_or("definition")?;
771        assert_eq!(def.name, "bar");
772
773        let hover = analyzer.hover_at(def.location).ok_or("hover not found")?;
774        assert!(hover.documentation.as_ref().ok_or("doc not found")?.contains("bar sub"));
775        Ok(())
776    }
777
778    #[test]
779    fn test_universal_method_hover_fallback() -> Result<(), Box<dyn std::error::Error>> {
780        let code = r#"
781package UNIVERSAL;
782sub can { 1 }
783sub isa { 1 }
784
785package Foo;
786sub new { bless {}, shift }
787"#;
788
789        let mut parser = Parser::new(code);
790        let ast = parser.parse()?;
791        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
792
793        let hover = analyzer
794            .resolve_inherited_method_hover("Foo", "can")
795            .ok_or("expected UNIVERSAL hover fallback")?;
796
797        assert!(
798            hover.signature.contains("UNIVERSAL::can"),
799            "expected UNIVERSAL hover signature, got: {}",
800            hover.signature
801        );
802        assert!(
803            hover.details.iter().any(|detail| detail.contains("UNIVERSAL")),
804            "expected UNIVERSAL hover details, got: {:?}",
805            hover.details
806        );
807        Ok(())
808    }
809
810    #[test]
811    fn test_autoload_hover_fallback() -> Result<(), Box<dyn std::error::Error>> {
812        let code = r#"
813package Foo;
814sub AUTOLOAD { 1 }
815"#;
816
817        let mut parser = Parser::new(code);
818        let ast = parser.parse()?;
819        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
820
821        let hover = analyzer
822            .resolve_inherited_method_hover("Foo", "dynamic_method")
823            .ok_or("expected AUTOLOAD hover fallback")?;
824
825        assert!(
826            hover.signature.contains("Foo::AUTOLOAD"),
827            "expected AUTOLOAD hover signature, got: {}",
828            hover.signature
829        );
830        assert!(
831            hover.details.iter().any(|detail| detail.contains("AUTOLOAD")),
832            "expected AUTOLOAD hover details, got: {:?}",
833            hover.details
834        );
835        assert!(
836            hover.details.iter().any(|detail| detail.contains("dynamic_method")),
837            "expected requested method detail, got: {:?}",
838            hover.details
839        );
840        Ok(())
841    }
842
843    #[test]
844    fn test_scope_identification() -> Result<(), Box<dyn std::error::Error>> {
845        let code = r#"
846my $x = 0;
847package Foo {
848    my $x = 1;
849    sub bar { return $x; }
850}
851my $y = $x;
852"#;
853
854        let mut parser = Parser::new(code);
855        let ast = parser.parse()?;
856        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
857
858        let inner_ref_pos = code.find("return $x").ok_or("return $x not found")? + "return ".len();
859        let inner_def = analyzer.find_definition(inner_ref_pos).ok_or("inner def not found")?;
860        let expected_inner = code.find("my $x = 1").ok_or("my $x = 1 not found")? + 3;
861        assert_eq!(inner_def.location.start, expected_inner);
862
863        let outer_ref_pos = code.rfind("$x;").ok_or("$x; not found")?;
864        let outer_def = analyzer.find_definition(outer_ref_pos).ok_or("outer def not found")?;
865        let expected_outer = code.find("my $x = 0").ok_or("my $x = 0 not found")? + 3;
866        assert_eq!(outer_def.location.start, expected_outer);
867        Ok(())
868    }
869
870    #[test]
871    fn test_pod_documentation_extraction() -> Result<(), Box<dyn std::error::Error>> {
872        // Test with a simple case that parses correctly
873        let code = r#"# Simple comment before sub
874sub documented_with_comment {
875    return "test";
876}
877"#;
878
879        let mut parser = Parser::new(code);
880        let ast = parser.parse()?;
881        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
882
883        let sub_symbols = analyzer.symbol_table().find_symbol(
884            "documented_with_comment",
885            0,
886            crate::symbol::SymbolKind::Subroutine,
887        );
888        assert!(!sub_symbols.is_empty());
889        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
890        let doc = hover.documentation.as_ref().ok_or("doc not found")?;
891        assert!(doc.contains("Simple comment before sub"));
892        Ok(())
893    }
894
895    #[test]
896    fn test_empty_source_handling() -> Result<(), Box<dyn std::error::Error>> {
897        let code = "";
898        let mut parser = Parser::new(code);
899        let ast = parser.parse()?;
900        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
901
902        // Should not crash with empty source
903        assert!(analyzer.semantic_tokens().is_empty());
904        assert!(analyzer.hover_info.is_empty());
905        Ok(())
906    }
907
908    #[test]
909    fn test_multiple_comment_lines() -> Result<(), Box<dyn std::error::Error>> {
910        let code = r#"
911# First comment
912# Second comment
913# Third comment
914sub multi_commented {
915    1;
916}
917"#;
918
919        let mut parser = Parser::new(code);
920        let ast = parser.parse()?;
921        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
922
923        let sub_symbols = analyzer.symbol_table().find_symbol(
924            "multi_commented",
925            0,
926            crate::symbol::SymbolKind::Subroutine,
927        );
928        assert!(!sub_symbols.is_empty());
929        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
930        let doc = hover.documentation.as_ref().ok_or("doc not found")?;
931        assert!(doc.contains("First comment"));
932        assert!(doc.contains("Second comment"));
933        assert!(doc.contains("Third comment"));
934        Ok(())
935    }
936
937    // SemanticModel tests
938    #[test]
939    fn test_semantic_model_build_and_tokens() -> Result<(), Box<dyn std::error::Error>> {
940        let code = r#"
941my $x = 42;
942my $y = 10;
943$x + $y;
944"#;
945        let mut parser = Parser::new(code);
946        let ast = parser.parse()?;
947
948        let model = SemanticModel::build(&ast, code);
949
950        // Should have semantic tokens
951        let tokens = model.tokens();
952        assert!(!tokens.is_empty(), "SemanticModel should provide tokens");
953
954        // Should have variable tokens
955        let var_tokens: Vec<_> = tokens
956            .iter()
957            .filter(|t| {
958                matches!(
959                    t.token_type,
960                    SemanticTokenType::Variable | SemanticTokenType::VariableDeclaration
961                )
962            })
963            .collect();
964        assert!(var_tokens.len() >= 2, "Should have at least 2 variable tokens");
965        Ok(())
966    }
967
968    #[test]
969    fn test_semantic_model_symbol_table_access() -> Result<(), Box<dyn std::error::Error>> {
970        let code = r#"
971my $x = 42;
972sub foo {
973    my $y = $x;
974}
975"#;
976        let mut parser = Parser::new(code);
977        let ast = parser.parse()?;
978
979        let model = SemanticModel::build(&ast, code);
980
981        // Should be able to access symbol table
982        let symbol_table = model.symbol_table();
983        let x_symbols = symbol_table.find_symbol("x", 0, SymbolKind::scalar());
984        assert!(!x_symbols.is_empty(), "Should find $x in symbol table");
985
986        let foo_symbols = symbol_table.find_symbol("foo", 0, SymbolKind::Subroutine);
987        assert!(!foo_symbols.is_empty(), "Should find sub foo in symbol table");
988        Ok(())
989    }
990
991    #[test]
992    fn test_semantic_model_package_edges() -> Result<(), Box<dyn std::error::Error>> {
993        let code = r#"
994package Child;
995use parent 'Base';
996use Moose;
997with 'Role';
9981;
999"#;
1000        let mut parser = Parser::new(code);
1001        let ast = parser.parse()?;
1002
1003        let model = SemanticModel::build(&ast, code);
1004        let edges = model.package_edges();
1005
1006        assert!(
1007            edges.iter().any(|edge| {
1008                edge.from_package == "Child"
1009                    && edge.to_package == "Base"
1010                    && edge.kind == PackageEdgeKind::Inherits
1011            }),
1012            "SemanticModel should expose the use-parent package edge, got: {edges:?}"
1013        );
1014        assert!(
1015            edges.iter().any(|edge| {
1016                edge.from_package == "Child"
1017                    && edge.to_package == "Role"
1018                    && edge.kind == PackageEdgeKind::ComposesRole
1019            }),
1020            "SemanticModel should expose the role-composition package edge, got: {edges:?}"
1021        );
1022        Ok(())
1023    }
1024
1025    #[test]
1026    fn test_semantic_model_generated_members() -> Result<(), Box<dyn std::error::Error>> {
1027        let code = r#"
1028package GeneratedExample;
1029use Moo;
1030has 'name' => (is => 'ro');
1031has '+status' => (is => 'rw', predicate => 1);
10321;
1033"#;
1034        let mut parser = Parser::new(code);
1035        let ast = parser.parse()?;
1036
1037        let model = SemanticModel::build(&ast, code);
1038        let members = model.generated_members();
1039
1040        let name = members
1041            .iter()
1042            .find(|member| member.name == "name")
1043            .ok_or("expected generated getter for `name`")?;
1044        assert_eq!(name.kind, GeneratedMemberKind::Getter);
1045        assert_eq!(name.package, "GeneratedExample");
1046        assert_eq!(name.provenance, Provenance::FrameworkSynthesis);
1047        assert_eq!(name.confidence, Confidence::Medium);
1048
1049        assert!(
1050            members.iter().any(|member| {
1051                member.name == "status" && member.kind == GeneratedMemberKind::Accessor
1052            }),
1053            "expected rw status accessor in generated members, got: {members:?}"
1054        );
1055        assert!(
1056            members.iter().any(|member| {
1057                member.name == "has_status" && member.kind == GeneratedMemberKind::Predicate
1058            }),
1059            "expected predicate generated from `+status`, got: {members:?}"
1060        );
1061        assert!(
1062            members.iter().all(|member| !member.name.starts_with('+')),
1063            "generated member names should not retain inherited-attribute `+`: {members:?}"
1064        );
1065        Ok(())
1066    }
1067
1068    #[test]
1069    fn test_semantic_model_hover_info() -> Result<(), Box<dyn std::error::Error>> {
1070        let code = r#"
1071# This is a documented variable
1072my $documented = 42;
1073"#;
1074        let mut parser = Parser::new(code);
1075        let ast = parser.parse()?;
1076
1077        let model = SemanticModel::build(&ast, code);
1078
1079        // Find the location of the variable declaration
1080        let symbol_table = model.symbol_table();
1081        let symbols = symbol_table.find_symbol("documented", 0, SymbolKind::scalar());
1082        assert!(!symbols.is_empty(), "Should find $documented");
1083
1084        // Check if hover info is available
1085        if let Some(hover) = model.hover_info_at(symbols[0].location) {
1086            assert!(hover.signature.contains("documented"), "Hover should contain variable name");
1087        }
1088        // Note: hover_info_at might return None if no explicit hover was generated,
1089        // which is acceptable for now
1090        Ok(())
1091    }
1092
1093    #[test]
1094    fn test_analyzer_find_definition_scalar() -> Result<(), Box<dyn std::error::Error>> {
1095        let code = "my $x = 1;\n$x + 2;\n";
1096        let mut parser = Parser::new(code);
1097        let ast = parser.parse()?;
1098
1099        // Use the same path SemanticModel uses to feed source
1100        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1101
1102        // Find the byte offset of the reference "$x" in the second line
1103        let ref_line = code.lines().nth(1).ok_or("line 2 not found")?;
1104        let line_offset = code.lines().next().ok_or("line 1 not found")?.len() + 1; // +1 for '\n'
1105        let col_in_line = ref_line.find("$x").ok_or("could not find $x on line 2")?;
1106        let ref_pos = line_offset + col_in_line;
1107
1108        let symbol =
1109            analyzer.find_definition(ref_pos).ok_or("definition not found for $x reference")?;
1110
1111        // 1. Must be a scalar named "x"
1112        assert_eq!(symbol.name, "x");
1113        assert_eq!(symbol.kind, SymbolKind::scalar());
1114
1115        // 2. Declaration must come before reference
1116        assert!(
1117            symbol.location.start < ref_pos,
1118            "Declaration {:?} should precede reference at byte {}",
1119            symbol.location.start,
1120            ref_pos
1121        );
1122        Ok(())
1123    }
1124
1125    #[test]
1126    fn test_semantic_model_definition_at() -> Result<(), Box<dyn std::error::Error>> {
1127        let code = "my $x = 1;\n$x + 2;\n";
1128        let mut parser = Parser::new(code);
1129        let ast = parser.parse()?;
1130
1131        let model = SemanticModel::build(&ast, code);
1132
1133        // Compute the byte offset of the reference "$x" on the second line
1134        let ref_line_index = 1;
1135        let ref_line = code.lines().nth(ref_line_index).ok_or("line not found")?;
1136        let col_in_line = ref_line.find("$x").ok_or("could not find $x")?;
1137        let byte_offset = code
1138            .lines()
1139            .take(ref_line_index)
1140            .map(|l| l.len() + 1) // +1 for '\n'
1141            .sum::<usize>()
1142            + col_in_line;
1143
1144        let definition = model.definition_at(byte_offset);
1145        assert!(
1146            definition.is_some(),
1147            "definition_at returned None for $x reference at {}",
1148            byte_offset
1149        );
1150        if let Some(symbol) = definition {
1151            assert_eq!(symbol.name, "x");
1152            assert_eq!(symbol.kind, SymbolKind::scalar());
1153            assert!(
1154                symbol.location.start < byte_offset,
1155                "Declaration {:?} should precede reference at byte {}",
1156                symbol.location.start,
1157                byte_offset
1158            );
1159        }
1160        Ok(())
1161    }
1162
1163    #[test]
1164    fn test_analyzer_find_definition_goto_label() -> Result<(), Box<dyn std::error::Error>> {
1165        let code = "START: while (1) {\n    goto START;\n}\n";
1166        let mut parser = Parser::new(code);
1167        let ast = parser.parse()?;
1168
1169        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1170        let ref_pos = code.find("START;\n").ok_or("could not find goto label")?;
1171
1172        let symbol = analyzer
1173            .find_definition(ref_pos)
1174            .ok_or("definition not found for goto label reference")?;
1175
1176        assert_eq!(symbol.name, "START");
1177        assert_eq!(symbol.kind, SymbolKind::Label);
1178        assert!(
1179            symbol.location.start < ref_pos,
1180            "Label definition {:?} should precede goto reference at byte {}",
1181            symbol.location.start,
1182            ref_pos
1183        );
1184        Ok(())
1185    }
1186
1187    #[test]
1188    fn test_anonymous_subroutine_semantic_tokens() -> Result<(), Box<dyn std::error::Error>> {
1189        let code = r#"
1190my $closure = sub {
1191    my $x = 42;
1192    return $x + 1;
1193};
1194"#;
1195
1196        let mut parser = Parser::new(code);
1197        let ast = parser.parse()?;
1198        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1199
1200        // Check that we have semantic tokens for the anonymous sub
1201        let tokens = analyzer.semantic_tokens();
1202
1203        // Should have a keyword token for 'sub'
1204        let sub_keywords: Vec<_> =
1205            tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Keyword)).collect();
1206
1207        assert!(!sub_keywords.is_empty(), "Should have keyword token for 'sub'");
1208
1209        // Check hover info exists for the anonymous sub
1210        let sub_position = code.find("sub {").ok_or("sub { not found")?;
1211        let hover_exists = analyzer
1212            .hover_info
1213            .iter()
1214            .any(|(loc, _)| loc.start <= sub_position && loc.end >= sub_position);
1215
1216        assert!(hover_exists, "Should have hover info for anonymous subroutine");
1217        Ok(())
1218    }
1219
1220    #[test]
1221    fn test_infer_type_for_literals() -> Result<(), Box<dyn std::error::Error>> {
1222        let code = r#"
1223my $num = 42;
1224my $str = "hello";
1225my @arr = (1, 2, 3);
1226my %hash = (a => 1);
1227"#;
1228
1229        let mut parser = Parser::new(code);
1230        let ast = parser.parse()?;
1231        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1232
1233        // Find nodes and test type inference
1234        // We need to walk the AST to find the literal nodes
1235        fn find_number_node(node: &Node) -> Option<&Node> {
1236            match &node.kind {
1237                NodeKind::Number { .. } => Some(node),
1238                NodeKind::Program { statements } | NodeKind::Block { statements } => {
1239                    for stmt in statements {
1240                        if let Some(found) = find_number_node(stmt) {
1241                            return Some(found);
1242                        }
1243                    }
1244                    None
1245                }
1246                NodeKind::VariableDeclaration { initializer, .. } => {
1247                    initializer.as_ref().and_then(|init| find_number_node(init))
1248                }
1249                _ => None,
1250            }
1251        }
1252
1253        if let Some(num_node) = find_number_node(&ast) {
1254            let inferred = analyzer.infer_type(num_node);
1255            assert_eq!(inferred, Some("number".to_string()), "Should infer number type");
1256        }
1257
1258        Ok(())
1259    }
1260
1261    #[test]
1262    fn test_infer_type_for_binary_operations() -> Result<(), Box<dyn std::error::Error>> {
1263        let code = r#"my $sum = 10 + 20;
1264my $concat = "a" . "b";
1265"#;
1266
1267        let mut parser = Parser::new(code);
1268        let ast = parser.parse()?;
1269        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1270
1271        // Find binary operation nodes
1272        fn find_binary_node<'a>(node: &'a Node, op: &str) -> Option<&'a Node> {
1273            match &node.kind {
1274                NodeKind::Binary { op: node_op, .. } if node_op == op => Some(node),
1275                NodeKind::Program { statements } | NodeKind::Block { statements } => {
1276                    for stmt in statements {
1277                        if let Some(found) = find_binary_node(stmt, op) {
1278                            return Some(found);
1279                        }
1280                    }
1281                    None
1282                }
1283                NodeKind::VariableDeclaration { initializer, .. } => {
1284                    initializer.as_ref().and_then(|init| find_binary_node(init, op))
1285                }
1286                _ => None,
1287            }
1288        }
1289
1290        // Test arithmetic operation infers to number
1291        if let Some(add_node) = find_binary_node(&ast, "+") {
1292            let inferred = analyzer.infer_type(add_node);
1293            assert_eq!(inferred, Some("number".to_string()), "Arithmetic should infer to number");
1294        }
1295
1296        // Test concatenation infers to string
1297        if let Some(concat_node) = find_binary_node(&ast, ".") {
1298            let inferred = analyzer.infer_type(concat_node);
1299            assert_eq!(
1300                inferred,
1301                Some("string".to_string()),
1302                "Concatenation should infer to string"
1303            );
1304        }
1305
1306        Ok(())
1307    }
1308
1309    #[test]
1310    fn test_anonymous_subroutine_hover_info() -> Result<(), Box<dyn std::error::Error>> {
1311        let code = r#"
1312# This is a closure
1313my $adder = sub {
1314    my ($x, $y) = @_;
1315    return $x + $y;
1316};
1317"#;
1318
1319        let mut parser = Parser::new(code);
1320        let ast = parser.parse()?;
1321        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1322
1323        // Find hover info for the anonymous sub
1324        let sub_position = code.find("sub {").ok_or("sub { not found")?;
1325        let hover = analyzer
1326            .hover_info
1327            .iter()
1328            .find(|(loc, _)| loc.start <= sub_position && loc.end >= sub_position)
1329            .map(|(_, h)| h);
1330
1331        assert!(hover.is_some(), "Should have hover info");
1332
1333        if let Some(h) = hover {
1334            assert!(h.signature.contains("sub"), "Hover signature should contain 'sub'");
1335            assert!(
1336                h.details.iter().any(|d| d.contains("Anonymous")),
1337                "Hover details should mention anonymous subroutine"
1338            );
1339            // Documentation extraction searches backwards from the sub keyword,
1340            // but the comment is before `my $adder =` (not immediately before `sub`),
1341            // so extract_documentation may not find it. Accept either outcome.
1342            if let Some(doc) = &h.documentation {
1343                assert!(
1344                    doc.contains("closure"),
1345                    "If documentation found, it should mention closure"
1346                );
1347            }
1348        }
1349        Ok(())
1350    }
1351
1352    // Phase 2/3 Handler Tests
1353    #[test]
1354    fn test_substitution_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
1355        let code = r#"
1356my $str = "hello world";
1357$str =~ s/world/Perl/;
1358"#;
1359        let mut parser = Parser::new(code);
1360        let ast = parser.parse()?;
1361        let analyzer = SemanticAnalyzer::analyze(&ast);
1362
1363        let tokens = analyzer.semantic_tokens();
1364        let operator_tokens: Vec<_> =
1365            tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1366
1367        assert!(!operator_tokens.is_empty(), "Should have operator tokens for substitution");
1368        Ok(())
1369    }
1370
1371    #[test]
1372    fn test_transliteration_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
1373        let code = r#"
1374my $str = "hello";
1375$str =~ tr/el/ol/;
1376"#;
1377        let mut parser = Parser::new(code);
1378        let ast = parser.parse()?;
1379        let analyzer = SemanticAnalyzer::analyze(&ast);
1380
1381        let tokens = analyzer.semantic_tokens();
1382        let operator_tokens: Vec<_> =
1383            tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1384
1385        assert!(!operator_tokens.is_empty(), "Should have operator tokens for transliteration");
1386        Ok(())
1387    }
1388
1389    #[test]
1390    fn test_reference_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
1391        let code = r#"
1392my $x = 42;
1393my $ref = \$x;
1394"#;
1395        let mut parser = Parser::new(code);
1396        let ast = parser.parse()?;
1397        let analyzer = SemanticAnalyzer::analyze(&ast);
1398
1399        let tokens = analyzer.semantic_tokens();
1400        let operator_tokens: Vec<_> =
1401            tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1402
1403        assert!(!operator_tokens.is_empty(), "Should have operator tokens for reference operator");
1404        Ok(())
1405    }
1406
1407    #[test]
1408    fn test_postfix_loop_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
1409        let code = r#"
1410my @list = (1, 2, 3);
1411print $_ for @list;
1412my $x = 0;
1413$x++ while $x < 10;
1414"#;
1415        let mut parser = Parser::new(code);
1416        let ast = parser.parse()?;
1417        let analyzer = SemanticAnalyzer::analyze(&ast);
1418
1419        let tokens = analyzer.semantic_tokens();
1420        let control_tokens: Vec<_> = tokens
1421            .iter()
1422            .filter(|t| matches!(t.token_type, SemanticTokenType::KeywordControl))
1423            .collect();
1424
1425        assert!(!control_tokens.is_empty(), "Should have control keyword tokens for postfix loops");
1426        Ok(())
1427    }
1428
1429    #[test]
1430    fn test_file_test_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
1431        let code = r#"
1432my $file = "test.txt";
1433if (-e $file) {
1434    print "exists";
1435}
1436if (-d $file) {
1437    print "directory";
1438}
1439if (-f $file) {
1440    print "file";
1441}
1442"#;
1443        let mut parser = Parser::new(code);
1444        let ast = parser.parse()?;
1445        let analyzer = SemanticAnalyzer::analyze(&ast);
1446
1447        let tokens = analyzer.semantic_tokens();
1448        let operator_tokens: Vec<_> =
1449            tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1450
1451        assert!(!operator_tokens.is_empty(), "Should have operator tokens for file test operators");
1452        Ok(())
1453    }
1454
1455    #[test]
1456    fn test_all_file_test_operators_recognized() -> Result<(), Box<dyn std::error::Error>> {
1457        // Test that the is_file_test_operator helper recognizes all file test operators
1458        let file_test_ops = vec![
1459            "-e", "-d", "-f", "-r", "-w", "-x", "-s", "-z", "-T", "-B", "-M", "-A", "-C", "-l",
1460            "-p", "-S", "-u", "-g", "-k", "-t", "-O", "-G", "-R", "-b", "-c",
1461        ];
1462
1463        for op in file_test_ops {
1464            assert!(
1465                SemanticAnalyzer::is_file_test_operator(op),
1466                "Operator {} should be recognized as file test operator",
1467                op
1468            );
1469        }
1470
1471        // Test that non-file-test operators are not recognized
1472        assert!(
1473            !SemanticAnalyzer::is_file_test_operator("+"),
1474            "Operator '+' should not be recognized as file test operator"
1475        );
1476        assert!(
1477            !SemanticAnalyzer::is_file_test_operator("-"),
1478            "Operator '-' should not be recognized as file test operator"
1479        );
1480        assert!(
1481            !SemanticAnalyzer::is_file_test_operator("++"),
1482            "Operator '++' should not be recognized as file test operator"
1483        );
1484
1485        Ok(())
1486    }
1487
1488    #[test]
1489    fn test_postfix_loop_modifiers() -> Result<(), Box<dyn std::error::Error>> {
1490        let code = r#"
1491my @items = (1, 2, 3);
1492print $_ for @items;
1493print $_ foreach @items;
1494my $x = 0;
1495$x++ while $x < 10;
1496$x-- until $x < 0;
1497"#;
1498        let mut parser = Parser::new(code);
1499        let ast = parser.parse()?;
1500        let analyzer = SemanticAnalyzer::analyze(&ast);
1501
1502        let tokens = analyzer.semantic_tokens();
1503        let control_tokens: Vec<_> = tokens
1504            .iter()
1505            .filter(|t| matches!(t.token_type, SemanticTokenType::KeywordControl))
1506            .collect();
1507
1508        // Should have at least 4 control keyword tokens (for, foreach, while, until)
1509        assert!(
1510            control_tokens.len() >= 4,
1511            "Should have at least 4 control keyword tokens for postfix loop modifiers"
1512        );
1513        Ok(())
1514    }
1515
1516    #[test]
1517    fn test_substitution_with_modifiers() -> Result<(), Box<dyn std::error::Error>> {
1518        let code = r#"
1519my $str = "hello world";
1520$str =~ s/world/Perl/gi;
1521"#;
1522        let mut parser = Parser::new(code);
1523        let ast = parser.parse()?;
1524        let analyzer = SemanticAnalyzer::analyze(&ast);
1525
1526        let tokens = analyzer.semantic_tokens();
1527        let operator_tokens: Vec<_> =
1528            tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1529
1530        assert!(
1531            !operator_tokens.is_empty(),
1532            "Should have operator tokens for substitution with modifiers"
1533        );
1534        Ok(())
1535    }
1536
1537    #[test]
1538    fn test_transliteration_y_operator() -> Result<(), Box<dyn std::error::Error>> {
1539        let code = r#"
1540my $str = "hello";
1541$str =~ y/hello/world/;
1542"#;
1543        let mut parser = Parser::new(code);
1544        let ast = parser.parse()?;
1545        let analyzer = SemanticAnalyzer::analyze(&ast);
1546
1547        let tokens = analyzer.semantic_tokens();
1548        let operator_tokens: Vec<_> =
1549            tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1550
1551        assert!(
1552            !operator_tokens.is_empty(),
1553            "Should have operator tokens for y/// transliteration"
1554        );
1555        Ok(())
1556    }
1557
1558    #[test]
1559    fn test_builtin_documentation_coverage() -> Result<(), Box<dyn std::error::Error>> {
1560        // Verify that commonly-used builtins have documentation
1561        let builtins = [
1562            "print", "say", "push", "pop", "shift", "unshift", "map", "grep", "sort", "reverse",
1563            "split", "join", "chomp", "chop", "length", "substr", "index", "rindex", "lc", "uc",
1564            "die", "warn", "eval", "open", "close", "read", "keys", "values", "exists", "delete",
1565            "defined", "ref", "bless", "sprintf", "chr", "ord",
1566        ];
1567
1568        for name in &builtins {
1569            let doc = get_builtin_documentation(name);
1570            assert!(doc.is_some(), "Built-in '{}' should have documentation", name);
1571            let doc = doc.unwrap();
1572            assert!(
1573                !doc.signature.is_empty(),
1574                "Built-in '{}' should have a non-empty signature",
1575                name
1576            );
1577            assert!(
1578                !doc.description.is_empty(),
1579                "Built-in '{}' should have a non-empty description",
1580                name
1581            );
1582        }
1583        Ok(())
1584    }
1585
1586    #[test]
1587    fn test_builtin_hover_for_function_call() -> Result<(), Box<dyn std::error::Error>> {
1588        let code = r#"
1589my @items = (3, 1, 4);
1590push @items, 5;
1591"#;
1592        let mut parser = Parser::new(code);
1593        let ast = parser.parse()?;
1594        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1595
1596        // Find the hover info for 'push' function call
1597        let push_pos = code.find("push").ok_or("push not found")?;
1598        let hover_for_push =
1599            analyzer.hover_info.iter().find(|(loc, _)| loc.start <= push_pos && loc.end > push_pos);
1600
1601        assert!(hover_for_push.is_some(), "Should have hover info for 'push' builtin");
1602        let (_, hover) = hover_for_push.unwrap();
1603        assert!(
1604            hover.signature.contains("push"),
1605            "Hover signature should contain 'push', got: {}",
1606            hover.signature
1607        );
1608        assert!(hover.documentation.is_some(), "Hover for 'push' should have documentation");
1609        Ok(())
1610    }
1611
1612    #[test]
1613    fn test_core_prefixed_builtin_hover_for_function_call() -> Result<(), Box<dyn std::error::Error>>
1614    {
1615        let code = r#"
1616my $value = "abc";
1617CORE::length($value);
1618"#;
1619        let mut parser = Parser::new(code);
1620        let ast = parser.parse()?;
1621        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1622
1623        let length_pos = code.find("CORE::length").ok_or("CORE::length not found")?;
1624        let hover = analyzer
1625            .hover_info
1626            .iter()
1627            .find(|(loc, _)| loc.start <= length_pos && loc.end > length_pos);
1628
1629        assert!(hover.is_some(), "Should have hover info for CORE::length builtin");
1630        let (_, hover) = hover.ok_or("missing hover for CORE::length")?;
1631        assert!(
1632            hover.signature.contains("length"),
1633            "Hover signature should contain 'length', got: {}",
1634            hover.signature
1635        );
1636        Ok(())
1637    }
1638
1639    #[test]
1640    fn test_utf8_builtin_hover_for_function_call() -> Result<(), Box<dyn std::error::Error>> {
1641        let code = r#"
1642my $value = "\x{100}";
1643utf8::encode($value);
1644"#;
1645        let mut parser = Parser::new(code);
1646        let ast = parser.parse()?;
1647        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1648
1649        let encode_pos = code.find("utf8::encode").ok_or("utf8::encode not found")?;
1650        let hover = analyzer
1651            .hover_info
1652            .iter()
1653            .find(|(loc, _)| loc.start <= encode_pos && loc.end > encode_pos);
1654
1655        assert!(hover.is_some(), "Should have hover info for utf8::encode builtin");
1656        let (_, hover) = hover.ok_or("missing hover for utf8::encode")?;
1657        assert!(
1658            hover.signature.contains("utf8::encode"),
1659            "Hover signature should contain utf8::encode, got: {}",
1660            hover.signature
1661        );
1662        let documentation =
1663            hover.documentation.as_deref().ok_or("utf8::encode hover should have docs")?;
1664        assert!(
1665            documentation.contains("extended UTF-8 octets"),
1666            "Hover docs should describe utf8::encode semantics, got: {documentation}"
1667        );
1668        Ok(())
1669    }
1670
1671    #[test]
1672    fn test_package_hover_with_pod_name_section() -> Result<(), Box<dyn std::error::Error>> {
1673        let code = r#"
1674=head1 NAME
1675
1676My::Module - A great module for testing
1677
1678=head1 DESCRIPTION
1679
1680This module does great things.
1681
1682=cut
1683
1684package My::Module;
1685
1686sub new { bless {}, shift }
1687
16881;
1689"#;
1690        let mut parser = Parser::new(code);
1691        let ast = parser.parse()?;
1692        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1693
1694        // Find the package symbol
1695        let pkg_symbols = analyzer.symbol_table().symbols.get("My::Module");
1696        assert!(pkg_symbols.is_some(), "Should find My::Module in symbol table");
1697
1698        let pkg = &pkg_symbols.unwrap()[0];
1699        let hover = analyzer.hover_at(pkg.location);
1700        assert!(hover.is_some(), "Should have hover info for package");
1701
1702        let hover = hover.unwrap();
1703        assert!(
1704            hover.signature.contains("package My::Module"),
1705            "Package hover signature should contain 'package My::Module', got: {}",
1706            hover.signature
1707        );
1708        // The POD NAME section should be extracted as documentation
1709        if let Some(doc) = &hover.documentation {
1710            assert!(
1711                doc.contains("A great module for testing"),
1712                "Package hover should contain POD NAME content, got: {}",
1713                doc
1714            );
1715        }
1716        Ok(())
1717    }
1718
1719    #[test]
1720    fn test_package_documentation_via_symbol() -> Result<(), Box<dyn std::error::Error>> {
1721        let code = r#"
1722=head1 NAME
1723
1724Utils - Utility functions
1725
1726=cut
1727
1728package Utils;
1729
1730sub helper { 1 }
1731
17321;
1733"#;
1734        let mut parser = Parser::new(code);
1735        let ast = parser.parse()?;
1736        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1737
1738        let pkg_symbols = analyzer.symbol_table().symbols.get("Utils");
1739        assert!(pkg_symbols.is_some(), "Should find Utils package");
1740
1741        let pkg = &pkg_symbols.unwrap()[0];
1742        // The symbol extractor should have extracted POD docs
1743        assert!(
1744            pkg.documentation.is_some(),
1745            "Package symbol should have documentation from POD NAME section"
1746        );
1747        let doc = pkg.documentation.as_ref().unwrap();
1748        assert!(
1749            doc.contains("Utility functions"),
1750            "Package doc should contain 'Utility functions', got: {}",
1751            doc
1752        );
1753        Ok(())
1754    }
1755
1756    #[test]
1757    fn test_subroutine_with_pod_docs_hover() -> Result<(), Box<dyn std::error::Error>> {
1758        let code = r#"
1759=head2 process
1760
1761Processes input data and returns the result.
1762
1763=cut
1764
1765sub process {
1766    my ($input) = @_;
1767    return $input * 2;
1768}
1769"#;
1770        let mut parser = Parser::new(code);
1771        let ast = parser.parse()?;
1772        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1773
1774        let sub_symbols = analyzer.symbol_table().find_symbol("process", 0, SymbolKind::Subroutine);
1775        assert!(!sub_symbols.is_empty(), "Should find sub process");
1776
1777        let hover = analyzer.hover_at(sub_symbols[0].location);
1778        assert!(hover.is_some(), "Should have hover for sub process");
1779
1780        let hover = hover.unwrap();
1781        assert!(
1782            hover.signature.contains("sub process"),
1783            "Hover should show sub signature, got: {}",
1784            hover.signature
1785        );
1786        // The POD =head2 docs should be extracted
1787        if let Some(doc) = &hover.documentation {
1788            assert!(
1789                doc.contains("process") || doc.contains("Processes"),
1790                "Sub hover should contain POD documentation, got: {}",
1791                doc
1792            );
1793        }
1794        Ok(())
1795    }
1796
1797    #[test]
1798    fn test_variable_hover_shows_declaration_type() -> Result<(), Box<dyn std::error::Error>> {
1799        let code = r#"my $count = 42;
1800my @items = (1, 2, 3);
1801my %config = (key => "value");
1802"#;
1803        let mut parser = Parser::new(code);
1804        let ast = parser.parse()?;
1805        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1806
1807        // Check scalar variable hover
1808        let scalar_pos = code.find("$count").ok_or("$count not found")?;
1809        let scalar_hover = analyzer
1810            .hover_info
1811            .iter()
1812            .find(|(loc, _)| loc.start <= scalar_pos && loc.end > scalar_pos);
1813        assert!(scalar_hover.is_some(), "Should have hover for $count");
1814        let (_, hover) = scalar_hover.unwrap();
1815        assert!(
1816            hover.signature.contains("$count"),
1817            "Scalar hover should show variable name, got: {}",
1818            hover.signature
1819        );
1820
1821        // Check array variable hover
1822        let array_pos = code.find("@items").ok_or("@items not found")?;
1823        let array_hover = analyzer
1824            .hover_info
1825            .iter()
1826            .find(|(loc, _)| loc.start <= array_pos && loc.end > array_pos);
1827        assert!(array_hover.is_some(), "Should have hover for @items");
1828        let (_, hover) = array_hover.unwrap();
1829        assert!(
1830            hover.signature.contains("@items"),
1831            "Array hover should show variable name, got: {}",
1832            hover.signature
1833        );
1834
1835        // Check hash variable hover
1836        let hash_pos = code.find("%config").ok_or("%config not found")?;
1837        let hash_hover =
1838            analyzer.hover_info.iter().find(|(loc, _)| loc.start <= hash_pos && loc.end > hash_pos);
1839        assert!(hash_hover.is_some(), "Should have hover for %config");
1840        let (_, hover) = hash_hover.unwrap();
1841        assert!(
1842            hover.signature.contains("%config"),
1843            "Hash hover should show variable name, got: {}",
1844            hover.signature
1845        );
1846        Ok(())
1847    }
1848
1849    // -----------------------------------------------------------------------
1850    // Subroutine signature hover tests (issue #2353)
1851    // -----------------------------------------------------------------------
1852
1853    #[test]
1854    fn test_signature_hover_shows_param_names() -> Result<(), Box<dyn std::error::Error>> {
1855        // sub with named scalar parameters — hover must show them by name, not (...)
1856        let code = "sub add($x, $y) { $x + $y }";
1857        let mut parser = Parser::new(code);
1858        let ast = parser.parse()?;
1859        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1860
1861        let sub_symbols =
1862            analyzer.symbol_table().find_symbol("add", 0, crate::symbol::SymbolKind::Subroutine);
1863        assert!(!sub_symbols.is_empty(), "symbol 'add' not found");
1864
1865        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1866        assert!(
1867            hover.signature.contains("$x"),
1868            "hover signature should contain '$x', got: {}",
1869            hover.signature
1870        );
1871        assert!(
1872            hover.signature.contains("$y"),
1873            "hover signature should contain '$y', got: {}",
1874            hover.signature
1875        );
1876        assert!(
1877            !hover.signature.contains("(...)"),
1878            "hover signature must not fall back to '(...)', got: {}",
1879            hover.signature
1880        );
1881        Ok(())
1882    }
1883
1884    #[test]
1885    fn test_signature_hover_with_optional_param() -> Result<(), Box<dyn std::error::Error>> {
1886        // optional parameter with default value
1887        let code = "sub greet($name, $greeting = 'Hello') { \"$greeting, $name\" }";
1888        let mut parser = Parser::new(code);
1889        let ast = parser.parse()?;
1890        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1891
1892        let sub_symbols =
1893            analyzer.symbol_table().find_symbol("greet", 0, crate::symbol::SymbolKind::Subroutine);
1894        assert!(!sub_symbols.is_empty(), "symbol 'greet' not found");
1895
1896        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1897        assert!(
1898            hover.signature.contains("$name"),
1899            "hover signature should contain '$name', got: {}",
1900            hover.signature
1901        );
1902        assert!(
1903            hover.signature.contains("$greeting"),
1904            "hover signature should contain '$greeting', got: {}",
1905            hover.signature
1906        );
1907        Ok(())
1908    }
1909
1910    #[test]
1911    fn test_signature_hover_with_slurpy_param() -> Result<(), Box<dyn std::error::Error>> {
1912        // slurpy array parameter collects remaining args
1913        let code = "sub log_all($level, @messages) { print \"$level: @messages\" }";
1914        let mut parser = Parser::new(code);
1915        let ast = parser.parse()?;
1916        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1917
1918        let sub_symbols = analyzer.symbol_table().find_symbol(
1919            "log_all",
1920            0,
1921            crate::symbol::SymbolKind::Subroutine,
1922        );
1923        assert!(!sub_symbols.is_empty(), "symbol 'log_all' not found");
1924
1925        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1926        assert!(
1927            hover.signature.contains("@messages"),
1928            "hover signature should contain '@messages', got: {}",
1929            hover.signature
1930        );
1931        Ok(())
1932    }
1933
1934    #[test]
1935    fn test_find_definition_returns_method_kind_for_native_method()
1936    -> Result<(), Box<dyn std::error::Error>> {
1937        let code = "class Foo {\n    method bar { return 1; }\n}\n";
1938        let mut parser = Parser::new(code);
1939        let ast = parser.parse()?;
1940
1941        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1942
1943        // Find offset of "bar" in "method bar" on line 1
1944        let line1 = code.lines().nth(1).ok_or("no line 1")?;
1945        let line0_len = code.lines().next().ok_or("no line 0")?.len() + 1;
1946        let col = line1.find("bar").ok_or("bar not found on line 1")?;
1947        let offset = line0_len + col;
1948
1949        let sym = analyzer.find_definition(offset).ok_or("no symbol found at 'bar'")?;
1950        assert_eq!(sym.name, "bar", "symbol name should be 'bar'");
1951        assert_eq!(
1952            sym.kind,
1953            SymbolKind::Method,
1954            "native method should have SymbolKind::Method, got {:?}",
1955            sym.kind
1956        );
1957        Ok(())
1958    }
1959
1960    #[test]
1961    fn test_find_definition_redirects_method_modifier_to_target_method()
1962    -> Result<(), Box<dyn std::error::Error>> {
1963        let code = include_str!(
1964            "../../../../perl-lsp-rs/tests/fixtures/frameworks/moo_method_modifiers.pl"
1965        );
1966        let mut parser = Parser::new(code);
1967        let ast = parser.parse()?;
1968
1969        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1970
1971        // The method definition is line 3; the modifier targets are on lines 8, 13, and 18.
1972        for target_line in [8, 13, 18] {
1973            let line = code.lines().nth(target_line).ok_or("missing modifier line")?;
1974            let col = line.find("save").ok_or("modifier target not found")?;
1975            let mut offset = 0;
1976            for line in code.lines().take(target_line) {
1977                offset += line.len() + 1;
1978            }
1979            offset += col;
1980
1981            let sym = analyzer
1982                .find_definition(offset)
1983                .ok_or("no symbol found at method modifier target")?;
1984            assert_eq!(sym.name, "save", "modifier target should resolve to save");
1985            let method_start = code.find("sub save").ok_or("method declaration not found")?;
1986            assert_eq!(
1987                sym.location.start, method_start,
1988                "modifier target should resolve to the underlying method declaration"
1989            );
1990            assert_eq!(
1991                sym.declaration, None,
1992                "definition should land on the real method, not the synthetic modifier"
1993            );
1994        }
1995
1996        Ok(())
1997    }
1998
1999    #[test]
2000    fn test_resolve_inherited_method_location_limits_dfs_depth()
2001    -> Result<(), Box<dyn std::error::Error>> {
2002        let chain_len = MAX_MRO_TRAVERSAL_DEPTH + 10;
2003        let mut code = String::new();
2004        for i in 0..chain_len {
2005            code.push_str(&format!("package P{i}; use parent 'P{}';\n", i + 1));
2006        }
2007        code.push_str(&format!("package P{chain_len}; sub target {{ 1 }}\n"));
2008
2009        let mut parser = Parser::new(&code);
2010        let ast = parser.parse()?;
2011        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, &code);
2012
2013        let location = analyzer.resolve_inherited_method_location("P0", "target");
2014        assert!(location.is_none(), "DFS traversal should stop at depth limit");
2015        Ok(())
2016    }
2017
2018    #[test]
2019    fn test_resolve_inherited_method_location_limits_c3_depth()
2020    -> Result<(), Box<dyn std::error::Error>> {
2021        let chain_len = MAX_MRO_TRAVERSAL_DEPTH + 10;
2022        let mut code = String::new();
2023        code.push_str("package P0; use mro 'c3'; use parent 'P1';\n");
2024        for i in 1..chain_len {
2025            code.push_str(&format!("package P{i}; use parent 'P{}';\n", i + 1));
2026        }
2027        code.push_str(&format!("package P{chain_len}; sub target {{ 1 }}\n"));
2028
2029        let mut parser = Parser::new(&code);
2030        let ast = parser.parse()?;
2031        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, &code);
2032
2033        let location = analyzer.resolve_inherited_method_location("P0", "target");
2034        assert!(location.is_none(), "C3 traversal should stop at depth limit");
2035        Ok(())
2036    }
2037}