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_package_hover_with_pod_name_section() -> Result<(), Box<dyn std::error::Error>> {
1641        let code = r#"
1642=head1 NAME
1643
1644My::Module - A great module for testing
1645
1646=head1 DESCRIPTION
1647
1648This module does great things.
1649
1650=cut
1651
1652package My::Module;
1653
1654sub new { bless {}, shift }
1655
16561;
1657"#;
1658        let mut parser = Parser::new(code);
1659        let ast = parser.parse()?;
1660        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1661
1662        // Find the package symbol
1663        let pkg_symbols = analyzer.symbol_table().symbols.get("My::Module");
1664        assert!(pkg_symbols.is_some(), "Should find My::Module in symbol table");
1665
1666        let pkg = &pkg_symbols.unwrap()[0];
1667        let hover = analyzer.hover_at(pkg.location);
1668        assert!(hover.is_some(), "Should have hover info for package");
1669
1670        let hover = hover.unwrap();
1671        assert!(
1672            hover.signature.contains("package My::Module"),
1673            "Package hover signature should contain 'package My::Module', got: {}",
1674            hover.signature
1675        );
1676        // The POD NAME section should be extracted as documentation
1677        if let Some(doc) = &hover.documentation {
1678            assert!(
1679                doc.contains("A great module for testing"),
1680                "Package hover should contain POD NAME content, got: {}",
1681                doc
1682            );
1683        }
1684        Ok(())
1685    }
1686
1687    #[test]
1688    fn test_package_documentation_via_symbol() -> Result<(), Box<dyn std::error::Error>> {
1689        let code = r#"
1690=head1 NAME
1691
1692Utils - Utility functions
1693
1694=cut
1695
1696package Utils;
1697
1698sub helper { 1 }
1699
17001;
1701"#;
1702        let mut parser = Parser::new(code);
1703        let ast = parser.parse()?;
1704        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1705
1706        let pkg_symbols = analyzer.symbol_table().symbols.get("Utils");
1707        assert!(pkg_symbols.is_some(), "Should find Utils package");
1708
1709        let pkg = &pkg_symbols.unwrap()[0];
1710        // The symbol extractor should have extracted POD docs
1711        assert!(
1712            pkg.documentation.is_some(),
1713            "Package symbol should have documentation from POD NAME section"
1714        );
1715        let doc = pkg.documentation.as_ref().unwrap();
1716        assert!(
1717            doc.contains("Utility functions"),
1718            "Package doc should contain 'Utility functions', got: {}",
1719            doc
1720        );
1721        Ok(())
1722    }
1723
1724    #[test]
1725    fn test_subroutine_with_pod_docs_hover() -> Result<(), Box<dyn std::error::Error>> {
1726        let code = r#"
1727=head2 process
1728
1729Processes input data and returns the result.
1730
1731=cut
1732
1733sub process {
1734    my ($input) = @_;
1735    return $input * 2;
1736}
1737"#;
1738        let mut parser = Parser::new(code);
1739        let ast = parser.parse()?;
1740        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1741
1742        let sub_symbols = analyzer.symbol_table().find_symbol("process", 0, SymbolKind::Subroutine);
1743        assert!(!sub_symbols.is_empty(), "Should find sub process");
1744
1745        let hover = analyzer.hover_at(sub_symbols[0].location);
1746        assert!(hover.is_some(), "Should have hover for sub process");
1747
1748        let hover = hover.unwrap();
1749        assert!(
1750            hover.signature.contains("sub process"),
1751            "Hover should show sub signature, got: {}",
1752            hover.signature
1753        );
1754        // The POD =head2 docs should be extracted
1755        if let Some(doc) = &hover.documentation {
1756            assert!(
1757                doc.contains("process") || doc.contains("Processes"),
1758                "Sub hover should contain POD documentation, got: {}",
1759                doc
1760            );
1761        }
1762        Ok(())
1763    }
1764
1765    #[test]
1766    fn test_variable_hover_shows_declaration_type() -> Result<(), Box<dyn std::error::Error>> {
1767        let code = r#"my $count = 42;
1768my @items = (1, 2, 3);
1769my %config = (key => "value");
1770"#;
1771        let mut parser = Parser::new(code);
1772        let ast = parser.parse()?;
1773        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1774
1775        // Check scalar variable hover
1776        let scalar_pos = code.find("$count").ok_or("$count not found")?;
1777        let scalar_hover = analyzer
1778            .hover_info
1779            .iter()
1780            .find(|(loc, _)| loc.start <= scalar_pos && loc.end > scalar_pos);
1781        assert!(scalar_hover.is_some(), "Should have hover for $count");
1782        let (_, hover) = scalar_hover.unwrap();
1783        assert!(
1784            hover.signature.contains("$count"),
1785            "Scalar hover should show variable name, got: {}",
1786            hover.signature
1787        );
1788
1789        // Check array variable hover
1790        let array_pos = code.find("@items").ok_or("@items not found")?;
1791        let array_hover = analyzer
1792            .hover_info
1793            .iter()
1794            .find(|(loc, _)| loc.start <= array_pos && loc.end > array_pos);
1795        assert!(array_hover.is_some(), "Should have hover for @items");
1796        let (_, hover) = array_hover.unwrap();
1797        assert!(
1798            hover.signature.contains("@items"),
1799            "Array hover should show variable name, got: {}",
1800            hover.signature
1801        );
1802
1803        // Check hash variable hover
1804        let hash_pos = code.find("%config").ok_or("%config not found")?;
1805        let hash_hover =
1806            analyzer.hover_info.iter().find(|(loc, _)| loc.start <= hash_pos && loc.end > hash_pos);
1807        assert!(hash_hover.is_some(), "Should have hover for %config");
1808        let (_, hover) = hash_hover.unwrap();
1809        assert!(
1810            hover.signature.contains("%config"),
1811            "Hash hover should show variable name, got: {}",
1812            hover.signature
1813        );
1814        Ok(())
1815    }
1816
1817    // -----------------------------------------------------------------------
1818    // Subroutine signature hover tests (issue #2353)
1819    // -----------------------------------------------------------------------
1820
1821    #[test]
1822    fn test_signature_hover_shows_param_names() -> Result<(), Box<dyn std::error::Error>> {
1823        // sub with named scalar parameters — hover must show them by name, not (...)
1824        let code = "sub add($x, $y) { $x + $y }";
1825        let mut parser = Parser::new(code);
1826        let ast = parser.parse()?;
1827        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1828
1829        let sub_symbols =
1830            analyzer.symbol_table().find_symbol("add", 0, crate::symbol::SymbolKind::Subroutine);
1831        assert!(!sub_symbols.is_empty(), "symbol 'add' not found");
1832
1833        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1834        assert!(
1835            hover.signature.contains("$x"),
1836            "hover signature should contain '$x', got: {}",
1837            hover.signature
1838        );
1839        assert!(
1840            hover.signature.contains("$y"),
1841            "hover signature should contain '$y', got: {}",
1842            hover.signature
1843        );
1844        assert!(
1845            !hover.signature.contains("(...)"),
1846            "hover signature must not fall back to '(...)', got: {}",
1847            hover.signature
1848        );
1849        Ok(())
1850    }
1851
1852    #[test]
1853    fn test_signature_hover_with_optional_param() -> Result<(), Box<dyn std::error::Error>> {
1854        // optional parameter with default value
1855        let code = "sub greet($name, $greeting = 'Hello') { \"$greeting, $name\" }";
1856        let mut parser = Parser::new(code);
1857        let ast = parser.parse()?;
1858        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1859
1860        let sub_symbols =
1861            analyzer.symbol_table().find_symbol("greet", 0, crate::symbol::SymbolKind::Subroutine);
1862        assert!(!sub_symbols.is_empty(), "symbol 'greet' not found");
1863
1864        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1865        assert!(
1866            hover.signature.contains("$name"),
1867            "hover signature should contain '$name', got: {}",
1868            hover.signature
1869        );
1870        assert!(
1871            hover.signature.contains("$greeting"),
1872            "hover signature should contain '$greeting', got: {}",
1873            hover.signature
1874        );
1875        Ok(())
1876    }
1877
1878    #[test]
1879    fn test_signature_hover_with_slurpy_param() -> Result<(), Box<dyn std::error::Error>> {
1880        // slurpy array parameter collects remaining args
1881        let code = "sub log_all($level, @messages) { print \"$level: @messages\" }";
1882        let mut parser = Parser::new(code);
1883        let ast = parser.parse()?;
1884        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1885
1886        let sub_symbols = analyzer.symbol_table().find_symbol(
1887            "log_all",
1888            0,
1889            crate::symbol::SymbolKind::Subroutine,
1890        );
1891        assert!(!sub_symbols.is_empty(), "symbol 'log_all' not found");
1892
1893        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1894        assert!(
1895            hover.signature.contains("@messages"),
1896            "hover signature should contain '@messages', got: {}",
1897            hover.signature
1898        );
1899        Ok(())
1900    }
1901
1902    #[test]
1903    fn test_find_definition_returns_method_kind_for_native_method()
1904    -> Result<(), Box<dyn std::error::Error>> {
1905        let code = "class Foo {\n    method bar { return 1; }\n}\n";
1906        let mut parser = Parser::new(code);
1907        let ast = parser.parse()?;
1908
1909        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1910
1911        // Find offset of "bar" in "method bar" on line 1
1912        let line1 = code.lines().nth(1).ok_or("no line 1")?;
1913        let line0_len = code.lines().next().ok_or("no line 0")?.len() + 1;
1914        let col = line1.find("bar").ok_or("bar not found on line 1")?;
1915        let offset = line0_len + col;
1916
1917        let sym = analyzer.find_definition(offset).ok_or("no symbol found at 'bar'")?;
1918        assert_eq!(sym.name, "bar", "symbol name should be 'bar'");
1919        assert_eq!(
1920            sym.kind,
1921            SymbolKind::Method,
1922            "native method should have SymbolKind::Method, got {:?}",
1923            sym.kind
1924        );
1925        Ok(())
1926    }
1927
1928    #[test]
1929    fn test_find_definition_redirects_method_modifier_to_target_method()
1930    -> Result<(), Box<dyn std::error::Error>> {
1931        let code = include_str!(
1932            "../../../../perl-lsp-rs/tests/fixtures/frameworks/moo_method_modifiers.pl"
1933        );
1934        let mut parser = Parser::new(code);
1935        let ast = parser.parse()?;
1936
1937        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1938
1939        // The method definition is line 3; the modifier targets are on lines 8, 13, and 18.
1940        for target_line in [8, 13, 18] {
1941            let line = code.lines().nth(target_line).ok_or("missing modifier line")?;
1942            let col = line.find("save").ok_or("modifier target not found")?;
1943            let mut offset = 0;
1944            for line in code.lines().take(target_line) {
1945                offset += line.len() + 1;
1946            }
1947            offset += col;
1948
1949            let sym = analyzer
1950                .find_definition(offset)
1951                .ok_or("no symbol found at method modifier target")?;
1952            assert_eq!(sym.name, "save", "modifier target should resolve to save");
1953            let method_start = code.find("sub save").ok_or("method declaration not found")?;
1954            assert_eq!(
1955                sym.location.start, method_start,
1956                "modifier target should resolve to the underlying method declaration"
1957            );
1958            assert_eq!(
1959                sym.declaration, None,
1960                "definition should land on the real method, not the synthetic modifier"
1961            );
1962        }
1963
1964        Ok(())
1965    }
1966
1967    #[test]
1968    fn test_resolve_inherited_method_location_limits_dfs_depth()
1969    -> Result<(), Box<dyn std::error::Error>> {
1970        let chain_len = MAX_MRO_TRAVERSAL_DEPTH + 10;
1971        let mut code = String::new();
1972        for i in 0..chain_len {
1973            code.push_str(&format!("package P{i}; use parent 'P{}';\n", i + 1));
1974        }
1975        code.push_str(&format!("package P{chain_len}; sub target {{ 1 }}\n"));
1976
1977        let mut parser = Parser::new(&code);
1978        let ast = parser.parse()?;
1979        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, &code);
1980
1981        let location = analyzer.resolve_inherited_method_location("P0", "target");
1982        assert!(location.is_none(), "DFS traversal should stop at depth limit");
1983        Ok(())
1984    }
1985
1986    #[test]
1987    fn test_resolve_inherited_method_location_limits_c3_depth()
1988    -> Result<(), Box<dyn std::error::Error>> {
1989        let chain_len = MAX_MRO_TRAVERSAL_DEPTH + 10;
1990        let mut code = String::new();
1991        code.push_str("package P0; use mro 'c3'; use parent 'P1';\n");
1992        for i in 1..chain_len {
1993            code.push_str(&format!("package P{i}; use parent 'P{}';\n", i + 1));
1994        }
1995        code.push_str(&format!("package P{chain_len}; sub target {{ 1 }}\n"));
1996
1997        let mut parser = Parser::new(&code);
1998        let ast = parser.parse()?;
1999        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, &code);
2000
2001        let location = analyzer.resolve_inherited_method_location("P0", "target");
2002        assert!(location.is_none(), "C3 traversal should stop at depth limit");
2003        Ok(())
2004    }
2005}