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 hover;
20mod model;
21mod node_analysis;
22mod references;
23mod tokens;
24
25// Public re-exports — downstream consumers see exactly the same surface.
26pub use builtins::{
27    BuiltinDoc, ExceptionContext, get_attribute_documentation, get_builtin_documentation,
28    get_exception_context, get_moose_type_documentation, is_exception_function,
29};
30pub use hover::HoverInfo;
31pub use model::SemanticModel;
32pub use tokens::{SemanticToken, SemanticTokenModifier, SemanticTokenType};
33
34use crate::SourceLocation;
35use crate::analysis::class_model::{ClassModel, ClassModelBuilder};
36use crate::ast::Node;
37use crate::symbol::{Symbol, SymbolExtractor, SymbolTable};
38use std::collections::HashMap;
39
40#[derive(Debug)]
41/// Semantic analyzer providing comprehensive IDE features for Perl code.
42///
43/// Central component for LSP semantic analysis, combining symbol table
44/// construction, semantic token generation, and hover information extraction
45/// with enterprise-grade performance characteristics.
46///
47/// # Performance Characteristics
48/// - Analysis time: O(n) where n is AST node count
49/// - Memory usage: ~1MB per 10K lines of Perl code
50/// - Incremental updates: ≤1ms for typical changes
51/// - Symbol resolution: <50μs average lookup time
52///
53/// # LSP Workflow Integration
54/// Core pipeline component:
55/// 1. **Parse**: AST generation from Perl source
56/// 2. **Index**: Symbol table and semantic token construction
57/// 3. **Navigate**: Symbol resolution for go-to-definition
58/// 4. **Complete**: Context-aware completion suggestions
59/// 5. **Analyze**: Cross-reference analysis and diagnostics
60///
61/// # Perl Language Support
62/// - Full Perl 5 syntax coverage with modern idioms
63/// - Package-qualified symbol resolution
64/// - Lexical scoping with `my`, `our`, `local`, `state`
65/// - Object-oriented method dispatch
66/// - Regular expression and heredoc analysis
67///
68/// # Examples
69///
70/// ```ignore
71/// use perl_parser::{Parser, SemanticAnalyzer};
72///
73/// let code = "my $greeting = 'hello'; sub say_hi { print $greeting; }";
74/// let mut parser = Parser::new(code);
75/// let ast = parser.parse()?;
76///
77/// let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
78/// let symbols = analyzer.symbol_table();
79/// let tokens = analyzer.semantic_tokens();
80/// ```
81pub struct SemanticAnalyzer {
82    /// Symbol table with scope hierarchy and definitions
83    pub(super) symbol_table: SymbolTable,
84    /// Generated semantic tokens for syntax highlighting
85    pub(super) semantic_tokens: Vec<SemanticToken>,
86    /// Hover information cache for symbol details
87    pub(super) hover_info: HashMap<SourceLocation, HoverInfo>,
88    /// Source code for text extraction and analysis
89    pub(super) source: String,
90    /// Class models extracted from the same file (for same-file inheritance resolution)
91    pub class_models: Vec<ClassModel>,
92}
93
94impl SemanticAnalyzer {
95    /// Create a new semantic analyzer from an AST.
96    ///
97    /// Equivalent to [`analyze_with_source`](Self::analyze_with_source) with an empty
98    /// source string. Use this when you only need symbol-table and token analysis
99    /// without source-text-dependent features like hover documentation.
100    ///
101    /// # Examples
102    ///
103    /// ```ignore
104    /// use perl_parser::{Parser, SemanticAnalyzer};
105    ///
106    /// let mut parser = Parser::new("my $x = 42;");
107    /// let ast = parser.parse()?;
108    /// let analyzer = SemanticAnalyzer::analyze(&ast);
109    /// assert!(!analyzer.symbol_table().symbols.is_empty());
110    /// ```
111    pub fn analyze(ast: &Node) -> Self {
112        Self::analyze_with_source(ast, "")
113    }
114
115    /// Create a new semantic analyzer from an AST and source text.
116    ///
117    /// The source text enables richer analysis including hover documentation
118    /// extraction and precise text-range lookups.
119    ///
120    /// # Examples
121    ///
122    /// ```ignore
123    /// use perl_parser::{Parser, SemanticAnalyzer};
124    ///
125    /// let code = "sub greet { print \"Hello\\n\"; }";
126    /// let mut parser = Parser::new(code);
127    /// let ast = parser.parse()?;
128    ///
129    /// let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
130    /// let tokens = analyzer.semantic_tokens();
131    /// // tokens contains semantic highlighting data for the parsed code
132    /// ```
133    pub fn analyze_with_source(ast: &Node, source: &str) -> Self {
134        let symbol_table = SymbolExtractor::new_with_source(source).extract(ast);
135        let class_models = ClassModelBuilder::new().build(ast);
136
137        let mut analyzer = SemanticAnalyzer {
138            symbol_table,
139            semantic_tokens: Vec::new(),
140            hover_info: HashMap::new(),
141            source: source.to_string(),
142            class_models,
143        };
144
145        analyzer.analyze_node(ast, 0);
146        analyzer
147    }
148
149    /// Get the symbol table.
150    pub fn symbol_table(&self) -> &SymbolTable {
151        &self.symbol_table
152    }
153
154    /// Get semantic tokens for syntax highlighting.
155    pub fn semantic_tokens(&self) -> &[SemanticToken] {
156        &self.semantic_tokens
157    }
158
159    /// Get hover information at a location for Navigate/Analyze stages.
160    pub fn hover_at(&self, location: SourceLocation) -> Option<&HoverInfo> {
161        self.hover_info.get(&location)
162    }
163
164    /// Iterate over all hover entries collected during analysis.
165    pub fn all_hover_entries(&self) -> impl Iterator<Item = &HoverInfo> {
166        self.hover_info.values()
167    }
168
169    /// Find the symbol at a given location for Navigate workflows.
170    ///
171    /// Returns the most specific (smallest range) symbol that contains the location.
172    /// This ensures that when hovering inside a subroutine body, we return the
173    /// variable at the cursor rather than the enclosing subroutine.
174    pub fn symbol_at(&self, location: SourceLocation) -> Option<&Symbol> {
175        let mut best: Option<&Symbol> = None;
176        let mut best_span = usize::MAX;
177
178        // Search through all symbols for the most specific one at this location
179        for symbols in self.symbol_table.symbols.values() {
180            for symbol in symbols {
181                if symbol.location.start <= location.start && symbol.location.end >= location.end {
182                    let span = symbol.location.end - symbol.location.start;
183                    if span < best_span {
184                        best = Some(symbol);
185                        best_span = span;
186                    }
187                }
188            }
189        }
190        best
191    }
192
193    /// Find the definition of a symbol at a given position for Navigate workflows.
194    pub fn find_definition(&self, position: usize) -> Option<&Symbol> {
195        // First, find if there's a reference at this position
196        for refs in self.symbol_table.references.values() {
197            for reference in refs {
198                if reference.location.start <= position && reference.location.end >= position {
199                    let symbols = self.resolve_reference_to_symbols(reference);
200                    if let Some(first_symbol) = symbols.first() {
201                        return Some(first_symbol);
202                    }
203                }
204            }
205        }
206
207        // If no reference found, check if we're on a definition itself
208        self.symbol_at(SourceLocation { start: position, end: position })
209    }
210
211    /// Check if an operator is a file test operator.
212    ///
213    /// File test operators in Perl are unary operators that test file properties:
214    /// -e (exists), -d (directory), -f (file), -r (readable), -w (writable), etc.
215    ///
216    /// Delegates to `builtins::is_file_test_operator`.
217    pub fn is_file_test_operator(op: &str) -> bool {
218        builtins::is_file_test_operator(op)
219    }
220
221    /// Resolve hover info for a method by walking the same-file parent chain.
222    ///
223    /// Given a receiver package name and a method name, walks the `parents` of
224    /// each `ClassModel` in `self.class_models` (BFS) and returns `HoverInfo` for
225    /// the first class in the chain that defines the method.
226    ///
227    /// For packages not in `class_models` (plain packages with no OO indicators),
228    /// falls back to the symbol table looking for `PackageName::method_name`.
229    ///
230    /// Returns `None` when no ancestor defined in the same source file
231    /// defines the method. For cross-file inherited-method navigation, the
232    /// LSP layer (`inherited_method_definition_location` in `navigation.rs`)
233    /// handles workspace index traversal - this method is intentionally
234    /// scoped to the current file's `class_models` only.
235    pub fn resolve_inherited_method_hover(
236        &self,
237        receiver_class: &str,
238        method_name: &str,
239    ) -> Option<HoverInfo> {
240        let mut visited: Vec<String> = Vec::new();
241        let mut queue: Vec<String> = vec![receiver_class.to_string()];
242
243        while !queue.is_empty() {
244            let current = queue.remove(0);
245            if visited.contains(&current) {
246                continue;
247            }
248            visited.push(current.clone());
249
250            // Check class model first (has method list + parent chain)
251            if let Some(model) = self.class_models.iter().find(|m| m.name == current) {
252                if model.methods.iter().any(|m| m.name == method_name) {
253                    let is_direct = current == receiver_class;
254                    let details = if is_direct {
255                        vec![format!("Defined in {}", current)]
256                    } else {
257                        vec![format!("Inherited from {}", current)]
258                    };
259                    return Some(HoverInfo {
260                        signature: format!("sub {}::{}", current, method_name),
261                        documentation: None,
262                        details,
263                    });
264                }
265                for parent in &model.parents {
266                    if !visited.contains(parent) {
267                        queue.push(parent.clone());
268                    }
269                }
270            } else {
271                // Plain package not in class_models — check the symbol table
272                // for a sub with qualified_name == "PackageName::method_name"
273                let qualified = format!("{}::{}", current, method_name);
274                let found_in_table =
275                    self.symbol_table.symbols.get(method_name).is_some_and(|syms| {
276                        syms.iter().any(|s| {
277                            matches!(s.kind, crate::symbol::SymbolKind::Subroutine)
278                                && s.qualified_name == qualified
279                        })
280                    }) || self.symbol_table.symbols.contains_key(&qualified);
281
282                if found_in_table {
283                    let is_direct = current == receiver_class;
284                    let details = if is_direct {
285                        vec![format!("Defined in {}", current)]
286                    } else {
287                        vec![format!("Inherited from {}", current)]
288                    };
289                    return Some(HoverInfo {
290                        signature: format!("sub {}::{}", current, method_name),
291                        documentation: None,
292                        details,
293                    });
294                }
295            }
296        }
297        None
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use crate::ast::{Node, NodeKind};
305    use crate::parser::Parser;
306    use crate::symbol::SymbolKind;
307
308    #[test]
309    fn test_semantic_tokens() -> Result<(), Box<dyn std::error::Error>> {
310        let code = r#"
311my $x = 42;
312print $x;
313"#;
314
315        let mut parser = Parser::new(code);
316        let ast = parser.parse()?;
317
318        let analyzer = SemanticAnalyzer::analyze(&ast);
319        let tokens = analyzer.semantic_tokens();
320
321        // Phase 1 implementation (Issue #188) handles critical AST node types
322        // including VariableListDeclaration, Ternary, ArrayLiteral, HashLiteral,
323        // Try, and PhaseBlock nodes
324
325        // Check first $x is a declaration
326        let x_tokens: Vec<_> = tokens
327            .iter()
328            .filter(|t| {
329                matches!(
330                    t.token_type,
331                    SemanticTokenType::Variable | SemanticTokenType::VariableDeclaration
332                )
333            })
334            .collect();
335        assert!(!x_tokens.is_empty());
336        assert!(x_tokens[0].modifiers.contains(&SemanticTokenModifier::Declaration));
337        Ok(())
338    }
339
340    #[test]
341    fn test_hover_info() -> Result<(), Box<dyn std::error::Error>> {
342        let code = r#"
343sub foo {
344    return 42;
345}
346
347my $result = foo();
348"#;
349
350        let mut parser = Parser::new(code);
351        let ast = parser.parse()?;
352
353        let analyzer = SemanticAnalyzer::analyze(&ast);
354
355        // The hover info would be at specific locations
356        // In practice, we'd look up by position
357        assert!(!analyzer.hover_info.is_empty());
358        Ok(())
359    }
360
361    #[test]
362    fn test_hover_doc_from_pod() -> Result<(), Box<dyn std::error::Error>> {
363        let code = r#"
364# This is foo
365# More docs
366sub foo {
367    return 1;
368}
369"#;
370
371        let mut parser = Parser::new(code);
372        let ast = parser.parse()?;
373
374        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
375
376        // Find the symbol for foo and check its hover documentation
377        let sym = analyzer.symbol_table().symbols.get("foo").ok_or("symbol not found")?[0].clone();
378        let hover = analyzer.hover_at(sym.location).ok_or("hover not found")?;
379        assert!(hover.documentation.as_ref().ok_or("doc not found")?.contains("This is foo"));
380        Ok(())
381    }
382
383    #[test]
384    fn test_comment_doc_extraction() -> Result<(), Box<dyn std::error::Error>> {
385        let code = r#"
386# Adds two numbers
387sub add { 1 }
388"#;
389
390        let mut parser = Parser::new(code);
391        let ast = parser.parse()?;
392
393        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
394
395        let sub_symbols =
396            analyzer.symbol_table().find_symbol("add", 0, crate::symbol::SymbolKind::Subroutine);
397        assert!(!sub_symbols.is_empty());
398        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
399        assert_eq!(hover.documentation.as_deref(), Some("Adds two numbers"));
400        Ok(())
401    }
402
403    #[test]
404    fn test_cross_package_navigation() -> Result<(), Box<dyn std::error::Error>> {
405        let code = r#"
406package Foo {
407    # bar sub
408    sub bar { 42 }
409}
410
411package main;
412Foo::bar();
413"#;
414
415        let mut parser = Parser::new(code);
416        let ast = parser.parse()?;
417        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
418        let pos = code.find("Foo::bar").ok_or("Foo::bar not found")? + 5; // position within "bar"
419        let def = analyzer.find_definition(pos).ok_or("definition")?;
420        assert_eq!(def.name, "bar");
421
422        let hover = analyzer.hover_at(def.location).ok_or("hover not found")?;
423        assert!(hover.documentation.as_ref().ok_or("doc not found")?.contains("bar sub"));
424        Ok(())
425    }
426
427    #[test]
428    fn test_scope_identification() -> Result<(), Box<dyn std::error::Error>> {
429        let code = r#"
430my $x = 0;
431package Foo {
432    my $x = 1;
433    sub bar { return $x; }
434}
435my $y = $x;
436"#;
437
438        let mut parser = Parser::new(code);
439        let ast = parser.parse()?;
440        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
441
442        let inner_ref_pos = code.find("return $x").ok_or("return $x not found")? + "return ".len();
443        let inner_def = analyzer.find_definition(inner_ref_pos).ok_or("inner def not found")?;
444        let expected_inner = code.find("my $x = 1").ok_or("my $x = 1 not found")? + 3;
445        assert_eq!(inner_def.location.start, expected_inner);
446
447        let outer_ref_pos = code.rfind("$x;").ok_or("$x; not found")?;
448        let outer_def = analyzer.find_definition(outer_ref_pos).ok_or("outer def not found")?;
449        let expected_outer = code.find("my $x = 0").ok_or("my $x = 0 not found")? + 3;
450        assert_eq!(outer_def.location.start, expected_outer);
451        Ok(())
452    }
453
454    #[test]
455    fn test_pod_documentation_extraction() -> Result<(), Box<dyn std::error::Error>> {
456        // Test with a simple case that parses correctly
457        let code = r#"# Simple comment before sub
458sub documented_with_comment {
459    return "test";
460}
461"#;
462
463        let mut parser = Parser::new(code);
464        let ast = parser.parse()?;
465        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
466
467        let sub_symbols = analyzer.symbol_table().find_symbol(
468            "documented_with_comment",
469            0,
470            crate::symbol::SymbolKind::Subroutine,
471        );
472        assert!(!sub_symbols.is_empty());
473        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
474        let doc = hover.documentation.as_ref().ok_or("doc not found")?;
475        assert!(doc.contains("Simple comment before sub"));
476        Ok(())
477    }
478
479    #[test]
480    fn test_empty_source_handling() -> Result<(), Box<dyn std::error::Error>> {
481        let code = "";
482        let mut parser = Parser::new(code);
483        let ast = parser.parse()?;
484        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
485
486        // Should not crash with empty source
487        assert!(analyzer.semantic_tokens().is_empty());
488        assert!(analyzer.hover_info.is_empty());
489        Ok(())
490    }
491
492    #[test]
493    fn test_multiple_comment_lines() -> Result<(), Box<dyn std::error::Error>> {
494        let code = r#"
495# First comment
496# Second comment
497# Third comment
498sub multi_commented {
499    1;
500}
501"#;
502
503        let mut parser = Parser::new(code);
504        let ast = parser.parse()?;
505        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
506
507        let sub_symbols = analyzer.symbol_table().find_symbol(
508            "multi_commented",
509            0,
510            crate::symbol::SymbolKind::Subroutine,
511        );
512        assert!(!sub_symbols.is_empty());
513        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
514        let doc = hover.documentation.as_ref().ok_or("doc not found")?;
515        assert!(doc.contains("First comment"));
516        assert!(doc.contains("Second comment"));
517        assert!(doc.contains("Third comment"));
518        Ok(())
519    }
520
521    // SemanticModel tests
522    #[test]
523    fn test_semantic_model_build_and_tokens() -> Result<(), Box<dyn std::error::Error>> {
524        let code = r#"
525my $x = 42;
526my $y = 10;
527$x + $y;
528"#;
529        let mut parser = Parser::new(code);
530        let ast = parser.parse()?;
531
532        let model = SemanticModel::build(&ast, code);
533
534        // Should have semantic tokens
535        let tokens = model.tokens();
536        assert!(!tokens.is_empty(), "SemanticModel should provide tokens");
537
538        // Should have variable tokens
539        let var_tokens: Vec<_> = tokens
540            .iter()
541            .filter(|t| {
542                matches!(
543                    t.token_type,
544                    SemanticTokenType::Variable | SemanticTokenType::VariableDeclaration
545                )
546            })
547            .collect();
548        assert!(var_tokens.len() >= 2, "Should have at least 2 variable tokens");
549        Ok(())
550    }
551
552    #[test]
553    fn test_semantic_model_symbol_table_access() -> Result<(), Box<dyn std::error::Error>> {
554        let code = r#"
555my $x = 42;
556sub foo {
557    my $y = $x;
558}
559"#;
560        let mut parser = Parser::new(code);
561        let ast = parser.parse()?;
562
563        let model = SemanticModel::build(&ast, code);
564
565        // Should be able to access symbol table
566        let symbol_table = model.symbol_table();
567        let x_symbols = symbol_table.find_symbol("x", 0, SymbolKind::scalar());
568        assert!(!x_symbols.is_empty(), "Should find $x in symbol table");
569
570        let foo_symbols = symbol_table.find_symbol("foo", 0, SymbolKind::Subroutine);
571        assert!(!foo_symbols.is_empty(), "Should find sub foo in symbol table");
572        Ok(())
573    }
574
575    #[test]
576    fn test_semantic_model_hover_info() -> Result<(), Box<dyn std::error::Error>> {
577        let code = r#"
578# This is a documented variable
579my $documented = 42;
580"#;
581        let mut parser = Parser::new(code);
582        let ast = parser.parse()?;
583
584        let model = SemanticModel::build(&ast, code);
585
586        // Find the location of the variable declaration
587        let symbol_table = model.symbol_table();
588        let symbols = symbol_table.find_symbol("documented", 0, SymbolKind::scalar());
589        assert!(!symbols.is_empty(), "Should find $documented");
590
591        // Check if hover info is available
592        if let Some(hover) = model.hover_info_at(symbols[0].location) {
593            assert!(hover.signature.contains("documented"), "Hover should contain variable name");
594        }
595        // Note: hover_info_at might return None if no explicit hover was generated,
596        // which is acceptable for now
597        Ok(())
598    }
599
600    #[test]
601    fn test_analyzer_find_definition_scalar() -> Result<(), Box<dyn std::error::Error>> {
602        let code = "my $x = 1;\n$x + 2;\n";
603        let mut parser = Parser::new(code);
604        let ast = parser.parse()?;
605
606        // Use the same path SemanticModel uses to feed source
607        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
608
609        // Find the byte offset of the reference "$x" in the second line
610        let ref_line = code.lines().nth(1).ok_or("line 2 not found")?;
611        let line_offset = code.lines().next().ok_or("line 1 not found")?.len() + 1; // +1 for '\n'
612        let col_in_line = ref_line.find("$x").ok_or("could not find $x on line 2")?;
613        let ref_pos = line_offset + col_in_line;
614
615        let symbol =
616            analyzer.find_definition(ref_pos).ok_or("definition not found for $x reference")?;
617
618        // 1. Must be a scalar named "x"
619        assert_eq!(symbol.name, "x");
620        assert_eq!(symbol.kind, SymbolKind::scalar());
621
622        // 2. Declaration must come before reference
623        assert!(
624            symbol.location.start < ref_pos,
625            "Declaration {:?} should precede reference at byte {}",
626            symbol.location.start,
627            ref_pos
628        );
629        Ok(())
630    }
631
632    #[test]
633    fn test_semantic_model_definition_at() -> Result<(), Box<dyn std::error::Error>> {
634        let code = "my $x = 1;\n$x + 2;\n";
635        let mut parser = Parser::new(code);
636        let ast = parser.parse()?;
637
638        let model = SemanticModel::build(&ast, code);
639
640        // Compute the byte offset of the reference "$x" on the second line
641        let ref_line_index = 1;
642        let ref_line = code.lines().nth(ref_line_index).ok_or("line not found")?;
643        let col_in_line = ref_line.find("$x").ok_or("could not find $x")?;
644        let byte_offset = code
645            .lines()
646            .take(ref_line_index)
647            .map(|l| l.len() + 1) // +1 for '\n'
648            .sum::<usize>()
649            + col_in_line;
650
651        let definition = model.definition_at(byte_offset);
652        assert!(
653            definition.is_some(),
654            "definition_at returned None for $x reference at {}",
655            byte_offset
656        );
657        if let Some(symbol) = definition {
658            assert_eq!(symbol.name, "x");
659            assert_eq!(symbol.kind, SymbolKind::scalar());
660            assert!(
661                symbol.location.start < byte_offset,
662                "Declaration {:?} should precede reference at byte {}",
663                symbol.location.start,
664                byte_offset
665            );
666        }
667        Ok(())
668    }
669
670    #[test]
671    fn test_anonymous_subroutine_semantic_tokens() -> Result<(), Box<dyn std::error::Error>> {
672        let code = r#"
673my $closure = sub {
674    my $x = 42;
675    return $x + 1;
676};
677"#;
678
679        let mut parser = Parser::new(code);
680        let ast = parser.parse()?;
681        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
682
683        // Check that we have semantic tokens for the anonymous sub
684        let tokens = analyzer.semantic_tokens();
685
686        // Should have a keyword token for 'sub'
687        let sub_keywords: Vec<_> =
688            tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Keyword)).collect();
689
690        assert!(!sub_keywords.is_empty(), "Should have keyword token for 'sub'");
691
692        // Check hover info exists for the anonymous sub
693        let sub_position = code.find("sub {").ok_or("sub { not found")?;
694        let hover_exists = analyzer
695            .hover_info
696            .iter()
697            .any(|(loc, _)| loc.start <= sub_position && loc.end >= sub_position);
698
699        assert!(hover_exists, "Should have hover info for anonymous subroutine");
700        Ok(())
701    }
702
703    #[test]
704    fn test_infer_type_for_literals() -> Result<(), Box<dyn std::error::Error>> {
705        let code = r#"
706my $num = 42;
707my $str = "hello";
708my @arr = (1, 2, 3);
709my %hash = (a => 1);
710"#;
711
712        let mut parser = Parser::new(code);
713        let ast = parser.parse()?;
714        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
715
716        // Find nodes and test type inference
717        // We need to walk the AST to find the literal nodes
718        fn find_number_node(node: &Node) -> Option<&Node> {
719            match &node.kind {
720                NodeKind::Number { .. } => Some(node),
721                NodeKind::Program { statements } | NodeKind::Block { statements } => {
722                    for stmt in statements {
723                        if let Some(found) = find_number_node(stmt) {
724                            return Some(found);
725                        }
726                    }
727                    None
728                }
729                NodeKind::VariableDeclaration { initializer, .. } => {
730                    initializer.as_ref().and_then(|init| find_number_node(init))
731                }
732                _ => None,
733            }
734        }
735
736        if let Some(num_node) = find_number_node(&ast) {
737            let inferred = analyzer.infer_type(num_node);
738            assert_eq!(inferred, Some("number".to_string()), "Should infer number type");
739        }
740
741        Ok(())
742    }
743
744    #[test]
745    fn test_infer_type_for_binary_operations() -> Result<(), Box<dyn std::error::Error>> {
746        let code = r#"my $sum = 10 + 20;
747my $concat = "a" . "b";
748"#;
749
750        let mut parser = Parser::new(code);
751        let ast = parser.parse()?;
752        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
753
754        // Find binary operation nodes
755        fn find_binary_node<'a>(node: &'a Node, op: &str) -> Option<&'a Node> {
756            match &node.kind {
757                NodeKind::Binary { op: node_op, .. } if node_op == op => Some(node),
758                NodeKind::Program { statements } | NodeKind::Block { statements } => {
759                    for stmt in statements {
760                        if let Some(found) = find_binary_node(stmt, op) {
761                            return Some(found);
762                        }
763                    }
764                    None
765                }
766                NodeKind::VariableDeclaration { initializer, .. } => {
767                    initializer.as_ref().and_then(|init| find_binary_node(init, op))
768                }
769                _ => None,
770            }
771        }
772
773        // Test arithmetic operation infers to number
774        if let Some(add_node) = find_binary_node(&ast, "+") {
775            let inferred = analyzer.infer_type(add_node);
776            assert_eq!(inferred, Some("number".to_string()), "Arithmetic should infer to number");
777        }
778
779        // Test concatenation infers to string
780        if let Some(concat_node) = find_binary_node(&ast, ".") {
781            let inferred = analyzer.infer_type(concat_node);
782            assert_eq!(
783                inferred,
784                Some("string".to_string()),
785                "Concatenation should infer to string"
786            );
787        }
788
789        Ok(())
790    }
791
792    #[test]
793    fn test_anonymous_subroutine_hover_info() -> Result<(), Box<dyn std::error::Error>> {
794        let code = r#"
795# This is a closure
796my $adder = sub {
797    my ($x, $y) = @_;
798    return $x + $y;
799};
800"#;
801
802        let mut parser = Parser::new(code);
803        let ast = parser.parse()?;
804        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
805
806        // Find hover info for the anonymous sub
807        let sub_position = code.find("sub {").ok_or("sub { not found")?;
808        let hover = analyzer
809            .hover_info
810            .iter()
811            .find(|(loc, _)| loc.start <= sub_position && loc.end >= sub_position)
812            .map(|(_, h)| h);
813
814        assert!(hover.is_some(), "Should have hover info");
815
816        if let Some(h) = hover {
817            assert!(h.signature.contains("sub"), "Hover signature should contain 'sub'");
818            assert!(
819                h.details.iter().any(|d| d.contains("Anonymous")),
820                "Hover details should mention anonymous subroutine"
821            );
822            // Documentation extraction searches backwards from the sub keyword,
823            // but the comment is before `my $adder =` (not immediately before `sub`),
824            // so extract_documentation may not find it. Accept either outcome.
825            if let Some(doc) = &h.documentation {
826                assert!(
827                    doc.contains("closure"),
828                    "If documentation found, it should mention closure"
829                );
830            }
831        }
832        Ok(())
833    }
834
835    // Phase 2/3 Handler Tests
836    #[test]
837    fn test_substitution_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
838        let code = r#"
839my $str = "hello world";
840$str =~ s/world/Perl/;
841"#;
842        let mut parser = Parser::new(code);
843        let ast = parser.parse()?;
844        let analyzer = SemanticAnalyzer::analyze(&ast);
845
846        let tokens = analyzer.semantic_tokens();
847        let operator_tokens: Vec<_> =
848            tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
849
850        assert!(!operator_tokens.is_empty(), "Should have operator tokens for substitution");
851        Ok(())
852    }
853
854    #[test]
855    fn test_transliteration_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
856        let code = r#"
857my $str = "hello";
858$str =~ tr/el/ol/;
859"#;
860        let mut parser = Parser::new(code);
861        let ast = parser.parse()?;
862        let analyzer = SemanticAnalyzer::analyze(&ast);
863
864        let tokens = analyzer.semantic_tokens();
865        let operator_tokens: Vec<_> =
866            tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
867
868        assert!(!operator_tokens.is_empty(), "Should have operator tokens for transliteration");
869        Ok(())
870    }
871
872    #[test]
873    fn test_reference_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
874        let code = r#"
875my $x = 42;
876my $ref = \$x;
877"#;
878        let mut parser = Parser::new(code);
879        let ast = parser.parse()?;
880        let analyzer = SemanticAnalyzer::analyze(&ast);
881
882        let tokens = analyzer.semantic_tokens();
883        let operator_tokens: Vec<_> =
884            tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
885
886        assert!(!operator_tokens.is_empty(), "Should have operator tokens for reference operator");
887        Ok(())
888    }
889
890    #[test]
891    fn test_postfix_loop_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
892        let code = r#"
893my @list = (1, 2, 3);
894print $_ for @list;
895my $x = 0;
896$x++ while $x < 10;
897"#;
898        let mut parser = Parser::new(code);
899        let ast = parser.parse()?;
900        let analyzer = SemanticAnalyzer::analyze(&ast);
901
902        let tokens = analyzer.semantic_tokens();
903        let control_tokens: Vec<_> = tokens
904            .iter()
905            .filter(|t| matches!(t.token_type, SemanticTokenType::KeywordControl))
906            .collect();
907
908        assert!(!control_tokens.is_empty(), "Should have control keyword tokens for postfix loops");
909        Ok(())
910    }
911
912    #[test]
913    fn test_file_test_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
914        let code = r#"
915my $file = "test.txt";
916if (-e $file) {
917    print "exists";
918}
919if (-d $file) {
920    print "directory";
921}
922if (-f $file) {
923    print "file";
924}
925"#;
926        let mut parser = Parser::new(code);
927        let ast = parser.parse()?;
928        let analyzer = SemanticAnalyzer::analyze(&ast);
929
930        let tokens = analyzer.semantic_tokens();
931        let operator_tokens: Vec<_> =
932            tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
933
934        assert!(!operator_tokens.is_empty(), "Should have operator tokens for file test operators");
935        Ok(())
936    }
937
938    #[test]
939    fn test_all_file_test_operators_recognized() -> Result<(), Box<dyn std::error::Error>> {
940        // Test that the is_file_test_operator helper recognizes all file test operators
941        let file_test_ops = vec![
942            "-e", "-d", "-f", "-r", "-w", "-x", "-s", "-z", "-T", "-B", "-M", "-A", "-C", "-l",
943            "-p", "-S", "-u", "-g", "-k", "-t", "-O", "-G", "-R", "-b", "-c",
944        ];
945
946        for op in file_test_ops {
947            assert!(
948                SemanticAnalyzer::is_file_test_operator(op),
949                "Operator {} should be recognized as file test operator",
950                op
951            );
952        }
953
954        // Test that non-file-test operators are not recognized
955        assert!(
956            !SemanticAnalyzer::is_file_test_operator("+"),
957            "Operator '+' should not be recognized as file test operator"
958        );
959        assert!(
960            !SemanticAnalyzer::is_file_test_operator("-"),
961            "Operator '-' should not be recognized as file test operator"
962        );
963        assert!(
964            !SemanticAnalyzer::is_file_test_operator("++"),
965            "Operator '++' should not be recognized as file test operator"
966        );
967
968        Ok(())
969    }
970
971    #[test]
972    fn test_postfix_loop_modifiers() -> Result<(), Box<dyn std::error::Error>> {
973        let code = r#"
974my @items = (1, 2, 3);
975print $_ for @items;
976print $_ foreach @items;
977my $x = 0;
978$x++ while $x < 10;
979$x-- until $x < 0;
980"#;
981        let mut parser = Parser::new(code);
982        let ast = parser.parse()?;
983        let analyzer = SemanticAnalyzer::analyze(&ast);
984
985        let tokens = analyzer.semantic_tokens();
986        let control_tokens: Vec<_> = tokens
987            .iter()
988            .filter(|t| matches!(t.token_type, SemanticTokenType::KeywordControl))
989            .collect();
990
991        // Should have at least 4 control keyword tokens (for, foreach, while, until)
992        assert!(
993            control_tokens.len() >= 4,
994            "Should have at least 4 control keyword tokens for postfix loop modifiers"
995        );
996        Ok(())
997    }
998
999    #[test]
1000    fn test_substitution_with_modifiers() -> Result<(), Box<dyn std::error::Error>> {
1001        let code = r#"
1002my $str = "hello world";
1003$str =~ s/world/Perl/gi;
1004"#;
1005        let mut parser = Parser::new(code);
1006        let ast = parser.parse()?;
1007        let analyzer = SemanticAnalyzer::analyze(&ast);
1008
1009        let tokens = analyzer.semantic_tokens();
1010        let operator_tokens: Vec<_> =
1011            tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1012
1013        assert!(
1014            !operator_tokens.is_empty(),
1015            "Should have operator tokens for substitution with modifiers"
1016        );
1017        Ok(())
1018    }
1019
1020    #[test]
1021    fn test_transliteration_y_operator() -> Result<(), Box<dyn std::error::Error>> {
1022        let code = r#"
1023my $str = "hello";
1024$str =~ y/hello/world/;
1025"#;
1026        let mut parser = Parser::new(code);
1027        let ast = parser.parse()?;
1028        let analyzer = SemanticAnalyzer::analyze(&ast);
1029
1030        let tokens = analyzer.semantic_tokens();
1031        let operator_tokens: Vec<_> =
1032            tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1033
1034        assert!(
1035            !operator_tokens.is_empty(),
1036            "Should have operator tokens for y/// transliteration"
1037        );
1038        Ok(())
1039    }
1040
1041    #[test]
1042    fn test_builtin_documentation_coverage() -> Result<(), Box<dyn std::error::Error>> {
1043        // Verify that commonly-used builtins have documentation
1044        let builtins = [
1045            "print", "say", "push", "pop", "shift", "unshift", "map", "grep", "sort", "reverse",
1046            "split", "join", "chomp", "chop", "length", "substr", "index", "rindex", "lc", "uc",
1047            "die", "warn", "eval", "open", "close", "read", "keys", "values", "exists", "delete",
1048            "defined", "ref", "bless", "sprintf", "chr", "ord",
1049        ];
1050
1051        for name in &builtins {
1052            let doc = get_builtin_documentation(name);
1053            assert!(doc.is_some(), "Built-in '{}' should have documentation", name);
1054            let doc = doc.unwrap();
1055            assert!(
1056                !doc.signature.is_empty(),
1057                "Built-in '{}' should have a non-empty signature",
1058                name
1059            );
1060            assert!(
1061                !doc.description.is_empty(),
1062                "Built-in '{}' should have a non-empty description",
1063                name
1064            );
1065        }
1066        Ok(())
1067    }
1068
1069    #[test]
1070    fn test_builtin_hover_for_function_call() -> Result<(), Box<dyn std::error::Error>> {
1071        let code = r#"
1072my @items = (3, 1, 4);
1073push @items, 5;
1074"#;
1075        let mut parser = Parser::new(code);
1076        let ast = parser.parse()?;
1077        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1078
1079        // Find the hover info for 'push' function call
1080        let push_pos = code.find("push").ok_or("push not found")?;
1081        let hover_for_push =
1082            analyzer.hover_info.iter().find(|(loc, _)| loc.start <= push_pos && loc.end > push_pos);
1083
1084        assert!(hover_for_push.is_some(), "Should have hover info for 'push' builtin");
1085        let (_, hover) = hover_for_push.unwrap();
1086        assert!(
1087            hover.signature.contains("push"),
1088            "Hover signature should contain 'push', got: {}",
1089            hover.signature
1090        );
1091        assert!(hover.documentation.is_some(), "Hover for 'push' should have documentation");
1092        Ok(())
1093    }
1094
1095    #[test]
1096    fn test_package_hover_with_pod_name_section() -> Result<(), Box<dyn std::error::Error>> {
1097        let code = r#"
1098=head1 NAME
1099
1100My::Module - A great module for testing
1101
1102=head1 DESCRIPTION
1103
1104This module does great things.
1105
1106=cut
1107
1108package My::Module;
1109
1110sub new { bless {}, shift }
1111
11121;
1113"#;
1114        let mut parser = Parser::new(code);
1115        let ast = parser.parse()?;
1116        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1117
1118        // Find the package symbol
1119        let pkg_symbols = analyzer.symbol_table().symbols.get("My::Module");
1120        assert!(pkg_symbols.is_some(), "Should find My::Module in symbol table");
1121
1122        let pkg = &pkg_symbols.unwrap()[0];
1123        let hover = analyzer.hover_at(pkg.location);
1124        assert!(hover.is_some(), "Should have hover info for package");
1125
1126        let hover = hover.unwrap();
1127        assert!(
1128            hover.signature.contains("package My::Module"),
1129            "Package hover signature should contain 'package My::Module', got: {}",
1130            hover.signature
1131        );
1132        // The POD NAME section should be extracted as documentation
1133        if let Some(doc) = &hover.documentation {
1134            assert!(
1135                doc.contains("A great module for testing"),
1136                "Package hover should contain POD NAME content, got: {}",
1137                doc
1138            );
1139        }
1140        Ok(())
1141    }
1142
1143    #[test]
1144    fn test_package_documentation_via_symbol() -> Result<(), Box<dyn std::error::Error>> {
1145        let code = r#"
1146=head1 NAME
1147
1148Utils - Utility functions
1149
1150=cut
1151
1152package Utils;
1153
1154sub helper { 1 }
1155
11561;
1157"#;
1158        let mut parser = Parser::new(code);
1159        let ast = parser.parse()?;
1160        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1161
1162        let pkg_symbols = analyzer.symbol_table().symbols.get("Utils");
1163        assert!(pkg_symbols.is_some(), "Should find Utils package");
1164
1165        let pkg = &pkg_symbols.unwrap()[0];
1166        // The symbol extractor should have extracted POD docs
1167        assert!(
1168            pkg.documentation.is_some(),
1169            "Package symbol should have documentation from POD NAME section"
1170        );
1171        let doc = pkg.documentation.as_ref().unwrap();
1172        assert!(
1173            doc.contains("Utility functions"),
1174            "Package doc should contain 'Utility functions', got: {}",
1175            doc
1176        );
1177        Ok(())
1178    }
1179
1180    #[test]
1181    fn test_subroutine_with_pod_docs_hover() -> Result<(), Box<dyn std::error::Error>> {
1182        let code = r#"
1183=head2 process
1184
1185Processes input data and returns the result.
1186
1187=cut
1188
1189sub process {
1190    my ($input) = @_;
1191    return $input * 2;
1192}
1193"#;
1194        let mut parser = Parser::new(code);
1195        let ast = parser.parse()?;
1196        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1197
1198        let sub_symbols = analyzer.symbol_table().find_symbol("process", 0, SymbolKind::Subroutine);
1199        assert!(!sub_symbols.is_empty(), "Should find sub process");
1200
1201        let hover = analyzer.hover_at(sub_symbols[0].location);
1202        assert!(hover.is_some(), "Should have hover for sub process");
1203
1204        let hover = hover.unwrap();
1205        assert!(
1206            hover.signature.contains("sub process"),
1207            "Hover should show sub signature, got: {}",
1208            hover.signature
1209        );
1210        // The POD =head2 docs should be extracted
1211        if let Some(doc) = &hover.documentation {
1212            assert!(
1213                doc.contains("process") || doc.contains("Processes"),
1214                "Sub hover should contain POD documentation, got: {}",
1215                doc
1216            );
1217        }
1218        Ok(())
1219    }
1220
1221    #[test]
1222    fn test_variable_hover_shows_declaration_type() -> Result<(), Box<dyn std::error::Error>> {
1223        let code = r#"my $count = 42;
1224my @items = (1, 2, 3);
1225my %config = (key => "value");
1226"#;
1227        let mut parser = Parser::new(code);
1228        let ast = parser.parse()?;
1229        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1230
1231        // Check scalar variable hover
1232        let scalar_pos = code.find("$count").ok_or("$count not found")?;
1233        let scalar_hover = analyzer
1234            .hover_info
1235            .iter()
1236            .find(|(loc, _)| loc.start <= scalar_pos && loc.end > scalar_pos);
1237        assert!(scalar_hover.is_some(), "Should have hover for $count");
1238        let (_, hover) = scalar_hover.unwrap();
1239        assert!(
1240            hover.signature.contains("$count"),
1241            "Scalar hover should show variable name, got: {}",
1242            hover.signature
1243        );
1244
1245        // Check array variable hover
1246        let array_pos = code.find("@items").ok_or("@items not found")?;
1247        let array_hover = analyzer
1248            .hover_info
1249            .iter()
1250            .find(|(loc, _)| loc.start <= array_pos && loc.end > array_pos);
1251        assert!(array_hover.is_some(), "Should have hover for @items");
1252        let (_, hover) = array_hover.unwrap();
1253        assert!(
1254            hover.signature.contains("@items"),
1255            "Array hover should show variable name, got: {}",
1256            hover.signature
1257        );
1258
1259        // Check hash variable hover
1260        let hash_pos = code.find("%config").ok_or("%config not found")?;
1261        let hash_hover =
1262            analyzer.hover_info.iter().find(|(loc, _)| loc.start <= hash_pos && loc.end > hash_pos);
1263        assert!(hash_hover.is_some(), "Should have hover for %config");
1264        let (_, hover) = hash_hover.unwrap();
1265        assert!(
1266            hover.signature.contains("%config"),
1267            "Hash hover should show variable name, got: {}",
1268            hover.signature
1269        );
1270        Ok(())
1271    }
1272
1273    // -----------------------------------------------------------------------
1274    // Subroutine signature hover tests (issue #2353)
1275    // -----------------------------------------------------------------------
1276
1277    #[test]
1278    fn test_signature_hover_shows_param_names() -> Result<(), Box<dyn std::error::Error>> {
1279        // sub with named scalar parameters — hover must show them by name, not (...)
1280        let code = "sub add($x, $y) { $x + $y }";
1281        let mut parser = Parser::new(code);
1282        let ast = parser.parse()?;
1283        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1284
1285        let sub_symbols =
1286            analyzer.symbol_table().find_symbol("add", 0, crate::symbol::SymbolKind::Subroutine);
1287        assert!(!sub_symbols.is_empty(), "symbol 'add' not found");
1288
1289        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1290        assert!(
1291            hover.signature.contains("$x"),
1292            "hover signature should contain '$x', got: {}",
1293            hover.signature
1294        );
1295        assert!(
1296            hover.signature.contains("$y"),
1297            "hover signature should contain '$y', got: {}",
1298            hover.signature
1299        );
1300        assert!(
1301            !hover.signature.contains("(...)"),
1302            "hover signature must not fall back to '(...)', got: {}",
1303            hover.signature
1304        );
1305        Ok(())
1306    }
1307
1308    #[test]
1309    fn test_signature_hover_with_optional_param() -> Result<(), Box<dyn std::error::Error>> {
1310        // optional parameter with default value
1311        let code = "sub greet($name, $greeting = 'Hello') { \"$greeting, $name\" }";
1312        let mut parser = Parser::new(code);
1313        let ast = parser.parse()?;
1314        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1315
1316        let sub_symbols =
1317            analyzer.symbol_table().find_symbol("greet", 0, crate::symbol::SymbolKind::Subroutine);
1318        assert!(!sub_symbols.is_empty(), "symbol 'greet' not found");
1319
1320        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1321        assert!(
1322            hover.signature.contains("$name"),
1323            "hover signature should contain '$name', got: {}",
1324            hover.signature
1325        );
1326        assert!(
1327            hover.signature.contains("$greeting"),
1328            "hover signature should contain '$greeting', got: {}",
1329            hover.signature
1330        );
1331        Ok(())
1332    }
1333
1334    #[test]
1335    fn test_signature_hover_with_slurpy_param() -> Result<(), Box<dyn std::error::Error>> {
1336        // slurpy array parameter collects remaining args
1337        let code = "sub log_all($level, @messages) { print \"$level: @messages\" }";
1338        let mut parser = Parser::new(code);
1339        let ast = parser.parse()?;
1340        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1341
1342        let sub_symbols = analyzer.symbol_table().find_symbol(
1343            "log_all",
1344            0,
1345            crate::symbol::SymbolKind::Subroutine,
1346        );
1347        assert!(!sub_symbols.is_empty(), "symbol 'log_all' not found");
1348
1349        let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1350        assert!(
1351            hover.signature.contains("@messages"),
1352            "hover signature should contain '@messages', got: {}",
1353            hover.signature
1354        );
1355        Ok(())
1356    }
1357
1358    #[test]
1359    fn test_find_definition_returns_method_kind_for_native_method()
1360    -> Result<(), Box<dyn std::error::Error>> {
1361        let code = "class Foo {\n    method bar { return 1; }\n}\n";
1362        let mut parser = Parser::new(code);
1363        let ast = parser.parse()?;
1364
1365        let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1366
1367        // Find offset of "bar" in "method bar" on line 1
1368        let line1 = code.lines().nth(1).ok_or("no line 1")?;
1369        let line0_len = code.lines().next().ok_or("no line 0")?.len() + 1;
1370        let col = line1.find("bar").ok_or("bar not found on line 1")?;
1371        let offset = line0_len + col;
1372
1373        let sym = analyzer.find_definition(offset).ok_or("no symbol found at 'bar'")?;
1374        assert_eq!(sym.name, "bar", "symbol name should be 'bar'");
1375        assert_eq!(
1376            sym.kind,
1377            SymbolKind::Method,
1378            "native method should have SymbolKind::Method, got {:?}",
1379            sym.kind
1380        );
1381        Ok(())
1382    }
1383}