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(
233 &self,
234 receiver_class: &str,
235 method_name: &str,
236 ) -> Option<HoverInfo> {
237 let mut visited: Vec<String> = Vec::new();
238 let mut queue: Vec<String> = vec![receiver_class.to_string()];
239
240 while !queue.is_empty() {
241 let current = queue.remove(0);
242 if visited.contains(¤t) {
243 continue;
244 }
245 visited.push(current.clone());
246
247 if let Some(model) = self.class_models.iter().find(|m| m.name == current) {
249 if model.methods.iter().any(|m| m.name == method_name) {
250 let is_direct = current == receiver_class;
251 let details = if is_direct {
252 vec![format!("Defined in {}", current)]
253 } else {
254 vec![format!("Inherited from {}", current)]
255 };
256 return Some(HoverInfo {
257 signature: format!("sub {}::{}", current, method_name),
258 documentation: None,
259 details,
260 });
261 }
262 for parent in &model.parents {
263 if !visited.contains(parent) {
264 queue.push(parent.clone());
265 }
266 }
267 } else {
268 let qualified = format!("{}::{}", current, method_name);
271 let found_in_table =
272 self.symbol_table.symbols.get(method_name).is_some_and(|syms| {
273 syms.iter().any(|s| {
274 matches!(s.kind, crate::symbol::SymbolKind::Subroutine)
275 && s.qualified_name == qualified
276 })
277 }) || self.symbol_table.symbols.contains_key(&qualified);
278
279 if found_in_table {
280 let is_direct = current == receiver_class;
281 let details = if is_direct {
282 vec![format!("Defined in {}", current)]
283 } else {
284 vec![format!("Inherited from {}", current)]
285 };
286 return Some(HoverInfo {
287 signature: format!("sub {}::{}", current, method_name),
288 documentation: None,
289 details,
290 });
291 }
292 }
293 }
294 None
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use crate::ast::{Node, NodeKind};
302 use crate::parser::Parser;
303 use crate::symbol::SymbolKind;
304
305 #[test]
306 fn test_semantic_tokens() -> Result<(), Box<dyn std::error::Error>> {
307 let code = r#"
308my $x = 42;
309print $x;
310"#;
311
312 let mut parser = Parser::new(code);
313 let ast = parser.parse()?;
314
315 let analyzer = SemanticAnalyzer::analyze(&ast);
316 let tokens = analyzer.semantic_tokens();
317
318 let x_tokens: Vec<_> = tokens
324 .iter()
325 .filter(|t| {
326 matches!(
327 t.token_type,
328 SemanticTokenType::Variable | SemanticTokenType::VariableDeclaration
329 )
330 })
331 .collect();
332 assert!(!x_tokens.is_empty());
333 assert!(x_tokens[0].modifiers.contains(&SemanticTokenModifier::Declaration));
334 Ok(())
335 }
336
337 #[test]
338 fn test_hover_info() -> Result<(), Box<dyn std::error::Error>> {
339 let code = r#"
340sub foo {
341 return 42;
342}
343
344my $result = foo();
345"#;
346
347 let mut parser = Parser::new(code);
348 let ast = parser.parse()?;
349
350 let analyzer = SemanticAnalyzer::analyze(&ast);
351
352 assert!(!analyzer.hover_info.is_empty());
355 Ok(())
356 }
357
358 #[test]
359 fn test_hover_doc_from_pod() -> Result<(), Box<dyn std::error::Error>> {
360 let code = r#"
361# This is foo
362# More docs
363sub foo {
364 return 1;
365}
366"#;
367
368 let mut parser = Parser::new(code);
369 let ast = parser.parse()?;
370
371 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
372
373 let sym = analyzer.symbol_table().symbols.get("foo").ok_or("symbol not found")?[0].clone();
375 let hover = analyzer.hover_at(sym.location).ok_or("hover not found")?;
376 assert!(hover.documentation.as_ref().ok_or("doc not found")?.contains("This is foo"));
377 Ok(())
378 }
379
380 #[test]
381 fn test_comment_doc_extraction() -> Result<(), Box<dyn std::error::Error>> {
382 let code = r#"
383# Adds two numbers
384sub add { 1 }
385"#;
386
387 let mut parser = Parser::new(code);
388 let ast = parser.parse()?;
389
390 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
391
392 let sub_symbols =
393 analyzer.symbol_table().find_symbol("add", 0, crate::symbol::SymbolKind::Subroutine);
394 assert!(!sub_symbols.is_empty());
395 let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
396 assert_eq!(hover.documentation.as_deref(), Some("Adds two numbers"));
397 Ok(())
398 }
399
400 #[test]
401 fn test_cross_package_navigation() -> Result<(), Box<dyn std::error::Error>> {
402 let code = r#"
403package Foo {
404 # bar sub
405 sub bar { 42 }
406}
407
408package main;
409Foo::bar();
410"#;
411
412 let mut parser = Parser::new(code);
413 let ast = parser.parse()?;
414 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
415 let pos = code.find("Foo::bar").ok_or("Foo::bar not found")? + 5; let def = analyzer.find_definition(pos).ok_or("definition")?;
417 assert_eq!(def.name, "bar");
418
419 let hover = analyzer.hover_at(def.location).ok_or("hover not found")?;
420 assert!(hover.documentation.as_ref().ok_or("doc not found")?.contains("bar sub"));
421 Ok(())
422 }
423
424 #[test]
425 fn test_scope_identification() -> Result<(), Box<dyn std::error::Error>> {
426 let code = r#"
427my $x = 0;
428package Foo {
429 my $x = 1;
430 sub bar { return $x; }
431}
432my $y = $x;
433"#;
434
435 let mut parser = Parser::new(code);
436 let ast = parser.parse()?;
437 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
438
439 let inner_ref_pos = code.find("return $x").ok_or("return $x not found")? + "return ".len();
440 let inner_def = analyzer.find_definition(inner_ref_pos).ok_or("inner def not found")?;
441 let expected_inner = code.find("my $x = 1").ok_or("my $x = 1 not found")? + 3;
442 assert_eq!(inner_def.location.start, expected_inner);
443
444 let outer_ref_pos = code.rfind("$x;").ok_or("$x; not found")?;
445 let outer_def = analyzer.find_definition(outer_ref_pos).ok_or("outer def not found")?;
446 let expected_outer = code.find("my $x = 0").ok_or("my $x = 0 not found")? + 3;
447 assert_eq!(outer_def.location.start, expected_outer);
448 Ok(())
449 }
450
451 #[test]
452 fn test_pod_documentation_extraction() -> Result<(), Box<dyn std::error::Error>> {
453 let code = r#"# Simple comment before sub
455sub documented_with_comment {
456 return "test";
457}
458"#;
459
460 let mut parser = Parser::new(code);
461 let ast = parser.parse()?;
462 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
463
464 let sub_symbols = analyzer.symbol_table().find_symbol(
465 "documented_with_comment",
466 0,
467 crate::symbol::SymbolKind::Subroutine,
468 );
469 assert!(!sub_symbols.is_empty());
470 let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
471 let doc = hover.documentation.as_ref().ok_or("doc not found")?;
472 assert!(doc.contains("Simple comment before sub"));
473 Ok(())
474 }
475
476 #[test]
477 fn test_empty_source_handling() -> Result<(), Box<dyn std::error::Error>> {
478 let code = "";
479 let mut parser = Parser::new(code);
480 let ast = parser.parse()?;
481 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
482
483 assert!(analyzer.semantic_tokens().is_empty());
485 assert!(analyzer.hover_info.is_empty());
486 Ok(())
487 }
488
489 #[test]
490 fn test_multiple_comment_lines() -> Result<(), Box<dyn std::error::Error>> {
491 let code = r#"
492# First comment
493# Second comment
494# Third comment
495sub multi_commented {
496 1;
497}
498"#;
499
500 let mut parser = Parser::new(code);
501 let ast = parser.parse()?;
502 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
503
504 let sub_symbols = analyzer.symbol_table().find_symbol(
505 "multi_commented",
506 0,
507 crate::symbol::SymbolKind::Subroutine,
508 );
509 assert!(!sub_symbols.is_empty());
510 let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
511 let doc = hover.documentation.as_ref().ok_or("doc not found")?;
512 assert!(doc.contains("First comment"));
513 assert!(doc.contains("Second comment"));
514 assert!(doc.contains("Third comment"));
515 Ok(())
516 }
517
518 #[test]
520 fn test_semantic_model_build_and_tokens() -> Result<(), Box<dyn std::error::Error>> {
521 let code = r#"
522my $x = 42;
523my $y = 10;
524$x + $y;
525"#;
526 let mut parser = Parser::new(code);
527 let ast = parser.parse()?;
528
529 let model = SemanticModel::build(&ast, code);
530
531 let tokens = model.tokens();
533 assert!(!tokens.is_empty(), "SemanticModel should provide tokens");
534
535 let var_tokens: Vec<_> = tokens
537 .iter()
538 .filter(|t| {
539 matches!(
540 t.token_type,
541 SemanticTokenType::Variable | SemanticTokenType::VariableDeclaration
542 )
543 })
544 .collect();
545 assert!(var_tokens.len() >= 2, "Should have at least 2 variable tokens");
546 Ok(())
547 }
548
549 #[test]
550 fn test_semantic_model_symbol_table_access() -> Result<(), Box<dyn std::error::Error>> {
551 let code = r#"
552my $x = 42;
553sub foo {
554 my $y = $x;
555}
556"#;
557 let mut parser = Parser::new(code);
558 let ast = parser.parse()?;
559
560 let model = SemanticModel::build(&ast, code);
561
562 let symbol_table = model.symbol_table();
564 let x_symbols = symbol_table.find_symbol("x", 0, SymbolKind::scalar());
565 assert!(!x_symbols.is_empty(), "Should find $x in symbol table");
566
567 let foo_symbols = symbol_table.find_symbol("foo", 0, SymbolKind::Subroutine);
568 assert!(!foo_symbols.is_empty(), "Should find sub foo in symbol table");
569 Ok(())
570 }
571
572 #[test]
573 fn test_semantic_model_hover_info() -> Result<(), Box<dyn std::error::Error>> {
574 let code = r#"
575# This is a documented variable
576my $documented = 42;
577"#;
578 let mut parser = Parser::new(code);
579 let ast = parser.parse()?;
580
581 let model = SemanticModel::build(&ast, code);
582
583 let symbol_table = model.symbol_table();
585 let symbols = symbol_table.find_symbol("documented", 0, SymbolKind::scalar());
586 assert!(!symbols.is_empty(), "Should find $documented");
587
588 if let Some(hover) = model.hover_info_at(symbols[0].location) {
590 assert!(hover.signature.contains("documented"), "Hover should contain variable name");
591 }
592 Ok(())
595 }
596
597 #[test]
598 fn test_analyzer_find_definition_scalar() -> Result<(), Box<dyn std::error::Error>> {
599 let code = "my $x = 1;\n$x + 2;\n";
600 let mut parser = Parser::new(code);
601 let ast = parser.parse()?;
602
603 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
605
606 let ref_line = code.lines().nth(1).ok_or("line 2 not found")?;
608 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")?;
610 let ref_pos = line_offset + col_in_line;
611
612 let symbol =
613 analyzer.find_definition(ref_pos).ok_or("definition not found for $x reference")?;
614
615 assert_eq!(symbol.name, "x");
617 assert_eq!(symbol.kind, SymbolKind::scalar());
618
619 assert!(
621 symbol.location.start < ref_pos,
622 "Declaration {:?} should precede reference at byte {}",
623 symbol.location.start,
624 ref_pos
625 );
626 Ok(())
627 }
628
629 #[test]
630 fn test_semantic_model_definition_at() -> Result<(), Box<dyn std::error::Error>> {
631 let code = "my $x = 1;\n$x + 2;\n";
632 let mut parser = Parser::new(code);
633 let ast = parser.parse()?;
634
635 let model = SemanticModel::build(&ast, code);
636
637 let ref_line_index = 1;
639 let ref_line = code.lines().nth(ref_line_index).ok_or("line not found")?;
640 let col_in_line = ref_line.find("$x").ok_or("could not find $x")?;
641 let byte_offset = code
642 .lines()
643 .take(ref_line_index)
644 .map(|l| l.len() + 1) .sum::<usize>()
646 + col_in_line;
647
648 let definition = model.definition_at(byte_offset);
649 assert!(
650 definition.is_some(),
651 "definition_at returned None for $x reference at {}",
652 byte_offset
653 );
654 if let Some(symbol) = definition {
655 assert_eq!(symbol.name, "x");
656 assert_eq!(symbol.kind, SymbolKind::scalar());
657 assert!(
658 symbol.location.start < byte_offset,
659 "Declaration {:?} should precede reference at byte {}",
660 symbol.location.start,
661 byte_offset
662 );
663 }
664 Ok(())
665 }
666
667 #[test]
668 fn test_anonymous_subroutine_semantic_tokens() -> Result<(), Box<dyn std::error::Error>> {
669 let code = r#"
670my $closure = sub {
671 my $x = 42;
672 return $x + 1;
673};
674"#;
675
676 let mut parser = Parser::new(code);
677 let ast = parser.parse()?;
678 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
679
680 let tokens = analyzer.semantic_tokens();
682
683 let sub_keywords: Vec<_> =
685 tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Keyword)).collect();
686
687 assert!(!sub_keywords.is_empty(), "Should have keyword token for 'sub'");
688
689 let sub_position = code.find("sub {").ok_or("sub { not found")?;
691 let hover_exists = analyzer
692 .hover_info
693 .iter()
694 .any(|(loc, _)| loc.start <= sub_position && loc.end >= sub_position);
695
696 assert!(hover_exists, "Should have hover info for anonymous subroutine");
697 Ok(())
698 }
699
700 #[test]
701 fn test_infer_type_for_literals() -> Result<(), Box<dyn std::error::Error>> {
702 let code = r#"
703my $num = 42;
704my $str = "hello";
705my @arr = (1, 2, 3);
706my %hash = (a => 1);
707"#;
708
709 let mut parser = Parser::new(code);
710 let ast = parser.parse()?;
711 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
712
713 fn find_number_node(node: &Node) -> Option<&Node> {
716 match &node.kind {
717 NodeKind::Number { .. } => Some(node),
718 NodeKind::Program { statements } | NodeKind::Block { statements } => {
719 for stmt in statements {
720 if let Some(found) = find_number_node(stmt) {
721 return Some(found);
722 }
723 }
724 None
725 }
726 NodeKind::VariableDeclaration { initializer, .. } => {
727 initializer.as_ref().and_then(|init| find_number_node(init))
728 }
729 _ => None,
730 }
731 }
732
733 if let Some(num_node) = find_number_node(&ast) {
734 let inferred = analyzer.infer_type(num_node);
735 assert_eq!(inferred, Some("number".to_string()), "Should infer number type");
736 }
737
738 Ok(())
739 }
740
741 #[test]
742 fn test_infer_type_for_binary_operations() -> Result<(), Box<dyn std::error::Error>> {
743 let code = r#"my $sum = 10 + 20;
744my $concat = "a" . "b";
745"#;
746
747 let mut parser = Parser::new(code);
748 let ast = parser.parse()?;
749 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
750
751 fn find_binary_node<'a>(node: &'a Node, op: &str) -> Option<&'a Node> {
753 match &node.kind {
754 NodeKind::Binary { op: node_op, .. } if node_op == op => Some(node),
755 NodeKind::Program { statements } | NodeKind::Block { statements } => {
756 for stmt in statements {
757 if let Some(found) = find_binary_node(stmt, op) {
758 return Some(found);
759 }
760 }
761 None
762 }
763 NodeKind::VariableDeclaration { initializer, .. } => {
764 initializer.as_ref().and_then(|init| find_binary_node(init, op))
765 }
766 _ => None,
767 }
768 }
769
770 if let Some(add_node) = find_binary_node(&ast, "+") {
772 let inferred = analyzer.infer_type(add_node);
773 assert_eq!(inferred, Some("number".to_string()), "Arithmetic should infer to number");
774 }
775
776 if let Some(concat_node) = find_binary_node(&ast, ".") {
778 let inferred = analyzer.infer_type(concat_node);
779 assert_eq!(
780 inferred,
781 Some("string".to_string()),
782 "Concatenation should infer to string"
783 );
784 }
785
786 Ok(())
787 }
788
789 #[test]
790 fn test_anonymous_subroutine_hover_info() -> Result<(), Box<dyn std::error::Error>> {
791 let code = r#"
792# This is a closure
793my $adder = sub {
794 my ($x, $y) = @_;
795 return $x + $y;
796};
797"#;
798
799 let mut parser = Parser::new(code);
800 let ast = parser.parse()?;
801 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
802
803 let sub_position = code.find("sub {").ok_or("sub { not found")?;
805 let hover = analyzer
806 .hover_info
807 .iter()
808 .find(|(loc, _)| loc.start <= sub_position && loc.end >= sub_position)
809 .map(|(_, h)| h);
810
811 assert!(hover.is_some(), "Should have hover info");
812
813 if let Some(h) = hover {
814 assert!(h.signature.contains("sub"), "Hover signature should contain 'sub'");
815 assert!(
816 h.details.iter().any(|d| d.contains("Anonymous")),
817 "Hover details should mention anonymous subroutine"
818 );
819 if let Some(doc) = &h.documentation {
823 assert!(
824 doc.contains("closure"),
825 "If documentation found, it should mention closure"
826 );
827 }
828 }
829 Ok(())
830 }
831
832 #[test]
834 fn test_substitution_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
835 let code = r#"
836my $str = "hello world";
837$str =~ s/world/Perl/;
838"#;
839 let mut parser = Parser::new(code);
840 let ast = parser.parse()?;
841 let analyzer = SemanticAnalyzer::analyze(&ast);
842
843 let tokens = analyzer.semantic_tokens();
844 let operator_tokens: Vec<_> =
845 tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
846
847 assert!(!operator_tokens.is_empty(), "Should have operator tokens for substitution");
848 Ok(())
849 }
850
851 #[test]
852 fn test_transliteration_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
853 let code = r#"
854my $str = "hello";
855$str =~ tr/el/ol/;
856"#;
857 let mut parser = Parser::new(code);
858 let ast = parser.parse()?;
859 let analyzer = SemanticAnalyzer::analyze(&ast);
860
861 let tokens = analyzer.semantic_tokens();
862 let operator_tokens: Vec<_> =
863 tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
864
865 assert!(!operator_tokens.is_empty(), "Should have operator tokens for transliteration");
866 Ok(())
867 }
868
869 #[test]
870 fn test_reference_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
871 let code = r#"
872my $x = 42;
873my $ref = \$x;
874"#;
875 let mut parser = Parser::new(code);
876 let ast = parser.parse()?;
877 let analyzer = SemanticAnalyzer::analyze(&ast);
878
879 let tokens = analyzer.semantic_tokens();
880 let operator_tokens: Vec<_> =
881 tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
882
883 assert!(!operator_tokens.is_empty(), "Should have operator tokens for reference operator");
884 Ok(())
885 }
886
887 #[test]
888 fn test_postfix_loop_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
889 let code = r#"
890my @list = (1, 2, 3);
891print $_ for @list;
892my $x = 0;
893$x++ while $x < 10;
894"#;
895 let mut parser = Parser::new(code);
896 let ast = parser.parse()?;
897 let analyzer = SemanticAnalyzer::analyze(&ast);
898
899 let tokens = analyzer.semantic_tokens();
900 let control_tokens: Vec<_> = tokens
901 .iter()
902 .filter(|t| matches!(t.token_type, SemanticTokenType::KeywordControl))
903 .collect();
904
905 assert!(!control_tokens.is_empty(), "Should have control keyword tokens for postfix loops");
906 Ok(())
907 }
908
909 #[test]
910 fn test_file_test_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
911 let code = r#"
912my $file = "test.txt";
913if (-e $file) {
914 print "exists";
915}
916if (-d $file) {
917 print "directory";
918}
919if (-f $file) {
920 print "file";
921}
922"#;
923 let mut parser = Parser::new(code);
924 let ast = parser.parse()?;
925 let analyzer = SemanticAnalyzer::analyze(&ast);
926
927 let tokens = analyzer.semantic_tokens();
928 let operator_tokens: Vec<_> =
929 tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
930
931 assert!(!operator_tokens.is_empty(), "Should have operator tokens for file test operators");
932 Ok(())
933 }
934
935 #[test]
936 fn test_all_file_test_operators_recognized() -> Result<(), Box<dyn std::error::Error>> {
937 let file_test_ops = vec![
939 "-e", "-d", "-f", "-r", "-w", "-x", "-s", "-z", "-T", "-B", "-M", "-A", "-C", "-l",
940 "-p", "-S", "-u", "-g", "-k", "-t", "-O", "-G", "-R", "-b", "-c",
941 ];
942
943 for op in file_test_ops {
944 assert!(
945 SemanticAnalyzer::is_file_test_operator(op),
946 "Operator {} should be recognized as file test operator",
947 op
948 );
949 }
950
951 assert!(
953 !SemanticAnalyzer::is_file_test_operator("+"),
954 "Operator '+' should not be recognized as file test operator"
955 );
956 assert!(
957 !SemanticAnalyzer::is_file_test_operator("-"),
958 "Operator '-' should not be recognized as file test operator"
959 );
960 assert!(
961 !SemanticAnalyzer::is_file_test_operator("++"),
962 "Operator '++' should not be recognized as file test operator"
963 );
964
965 Ok(())
966 }
967
968 #[test]
969 fn test_postfix_loop_modifiers() -> Result<(), Box<dyn std::error::Error>> {
970 let code = r#"
971my @items = (1, 2, 3);
972print $_ for @items;
973print $_ foreach @items;
974my $x = 0;
975$x++ while $x < 10;
976$x-- until $x < 0;
977"#;
978 let mut parser = Parser::new(code);
979 let ast = parser.parse()?;
980 let analyzer = SemanticAnalyzer::analyze(&ast);
981
982 let tokens = analyzer.semantic_tokens();
983 let control_tokens: Vec<_> = tokens
984 .iter()
985 .filter(|t| matches!(t.token_type, SemanticTokenType::KeywordControl))
986 .collect();
987
988 assert!(
990 control_tokens.len() >= 4,
991 "Should have at least 4 control keyword tokens for postfix loop modifiers"
992 );
993 Ok(())
994 }
995
996 #[test]
997 fn test_substitution_with_modifiers() -> Result<(), Box<dyn std::error::Error>> {
998 let code = r#"
999my $str = "hello world";
1000$str =~ s/world/Perl/gi;
1001"#;
1002 let mut parser = Parser::new(code);
1003 let ast = parser.parse()?;
1004 let analyzer = SemanticAnalyzer::analyze(&ast);
1005
1006 let tokens = analyzer.semantic_tokens();
1007 let operator_tokens: Vec<_> =
1008 tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1009
1010 assert!(
1011 !operator_tokens.is_empty(),
1012 "Should have operator tokens for substitution with modifiers"
1013 );
1014 Ok(())
1015 }
1016
1017 #[test]
1018 fn test_transliteration_y_operator() -> Result<(), Box<dyn std::error::Error>> {
1019 let code = r#"
1020my $str = "hello";
1021$str =~ y/hello/world/;
1022"#;
1023 let mut parser = Parser::new(code);
1024 let ast = parser.parse()?;
1025 let analyzer = SemanticAnalyzer::analyze(&ast);
1026
1027 let tokens = analyzer.semantic_tokens();
1028 let operator_tokens: Vec<_> =
1029 tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1030
1031 assert!(
1032 !operator_tokens.is_empty(),
1033 "Should have operator tokens for y/// transliteration"
1034 );
1035 Ok(())
1036 }
1037
1038 #[test]
1039 fn test_builtin_documentation_coverage() -> Result<(), Box<dyn std::error::Error>> {
1040 let builtins = [
1042 "print", "say", "push", "pop", "shift", "unshift", "map", "grep", "sort", "reverse",
1043 "split", "join", "chomp", "chop", "length", "substr", "index", "rindex", "lc", "uc",
1044 "die", "warn", "eval", "open", "close", "read", "keys", "values", "exists", "delete",
1045 "defined", "ref", "bless", "sprintf", "chr", "ord",
1046 ];
1047
1048 for name in &builtins {
1049 let doc = get_builtin_documentation(name);
1050 assert!(doc.is_some(), "Built-in '{}' should have documentation", name);
1051 let doc = doc.unwrap();
1052 assert!(
1053 !doc.signature.is_empty(),
1054 "Built-in '{}' should have a non-empty signature",
1055 name
1056 );
1057 assert!(
1058 !doc.description.is_empty(),
1059 "Built-in '{}' should have a non-empty description",
1060 name
1061 );
1062 }
1063 Ok(())
1064 }
1065
1066 #[test]
1067 fn test_builtin_hover_for_function_call() -> Result<(), Box<dyn std::error::Error>> {
1068 let code = r#"
1069my @items = (3, 1, 4);
1070push @items, 5;
1071"#;
1072 let mut parser = Parser::new(code);
1073 let ast = parser.parse()?;
1074 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1075
1076 let push_pos = code.find("push").ok_or("push not found")?;
1078 let hover_for_push =
1079 analyzer.hover_info.iter().find(|(loc, _)| loc.start <= push_pos && loc.end > push_pos);
1080
1081 assert!(hover_for_push.is_some(), "Should have hover info for 'push' builtin");
1082 let (_, hover) = hover_for_push.unwrap();
1083 assert!(
1084 hover.signature.contains("push"),
1085 "Hover signature should contain 'push', got: {}",
1086 hover.signature
1087 );
1088 assert!(hover.documentation.is_some(), "Hover for 'push' should have documentation");
1089 Ok(())
1090 }
1091
1092 #[test]
1093 fn test_package_hover_with_pod_name_section() -> Result<(), Box<dyn std::error::Error>> {
1094 let code = r#"
1095=head1 NAME
1096
1097My::Module - A great module for testing
1098
1099=head1 DESCRIPTION
1100
1101This module does great things.
1102
1103=cut
1104
1105package My::Module;
1106
1107sub new { bless {}, shift }
1108
11091;
1110"#;
1111 let mut parser = Parser::new(code);
1112 let ast = parser.parse()?;
1113 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1114
1115 let pkg_symbols = analyzer.symbol_table().symbols.get("My::Module");
1117 assert!(pkg_symbols.is_some(), "Should find My::Module in symbol table");
1118
1119 let pkg = &pkg_symbols.unwrap()[0];
1120 let hover = analyzer.hover_at(pkg.location);
1121 assert!(hover.is_some(), "Should have hover info for package");
1122
1123 let hover = hover.unwrap();
1124 assert!(
1125 hover.signature.contains("package My::Module"),
1126 "Package hover signature should contain 'package My::Module', got: {}",
1127 hover.signature
1128 );
1129 if let Some(doc) = &hover.documentation {
1131 assert!(
1132 doc.contains("A great module for testing"),
1133 "Package hover should contain POD NAME content, got: {}",
1134 doc
1135 );
1136 }
1137 Ok(())
1138 }
1139
1140 #[test]
1141 fn test_package_documentation_via_symbol() -> Result<(), Box<dyn std::error::Error>> {
1142 let code = r#"
1143=head1 NAME
1144
1145Utils - Utility functions
1146
1147=cut
1148
1149package Utils;
1150
1151sub helper { 1 }
1152
11531;
1154"#;
1155 let mut parser = Parser::new(code);
1156 let ast = parser.parse()?;
1157 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1158
1159 let pkg_symbols = analyzer.symbol_table().symbols.get("Utils");
1160 assert!(pkg_symbols.is_some(), "Should find Utils package");
1161
1162 let pkg = &pkg_symbols.unwrap()[0];
1163 assert!(
1165 pkg.documentation.is_some(),
1166 "Package symbol should have documentation from POD NAME section"
1167 );
1168 let doc = pkg.documentation.as_ref().unwrap();
1169 assert!(
1170 doc.contains("Utility functions"),
1171 "Package doc should contain 'Utility functions', got: {}",
1172 doc
1173 );
1174 Ok(())
1175 }
1176
1177 #[test]
1178 fn test_subroutine_with_pod_docs_hover() -> Result<(), Box<dyn std::error::Error>> {
1179 let code = r#"
1180=head2 process
1181
1182Processes input data and returns the result.
1183
1184=cut
1185
1186sub process {
1187 my ($input) = @_;
1188 return $input * 2;
1189}
1190"#;
1191 let mut parser = Parser::new(code);
1192 let ast = parser.parse()?;
1193 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1194
1195 let sub_symbols = analyzer.symbol_table().find_symbol("process", 0, SymbolKind::Subroutine);
1196 assert!(!sub_symbols.is_empty(), "Should find sub process");
1197
1198 let hover = analyzer.hover_at(sub_symbols[0].location);
1199 assert!(hover.is_some(), "Should have hover for sub process");
1200
1201 let hover = hover.unwrap();
1202 assert!(
1203 hover.signature.contains("sub process"),
1204 "Hover should show sub signature, got: {}",
1205 hover.signature
1206 );
1207 if let Some(doc) = &hover.documentation {
1209 assert!(
1210 doc.contains("process") || doc.contains("Processes"),
1211 "Sub hover should contain POD documentation, got: {}",
1212 doc
1213 );
1214 }
1215 Ok(())
1216 }
1217
1218 #[test]
1219 fn test_variable_hover_shows_declaration_type() -> Result<(), Box<dyn std::error::Error>> {
1220 let code = r#"my $count = 42;
1221my @items = (1, 2, 3);
1222my %config = (key => "value");
1223"#;
1224 let mut parser = Parser::new(code);
1225 let ast = parser.parse()?;
1226 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1227
1228 let scalar_pos = code.find("$count").ok_or("$count not found")?;
1230 let scalar_hover = analyzer
1231 .hover_info
1232 .iter()
1233 .find(|(loc, _)| loc.start <= scalar_pos && loc.end > scalar_pos);
1234 assert!(scalar_hover.is_some(), "Should have hover for $count");
1235 let (_, hover) = scalar_hover.unwrap();
1236 assert!(
1237 hover.signature.contains("$count"),
1238 "Scalar hover should show variable name, got: {}",
1239 hover.signature
1240 );
1241
1242 let array_pos = code.find("@items").ok_or("@items not found")?;
1244 let array_hover = analyzer
1245 .hover_info
1246 .iter()
1247 .find(|(loc, _)| loc.start <= array_pos && loc.end > array_pos);
1248 assert!(array_hover.is_some(), "Should have hover for @items");
1249 let (_, hover) = array_hover.unwrap();
1250 assert!(
1251 hover.signature.contains("@items"),
1252 "Array hover should show variable name, got: {}",
1253 hover.signature
1254 );
1255
1256 let hash_pos = code.find("%config").ok_or("%config not found")?;
1258 let hash_hover =
1259 analyzer.hover_info.iter().find(|(loc, _)| loc.start <= hash_pos && loc.end > hash_pos);
1260 assert!(hash_hover.is_some(), "Should have hover for %config");
1261 let (_, hover) = hash_hover.unwrap();
1262 assert!(
1263 hover.signature.contains("%config"),
1264 "Hash hover should show variable name, got: {}",
1265 hover.signature
1266 );
1267 Ok(())
1268 }
1269
1270 #[test]
1275 fn test_signature_hover_shows_param_names() -> Result<(), Box<dyn std::error::Error>> {
1276 let code = "sub add($x, $y) { $x + $y }";
1278 let mut parser = Parser::new(code);
1279 let ast = parser.parse()?;
1280 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1281
1282 let sub_symbols =
1283 analyzer.symbol_table().find_symbol("add", 0, crate::symbol::SymbolKind::Subroutine);
1284 assert!(!sub_symbols.is_empty(), "symbol 'add' not found");
1285
1286 let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1287 assert!(
1288 hover.signature.contains("$x"),
1289 "hover signature should contain '$x', got: {}",
1290 hover.signature
1291 );
1292 assert!(
1293 hover.signature.contains("$y"),
1294 "hover signature should contain '$y', got: {}",
1295 hover.signature
1296 );
1297 assert!(
1298 !hover.signature.contains("(...)"),
1299 "hover signature must not fall back to '(...)', got: {}",
1300 hover.signature
1301 );
1302 Ok(())
1303 }
1304
1305 #[test]
1306 fn test_signature_hover_with_optional_param() -> Result<(), Box<dyn std::error::Error>> {
1307 let code = "sub greet($name, $greeting = 'Hello') { \"$greeting, $name\" }";
1309 let mut parser = Parser::new(code);
1310 let ast = parser.parse()?;
1311 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1312
1313 let sub_symbols =
1314 analyzer.symbol_table().find_symbol("greet", 0, crate::symbol::SymbolKind::Subroutine);
1315 assert!(!sub_symbols.is_empty(), "symbol 'greet' not found");
1316
1317 let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1318 assert!(
1319 hover.signature.contains("$name"),
1320 "hover signature should contain '$name', got: {}",
1321 hover.signature
1322 );
1323 assert!(
1324 hover.signature.contains("$greeting"),
1325 "hover signature should contain '$greeting', got: {}",
1326 hover.signature
1327 );
1328 Ok(())
1329 }
1330
1331 #[test]
1332 fn test_signature_hover_with_slurpy_param() -> Result<(), Box<dyn std::error::Error>> {
1333 let code = "sub log_all($level, @messages) { print \"$level: @messages\" }";
1335 let mut parser = Parser::new(code);
1336 let ast = parser.parse()?;
1337 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1338
1339 let sub_symbols = analyzer.symbol_table().find_symbol(
1340 "log_all",
1341 0,
1342 crate::symbol::SymbolKind::Subroutine,
1343 );
1344 assert!(!sub_symbols.is_empty(), "symbol 'log_all' not found");
1345
1346 let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1347 assert!(
1348 hover.signature.contains("@messages"),
1349 "hover signature should contain '@messages', got: {}",
1350 hover.signature
1351 );
1352 Ok(())
1353 }
1354
1355 #[test]
1356 fn test_find_definition_returns_method_kind_for_native_method()
1357 -> Result<(), Box<dyn std::error::Error>> {
1358 let code = "class Foo {\n method bar { return 1; }\n}\n";
1359 let mut parser = Parser::new(code);
1360 let ast = parser.parse()?;
1361
1362 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1363
1364 let line1 = code.lines().nth(1).ok_or("no line 1")?;
1366 let line0_len = code.lines().next().ok_or("no line 0")?.len() + 1;
1367 let col = line1.find("bar").ok_or("bar not found on line 1")?;
1368 let offset = line0_len + col;
1369
1370 let sym = analyzer.find_definition(offset).ok_or("no symbol found at 'bar'")?;
1371 assert_eq!(sym.name, "bar", "symbol name should be 'bar'");
1372 assert_eq!(
1373 sym.kind,
1374 SymbolKind::Method,
1375 "native method should have SymbolKind::Method, got {:?}",
1376 sym.kind
1377 );
1378 Ok(())
1379 }
1380}