1mod builtins;
19mod hover;
20mod model;
21mod node_analysis;
22mod references;
23mod tokens;
24
25pub 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)]
41pub struct SemanticAnalyzer {
82 pub(super) symbol_table: SymbolTable,
84 pub(super) semantic_tokens: Vec<SemanticToken>,
86 pub(super) hover_info: HashMap<SourceLocation, HoverInfo>,
88 pub(super) source: String,
90 pub class_models: Vec<ClassModel>,
92}
93
94impl SemanticAnalyzer {
95 pub fn analyze(ast: &Node) -> Self {
112 Self::analyze_with_source(ast, "")
113 }
114
115 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 pub fn symbol_table(&self) -> &SymbolTable {
151 &self.symbol_table
152 }
153
154 pub fn semantic_tokens(&self) -> &[SemanticToken] {
156 &self.semantic_tokens
157 }
158
159 pub fn hover_at(&self, location: SourceLocation) -> Option<&HoverInfo> {
161 self.hover_info.get(&location)
162 }
163
164 pub fn all_hover_entries(&self) -> impl Iterator<Item = &HoverInfo> {
166 self.hover_info.values()
167 }
168
169 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 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 pub fn find_definition(&self, position: usize) -> Option<&Symbol> {
195 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 self.symbol_at(SourceLocation { start: position, end: position })
209 }
210
211 pub fn is_file_test_operator(op: &str) -> bool {
218 builtins::is_file_test_operator(op)
219 }
220
221 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(¤t) {
246 continue;
247 }
248 visited.push(current.clone());
249
250 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 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 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 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 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; 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 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 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 #[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 let tokens = model.tokens();
536 assert!(!tokens.is_empty(), "SemanticModel should provide tokens");
537
538 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 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 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 if let Some(hover) = model.hover_info_at(symbols[0].location) {
593 assert!(hover.signature.contains("documented"), "Hover should contain variable name");
594 }
595 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 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
608
609 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; 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 assert_eq!(symbol.name, "x");
620 assert_eq!(symbol.kind, SymbolKind::scalar());
621
622 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 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) .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 let tokens = analyzer.semantic_tokens();
685
686 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 #[test]
1278 fn test_signature_hover_shows_param_names() -> Result<(), Box<dyn std::error::Error>> {
1279 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 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 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 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}