1mod builtins;
19mod exporter_metadata;
20mod hover;
21mod model;
22mod node_analysis;
23mod query_facade;
24mod references;
25mod tokens;
26
27pub use builtins::{
29 BuiltinDoc, ExceptionContext, PragmaDoc, get_attribute_documentation,
30 get_builtin_documentation, get_exception_context, get_moose_type_documentation,
31 get_operator_documentation, get_pragma_documentation, is_exception_function,
32};
33pub use exporter_metadata::{ExportedSubroutine, FileExportMetadata, PackageExportMetadata};
34pub use hover::HoverInfo;
35pub use model::SemanticModel;
36pub use query_facade::{
37 DefinitionLocation, EffectivePragmaState, ParentChain, ResolvedSymbol, SemanticQueryFacade,
38 VisibleImport,
39};
40pub use tokens::{SemanticToken, SemanticTokenModifier, SemanticTokenType};
41
42use crate::SourceLocation;
43use crate::analysis::class_model::{ClassModel, ClassModelBuilder, MethodResolutionOrder};
44use crate::ast::Node;
45use crate::symbol::{Symbol, SymbolExtractor, SymbolTable, is_universal_method};
46use std::collections::{HashMap, HashSet};
47
48const MAX_MRO_TRAVERSAL_DEPTH: usize = 1024;
49
50#[derive(Debug)]
51pub struct SemanticAnalyzer {
92 pub(super) symbol_table: SymbolTable,
94 pub(super) semantic_tokens: Vec<SemanticToken>,
96 pub(super) hover_info: HashMap<SourceLocation, HoverInfo>,
98 pub(super) source: String,
100 pub class_models: Vec<ClassModel>,
102 pub export_metadata: FileExportMetadata,
104}
105
106impl SemanticAnalyzer {
107 pub fn analyze(ast: &Node) -> Self {
124 Self::analyze_with_source(ast, "")
125 }
126
127 pub fn analyze_with_source(ast: &Node, source: &str) -> Self {
146 let symbol_table = SymbolExtractor::new_with_source(source).extract(ast);
147 let class_models = ClassModelBuilder::new().build(ast);
148 let export_metadata = exporter_metadata::ExportMetadataBuilder::new().build(ast);
149
150 let mut analyzer = SemanticAnalyzer {
151 symbol_table,
152 semantic_tokens: Vec::new(),
153 hover_info: HashMap::new(),
154 source: source.to_string(),
155 class_models,
156 export_metadata,
157 };
158
159 analyzer.analyze_node(ast, 0);
160 analyzer
161 }
162
163 pub fn symbol_table(&self) -> &SymbolTable {
165 &self.symbol_table
166 }
167
168 pub fn semantic_tokens(&self) -> &[SemanticToken] {
170 &self.semantic_tokens
171 }
172
173 pub fn export_metadata(&self) -> &FileExportMetadata {
175 &self.export_metadata
176 }
177
178 pub fn hover_at(&self, location: SourceLocation) -> Option<&HoverInfo> {
180 self.hover_info.get(&location)
181 }
182
183 pub fn all_hover_entries(&self) -> impl Iterator<Item = &HoverInfo> {
185 self.hover_info.values()
186 }
187
188 pub fn symbol_at(&self, location: SourceLocation) -> Option<&Symbol> {
194 let mut best: Option<&Symbol> = None;
195 let mut best_span = usize::MAX;
196
197 for symbols in self.symbol_table.symbols.values() {
199 for symbol in symbols {
200 if symbol.location.start <= location.start && symbol.location.end >= location.end {
201 let span = symbol.location.end - symbol.location.start;
202 if span < best_span {
203 best = Some(symbol);
204 best_span = span;
205 }
206 }
207 }
208 }
209 best
210 }
211
212 pub fn find_definition(&self, position: usize) -> Option<&Symbol> {
214 for refs in self.symbol_table.references.values() {
216 for reference in refs {
217 if reference.location.start <= position && reference.location.end >= position {
218 let symbols = self.resolve_reference_to_symbols(reference);
219 if let Some(first_symbol) = symbols.first() {
220 return Some(self.resolve_definition_target(first_symbol));
221 }
222 }
223 }
224 }
225
226 self.symbol_at(SourceLocation { start: position, end: position })
228 .map(|symbol| self.resolve_definition_target(symbol))
229 }
230
231 fn resolve_definition_target<'a>(&'a self, symbol: &'a Symbol) -> &'a Symbol {
237 if let Some(target) = self.resolve_method_modifier_target(symbol) { target } else { symbol }
238 }
239
240 fn resolve_method_modifier_target<'a>(&'a self, symbol: &'a Symbol) -> Option<&'a Symbol> {
242 if !matches!(
243 symbol.declaration.as_deref(),
244 Some("before" | "after" | "around" | "override" | "augment")
245 ) {
246 return None;
247 }
248
249 self.symbol_table
250 .find_symbol(&symbol.name, symbol.scope_id, crate::symbol::SymbolKind::Subroutine)
251 .into_iter()
252 .find(|candidate| {
253 candidate.location != symbol.location
254 && !matches!(
255 candidate.declaration.as_deref(),
256 Some("before" | "after" | "around" | "override" | "augment")
257 )
258 })
259 }
260
261 pub fn is_file_test_operator(op: &str) -> bool {
268 builtins::is_file_test_operator(op)
269 }
270
271 pub fn resolve_inherited_method_hover(
286 &self,
287 receiver_class: &str,
288 method_name: &str,
289 ) -> Option<HoverInfo> {
290 self.resolve_inherited_method_hover_ordered(receiver_class, method_name)
291 }
292
293 pub fn resolve_inherited_method_location(
298 &self,
299 receiver_class: &str,
300 method_name: &str,
301 ) -> Option<SourceLocation> {
302 let models_by_name: HashMap<&str, &ClassModel> =
303 self.class_models.iter().map(|model| (model.name.as_str(), model)).collect();
304
305 let receiver_model = models_by_name.get(receiver_class).copied()?;
306 let ancestor_order = match receiver_model.mro {
307 MethodResolutionOrder::Dfs => self.dfs_ancestor_order(receiver_class, &models_by_name),
308 MethodResolutionOrder::C3 => self.c3_ancestor_order(receiver_class, &models_by_name),
309 };
310
311 for ancestor in ancestor_order {
312 if let Some(model) = models_by_name.get(ancestor.as_str()).copied()
313 && let Some(location) = self.method_location_in_model(model, method_name)
314 {
315 return Some(location);
316 }
317 }
318
319 if is_universal_method(method_name) {
320 return self
321 .symbol_table
322 .symbols
323 .get(method_name)
324 .and_then(|symbols| {
325 symbols.iter().find(|symbol| {
326 symbol.kind == crate::symbol::SymbolKind::Subroutine
327 && symbol.qualified_name == format!("UNIVERSAL::{method_name}")
328 })
329 })
330 .map(|symbol| symbol.location);
331 }
332
333 None
334 }
335
336 pub fn resolve_parent_chain(&self, receiver_class: &str) -> Option<Vec<String>> {
340 let models_by_name: HashMap<&str, &ClassModel> =
341 self.class_models.iter().map(|model| (model.name.as_str(), model)).collect();
342 let receiver_model = models_by_name.get(receiver_class).copied()?;
343
344 let chain = match receiver_model.mro {
345 MethodResolutionOrder::Dfs => self.dfs_ancestor_order(receiver_class, &models_by_name),
346 MethodResolutionOrder::C3 => self.c3_ancestor_order(receiver_class, &models_by_name),
347 };
348 Some(chain)
349 }
350
351 fn resolve_inherited_method_hover_ordered(
352 &self,
353 receiver_class: &str,
354 method_name: &str,
355 ) -> Option<HoverInfo> {
356 let models_by_name: HashMap<&str, &ClassModel> =
357 self.class_models.iter().map(|model| (model.name.as_str(), model)).collect();
358
359 let Some(receiver_model) = models_by_name.get(receiver_class).copied() else {
360 return self.resolve_plain_package_method_hover(receiver_class, method_name);
361 };
362
363 if let Some(hover) =
364 self.hover_for_model_method(receiver_model, receiver_class, method_name)
365 {
366 return Some(hover);
367 }
368
369 let ancestor_order = match receiver_model.mro {
370 MethodResolutionOrder::Dfs => self.dfs_ancestor_order(receiver_class, &models_by_name),
371 MethodResolutionOrder::C3 => self.c3_ancestor_order(receiver_class, &models_by_name),
372 };
373
374 for ancestor in ancestor_order {
375 if let Some(model) = models_by_name.get(ancestor.as_str()).copied() {
376 if let Some(hover) = self.hover_for_model_method(model, receiver_class, method_name)
377 {
378 return Some(hover);
379 }
380 } else if let Some(hover) =
381 self.resolve_plain_package_method_hover(&ancestor, method_name)
382 {
383 return Some(hover);
384 }
385 }
386
387 if is_universal_method(method_name) {
388 return Some(HoverInfo {
389 signature: format!("sub UNIVERSAL::{method_name}"),
390 documentation: None,
391 details: vec!["Defined in UNIVERSAL".to_string()],
392 });
393 }
394
395 None
396 }
397
398 fn hover_for_model_method(
399 &self,
400 model: &ClassModel,
401 receiver_class: &str,
402 method_name: &str,
403 ) -> Option<HoverInfo> {
404 if model.methods.iter().any(|m| m.name == method_name) {
405 let is_direct = model.name == receiver_class;
406 let details = if is_direct {
407 vec![format!("Defined in {}", model.name)]
408 } else {
409 vec![format!("Inherited from {}", model.name)]
410 };
411 return Some(HoverInfo {
412 signature: format!("sub {}::{}", model.name, method_name),
413 documentation: None,
414 details,
415 });
416 }
417 if model.methods.iter().any(|m| m.name == "AUTOLOAD") {
418 let is_direct = model.name == receiver_class;
419 let details = if is_direct {
420 vec![
421 format!("Resolved via AUTOLOAD in {}", model.name),
422 format!("Requested method: {method_name}"),
423 ]
424 } else {
425 vec![
426 format!("Resolved via inherited AUTOLOAD from {}", model.name),
427 format!("Requested method: {method_name}"),
428 ]
429 };
430 return Some(HoverInfo {
431 signature: format!("sub {}::AUTOLOAD", model.name),
432 documentation: None,
433 details,
434 });
435 }
436 None
437 }
438
439 fn method_location_in_model(
440 &self,
441 model: &ClassModel,
442 method_name: &str,
443 ) -> Option<SourceLocation> {
444 model
445 .methods
446 .iter()
447 .find(|method| method.name == method_name)
448 .or_else(|| model.methods.iter().find(|method| method.name == "AUTOLOAD"))
449 .map(|method| method.location)
450 }
451
452 fn resolve_plain_package_method_hover(
453 &self,
454 package_name: &str,
455 method_name: &str,
456 ) -> Option<HoverInfo> {
457 let qualified = format!("{}::{}", package_name, method_name);
458 let found_in_table = self.symbol_table.symbols.get(method_name).is_some_and(|syms| {
459 syms.iter().any(|s| {
460 matches!(s.kind, crate::symbol::SymbolKind::Subroutine)
461 && s.qualified_name == qualified
462 })
463 }) || self.symbol_table.symbols.contains_key(&qualified);
464
465 if found_in_table {
466 return Some(HoverInfo {
467 signature: format!("sub {}::{}", package_name, method_name),
468 documentation: None,
469 details: vec![format!("Inherited from {}", package_name)],
470 });
471 }
472
473 let qualified_autoload = format!("{}::AUTOLOAD", package_name);
474 let autoload_in_table = self.symbol_table.symbols.get("AUTOLOAD").is_some_and(|syms| {
475 syms.iter().any(|s| {
476 matches!(s.kind, crate::symbol::SymbolKind::Subroutine)
477 && s.qualified_name == qualified_autoload
478 })
479 }) || self.symbol_table.symbols.contains_key(&qualified_autoload);
480
481 if autoload_in_table {
482 return Some(HoverInfo {
483 signature: format!("sub {}::AUTOLOAD", package_name),
484 documentation: None,
485 details: vec![
486 format!("Resolved via AUTOLOAD in {}", package_name),
487 format!("Requested method: {}", method_name),
488 ],
489 });
490 }
491
492 if is_universal_method(method_name) {
493 return Some(HoverInfo {
494 signature: format!("sub UNIVERSAL::{method_name}"),
495 documentation: None,
496 details: vec!["Defined in UNIVERSAL".to_string()],
497 });
498 }
499
500 None
501 }
502
503 fn dfs_ancestor_order(
504 &self,
505 package: &str,
506 models_by_name: &HashMap<&str, &ClassModel>,
507 ) -> Vec<String> {
508 fn walk(
509 package: &str,
510 models_by_name: &HashMap<&str, &ClassModel>,
511 seen: &mut HashSet<String>,
512 out: &mut Vec<String>,
513 depth: usize,
514 ) {
515 if depth >= MAX_MRO_TRAVERSAL_DEPTH {
516 return;
517 }
518
519 let Some(model) = models_by_name.get(package).copied() else {
520 return;
521 };
522
523 for parent in &model.parents {
524 if seen.insert(parent.clone()) {
525 out.push(parent.clone());
526 walk(parent, models_by_name, seen, out, depth + 1);
527 }
528 }
529 }
530
531 let mut seen = HashSet::from([package.to_string()]);
532 let mut out = Vec::new();
533 walk(package, models_by_name, &mut seen, &mut out, 0);
534 out
535 }
536
537 fn c3_ancestor_order(
538 &self,
539 package: &str,
540 models_by_name: &HashMap<&str, &ClassModel>,
541 ) -> Vec<String> {
542 fn linearize(
543 package: &str,
544 models_by_name: &HashMap<&str, &ClassModel>,
545 visited: &mut HashSet<String>,
546 depth: usize,
547 ) -> Vec<String> {
548 if depth >= MAX_MRO_TRAVERSAL_DEPTH {
549 return vec![package.to_string()];
550 }
551
552 if !visited.insert(package.to_string()) {
553 return vec![];
554 }
555
556 let Some(model) = models_by_name.get(package).copied() else {
557 return vec![package.to_string()];
558 };
559
560 let parents = model.parents.clone();
561 if parents.is_empty() {
562 return vec![package.to_string()];
563 }
564
565 let mut parent_mros: Vec<Vec<String>> = parents
566 .iter()
567 .map(|parent| linearize(parent, models_by_name, &mut visited.clone(), depth + 1))
568 .collect();
569 parent_mros.push(parents.clone());
570
571 let mut result = vec![package.to_string()];
572 loop {
573 parent_mros.retain(|list| !list.is_empty());
574 if parent_mros.is_empty() {
575 break;
576 }
577
578 let chosen = parent_mros.iter().find_map(|list| {
579 let candidate = list.first()?;
580 let in_tail = parent_mros
581 .iter()
582 .any(|other| other.iter().skip(1).any(|name| name == candidate));
583 if in_tail { None } else { Some(candidate.clone()) }
584 });
585
586 match chosen {
587 Some(name) => {
588 if !result.contains(&name) {
589 result.push(name.clone());
590 }
591 for list in &mut parent_mros {
592 if list.first().is_some_and(|head| head == &name) {
593 list.remove(0);
594 }
595 }
596 }
597 None => {
598 for list in parent_mros {
599 if let Some(head) = list.first()
600 && !result.contains(head)
601 {
602 result.push(head.clone());
603 }
604 }
605 break;
606 }
607 }
608 }
609
610 result
611 }
612
613 linearize(package, models_by_name, &mut HashSet::new(), 0).into_iter().skip(1).collect()
614 }
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620 use crate::ast::{Node, NodeKind};
621 use crate::parser::Parser;
622 use crate::symbol::SymbolKind;
623
624 #[test]
625 fn test_semantic_tokens() -> Result<(), Box<dyn std::error::Error>> {
626 let code = r#"
627my $x = 42;
628print $x;
629"#;
630
631 let mut parser = Parser::new(code);
632 let ast = parser.parse()?;
633
634 let analyzer = SemanticAnalyzer::analyze(&ast);
635 let tokens = analyzer.semantic_tokens();
636
637 let x_tokens: Vec<_> = tokens
643 .iter()
644 .filter(|t| {
645 matches!(
646 t.token_type,
647 SemanticTokenType::Variable | SemanticTokenType::VariableDeclaration
648 )
649 })
650 .collect();
651 assert!(!x_tokens.is_empty());
652 assert!(x_tokens[0].modifiers.contains(&SemanticTokenModifier::Declaration));
653 Ok(())
654 }
655
656 #[test]
657 fn test_hover_info() -> Result<(), Box<dyn std::error::Error>> {
658 let code = r#"
659sub foo {
660 return 42;
661}
662
663my $result = foo();
664"#;
665
666 let mut parser = Parser::new(code);
667 let ast = parser.parse()?;
668
669 let analyzer = SemanticAnalyzer::analyze(&ast);
670
671 assert!(!analyzer.hover_info.is_empty());
674 Ok(())
675 }
676
677 #[test]
678 fn test_hover_doc_from_pod() -> Result<(), Box<dyn std::error::Error>> {
679 let code = r#"
680# This is foo
681# More docs
682sub foo {
683 return 1;
684}
685"#;
686
687 let mut parser = Parser::new(code);
688 let ast = parser.parse()?;
689
690 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
691
692 let sym = analyzer.symbol_table().symbols.get("foo").ok_or("symbol not found")?[0].clone();
694 let hover = analyzer.hover_at(sym.location).ok_or("hover not found")?;
695 assert!(hover.documentation.as_ref().ok_or("doc not found")?.contains("This is foo"));
696 Ok(())
697 }
698
699 #[test]
700 fn test_comment_doc_extraction() -> Result<(), Box<dyn std::error::Error>> {
701 let code = r#"
702# Adds two numbers
703sub add { 1 }
704"#;
705
706 let mut parser = Parser::new(code);
707 let ast = parser.parse()?;
708
709 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
710
711 let sub_symbols =
712 analyzer.symbol_table().find_symbol("add", 0, crate::symbol::SymbolKind::Subroutine);
713 assert!(!sub_symbols.is_empty());
714 let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
715 assert_eq!(hover.documentation.as_deref(), Some("Adds two numbers"));
716 Ok(())
717 }
718
719 #[test]
720 fn test_extract_documentation_with_out_of_bounds_offset()
721 -> Result<(), Box<dyn std::error::Error>> {
722 let code = "sub add { 1 }\n";
723 let mut parser = Parser::new(code);
724 let ast = parser.parse()?;
725 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
726
727 assert_eq!(analyzer.extract_documentation(code.len() + 1), None);
728 Ok(())
729 }
730
731 #[test]
732 fn test_cross_package_navigation() -> Result<(), Box<dyn std::error::Error>> {
733 let code = r#"
734package Foo {
735 # bar sub
736 sub bar { 42 }
737}
738
739package main;
740Foo::bar();
741"#;
742
743 let mut parser = Parser::new(code);
744 let ast = parser.parse()?;
745 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
746 let pos = code.find("Foo::bar").ok_or("Foo::bar not found")? + 5; let def = analyzer.find_definition(pos).ok_or("definition")?;
748 assert_eq!(def.name, "bar");
749
750 let hover = analyzer.hover_at(def.location).ok_or("hover not found")?;
751 assert!(hover.documentation.as_ref().ok_or("doc not found")?.contains("bar sub"));
752 Ok(())
753 }
754
755 #[test]
756 fn test_universal_method_hover_fallback() -> Result<(), Box<dyn std::error::Error>> {
757 let code = r#"
758package UNIVERSAL;
759sub can { 1 }
760sub isa { 1 }
761
762package Foo;
763sub new { bless {}, shift }
764"#;
765
766 let mut parser = Parser::new(code);
767 let ast = parser.parse()?;
768 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
769
770 let hover = analyzer
771 .resolve_inherited_method_hover("Foo", "can")
772 .ok_or("expected UNIVERSAL hover fallback")?;
773
774 assert!(
775 hover.signature.contains("UNIVERSAL::can"),
776 "expected UNIVERSAL hover signature, got: {}",
777 hover.signature
778 );
779 assert!(
780 hover.details.iter().any(|detail| detail.contains("UNIVERSAL")),
781 "expected UNIVERSAL hover details, got: {:?}",
782 hover.details
783 );
784 Ok(())
785 }
786
787 #[test]
788 fn test_autoload_hover_fallback() -> Result<(), Box<dyn std::error::Error>> {
789 let code = r#"
790package Foo;
791sub AUTOLOAD { 1 }
792"#;
793
794 let mut parser = Parser::new(code);
795 let ast = parser.parse()?;
796 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
797
798 let hover = analyzer
799 .resolve_inherited_method_hover("Foo", "dynamic_method")
800 .ok_or("expected AUTOLOAD hover fallback")?;
801
802 assert!(
803 hover.signature.contains("Foo::AUTOLOAD"),
804 "expected AUTOLOAD hover signature, got: {}",
805 hover.signature
806 );
807 assert!(
808 hover.details.iter().any(|detail| detail.contains("AUTOLOAD")),
809 "expected AUTOLOAD hover details, got: {:?}",
810 hover.details
811 );
812 assert!(
813 hover.details.iter().any(|detail| detail.contains("dynamic_method")),
814 "expected requested method detail, got: {:?}",
815 hover.details
816 );
817 Ok(())
818 }
819
820 #[test]
821 fn test_scope_identification() -> Result<(), Box<dyn std::error::Error>> {
822 let code = r#"
823my $x = 0;
824package Foo {
825 my $x = 1;
826 sub bar { return $x; }
827}
828my $y = $x;
829"#;
830
831 let mut parser = Parser::new(code);
832 let ast = parser.parse()?;
833 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
834
835 let inner_ref_pos = code.find("return $x").ok_or("return $x not found")? + "return ".len();
836 let inner_def = analyzer.find_definition(inner_ref_pos).ok_or("inner def not found")?;
837 let expected_inner = code.find("my $x = 1").ok_or("my $x = 1 not found")? + 3;
838 assert_eq!(inner_def.location.start, expected_inner);
839
840 let outer_ref_pos = code.rfind("$x;").ok_or("$x; not found")?;
841 let outer_def = analyzer.find_definition(outer_ref_pos).ok_or("outer def not found")?;
842 let expected_outer = code.find("my $x = 0").ok_or("my $x = 0 not found")? + 3;
843 assert_eq!(outer_def.location.start, expected_outer);
844 Ok(())
845 }
846
847 #[test]
848 fn test_pod_documentation_extraction() -> Result<(), Box<dyn std::error::Error>> {
849 let code = r#"# Simple comment before sub
851sub documented_with_comment {
852 return "test";
853}
854"#;
855
856 let mut parser = Parser::new(code);
857 let ast = parser.parse()?;
858 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
859
860 let sub_symbols = analyzer.symbol_table().find_symbol(
861 "documented_with_comment",
862 0,
863 crate::symbol::SymbolKind::Subroutine,
864 );
865 assert!(!sub_symbols.is_empty());
866 let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
867 let doc = hover.documentation.as_ref().ok_or("doc not found")?;
868 assert!(doc.contains("Simple comment before sub"));
869 Ok(())
870 }
871
872 #[test]
873 fn test_empty_source_handling() -> Result<(), Box<dyn std::error::Error>> {
874 let code = "";
875 let mut parser = Parser::new(code);
876 let ast = parser.parse()?;
877 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
878
879 assert!(analyzer.semantic_tokens().is_empty());
881 assert!(analyzer.hover_info.is_empty());
882 Ok(())
883 }
884
885 #[test]
886 fn test_multiple_comment_lines() -> Result<(), Box<dyn std::error::Error>> {
887 let code = r#"
888# First comment
889# Second comment
890# Third comment
891sub multi_commented {
892 1;
893}
894"#;
895
896 let mut parser = Parser::new(code);
897 let ast = parser.parse()?;
898 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
899
900 let sub_symbols = analyzer.symbol_table().find_symbol(
901 "multi_commented",
902 0,
903 crate::symbol::SymbolKind::Subroutine,
904 );
905 assert!(!sub_symbols.is_empty());
906 let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
907 let doc = hover.documentation.as_ref().ok_or("doc not found")?;
908 assert!(doc.contains("First comment"));
909 assert!(doc.contains("Second comment"));
910 assert!(doc.contains("Third comment"));
911 Ok(())
912 }
913
914 #[test]
916 fn test_semantic_model_build_and_tokens() -> Result<(), Box<dyn std::error::Error>> {
917 let code = r#"
918my $x = 42;
919my $y = 10;
920$x + $y;
921"#;
922 let mut parser = Parser::new(code);
923 let ast = parser.parse()?;
924
925 let model = SemanticModel::build(&ast, code);
926
927 let tokens = model.tokens();
929 assert!(!tokens.is_empty(), "SemanticModel should provide tokens");
930
931 let var_tokens: Vec<_> = tokens
933 .iter()
934 .filter(|t| {
935 matches!(
936 t.token_type,
937 SemanticTokenType::Variable | SemanticTokenType::VariableDeclaration
938 )
939 })
940 .collect();
941 assert!(var_tokens.len() >= 2, "Should have at least 2 variable tokens");
942 Ok(())
943 }
944
945 #[test]
946 fn test_semantic_model_symbol_table_access() -> Result<(), Box<dyn std::error::Error>> {
947 let code = r#"
948my $x = 42;
949sub foo {
950 my $y = $x;
951}
952"#;
953 let mut parser = Parser::new(code);
954 let ast = parser.parse()?;
955
956 let model = SemanticModel::build(&ast, code);
957
958 let symbol_table = model.symbol_table();
960 let x_symbols = symbol_table.find_symbol("x", 0, SymbolKind::scalar());
961 assert!(!x_symbols.is_empty(), "Should find $x in symbol table");
962
963 let foo_symbols = symbol_table.find_symbol("foo", 0, SymbolKind::Subroutine);
964 assert!(!foo_symbols.is_empty(), "Should find sub foo in symbol table");
965 Ok(())
966 }
967
968 #[test]
969 fn test_semantic_model_hover_info() -> Result<(), Box<dyn std::error::Error>> {
970 let code = r#"
971# This is a documented variable
972my $documented = 42;
973"#;
974 let mut parser = Parser::new(code);
975 let ast = parser.parse()?;
976
977 let model = SemanticModel::build(&ast, code);
978
979 let symbol_table = model.symbol_table();
981 let symbols = symbol_table.find_symbol("documented", 0, SymbolKind::scalar());
982 assert!(!symbols.is_empty(), "Should find $documented");
983
984 if let Some(hover) = model.hover_info_at(symbols[0].location) {
986 assert!(hover.signature.contains("documented"), "Hover should contain variable name");
987 }
988 Ok(())
991 }
992
993 #[test]
994 fn test_analyzer_find_definition_scalar() -> Result<(), Box<dyn std::error::Error>> {
995 let code = "my $x = 1;\n$x + 2;\n";
996 let mut parser = Parser::new(code);
997 let ast = parser.parse()?;
998
999 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1001
1002 let ref_line = code.lines().nth(1).ok_or("line 2 not found")?;
1004 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")?;
1006 let ref_pos = line_offset + col_in_line;
1007
1008 let symbol =
1009 analyzer.find_definition(ref_pos).ok_or("definition not found for $x reference")?;
1010
1011 assert_eq!(symbol.name, "x");
1013 assert_eq!(symbol.kind, SymbolKind::scalar());
1014
1015 assert!(
1017 symbol.location.start < ref_pos,
1018 "Declaration {:?} should precede reference at byte {}",
1019 symbol.location.start,
1020 ref_pos
1021 );
1022 Ok(())
1023 }
1024
1025 #[test]
1026 fn test_semantic_model_definition_at() -> Result<(), Box<dyn std::error::Error>> {
1027 let code = "my $x = 1;\n$x + 2;\n";
1028 let mut parser = Parser::new(code);
1029 let ast = parser.parse()?;
1030
1031 let model = SemanticModel::build(&ast, code);
1032
1033 let ref_line_index = 1;
1035 let ref_line = code.lines().nth(ref_line_index).ok_or("line not found")?;
1036 let col_in_line = ref_line.find("$x").ok_or("could not find $x")?;
1037 let byte_offset = code
1038 .lines()
1039 .take(ref_line_index)
1040 .map(|l| l.len() + 1) .sum::<usize>()
1042 + col_in_line;
1043
1044 let definition = model.definition_at(byte_offset);
1045 assert!(
1046 definition.is_some(),
1047 "definition_at returned None for $x reference at {}",
1048 byte_offset
1049 );
1050 if let Some(symbol) = definition {
1051 assert_eq!(symbol.name, "x");
1052 assert_eq!(symbol.kind, SymbolKind::scalar());
1053 assert!(
1054 symbol.location.start < byte_offset,
1055 "Declaration {:?} should precede reference at byte {}",
1056 symbol.location.start,
1057 byte_offset
1058 );
1059 }
1060 Ok(())
1061 }
1062
1063 #[test]
1064 fn test_analyzer_find_definition_goto_label() -> Result<(), Box<dyn std::error::Error>> {
1065 let code = "START: while (1) {\n goto START;\n}\n";
1066 let mut parser = Parser::new(code);
1067 let ast = parser.parse()?;
1068
1069 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1070 let ref_pos = code.find("START;\n").ok_or("could not find goto label")?;
1071
1072 let symbol = analyzer
1073 .find_definition(ref_pos)
1074 .ok_or("definition not found for goto label reference")?;
1075
1076 assert_eq!(symbol.name, "START");
1077 assert_eq!(symbol.kind, SymbolKind::Label);
1078 assert!(
1079 symbol.location.start < ref_pos,
1080 "Label definition {:?} should precede goto reference at byte {}",
1081 symbol.location.start,
1082 ref_pos
1083 );
1084 Ok(())
1085 }
1086
1087 #[test]
1088 fn test_anonymous_subroutine_semantic_tokens() -> Result<(), Box<dyn std::error::Error>> {
1089 let code = r#"
1090my $closure = sub {
1091 my $x = 42;
1092 return $x + 1;
1093};
1094"#;
1095
1096 let mut parser = Parser::new(code);
1097 let ast = parser.parse()?;
1098 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1099
1100 let tokens = analyzer.semantic_tokens();
1102
1103 let sub_keywords: Vec<_> =
1105 tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Keyword)).collect();
1106
1107 assert!(!sub_keywords.is_empty(), "Should have keyword token for 'sub'");
1108
1109 let sub_position = code.find("sub {").ok_or("sub { not found")?;
1111 let hover_exists = analyzer
1112 .hover_info
1113 .iter()
1114 .any(|(loc, _)| loc.start <= sub_position && loc.end >= sub_position);
1115
1116 assert!(hover_exists, "Should have hover info for anonymous subroutine");
1117 Ok(())
1118 }
1119
1120 #[test]
1121 fn test_infer_type_for_literals() -> Result<(), Box<dyn std::error::Error>> {
1122 let code = r#"
1123my $num = 42;
1124my $str = "hello";
1125my @arr = (1, 2, 3);
1126my %hash = (a => 1);
1127"#;
1128
1129 let mut parser = Parser::new(code);
1130 let ast = parser.parse()?;
1131 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1132
1133 fn find_number_node(node: &Node) -> Option<&Node> {
1136 match &node.kind {
1137 NodeKind::Number { .. } => Some(node),
1138 NodeKind::Program { statements } | NodeKind::Block { statements } => {
1139 for stmt in statements {
1140 if let Some(found) = find_number_node(stmt) {
1141 return Some(found);
1142 }
1143 }
1144 None
1145 }
1146 NodeKind::VariableDeclaration { initializer, .. } => {
1147 initializer.as_ref().and_then(|init| find_number_node(init))
1148 }
1149 _ => None,
1150 }
1151 }
1152
1153 if let Some(num_node) = find_number_node(&ast) {
1154 let inferred = analyzer.infer_type(num_node);
1155 assert_eq!(inferred, Some("number".to_string()), "Should infer number type");
1156 }
1157
1158 Ok(())
1159 }
1160
1161 #[test]
1162 fn test_infer_type_for_binary_operations() -> Result<(), Box<dyn std::error::Error>> {
1163 let code = r#"my $sum = 10 + 20;
1164my $concat = "a" . "b";
1165"#;
1166
1167 let mut parser = Parser::new(code);
1168 let ast = parser.parse()?;
1169 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1170
1171 fn find_binary_node<'a>(node: &'a Node, op: &str) -> Option<&'a Node> {
1173 match &node.kind {
1174 NodeKind::Binary { op: node_op, .. } if node_op == op => Some(node),
1175 NodeKind::Program { statements } | NodeKind::Block { statements } => {
1176 for stmt in statements {
1177 if let Some(found) = find_binary_node(stmt, op) {
1178 return Some(found);
1179 }
1180 }
1181 None
1182 }
1183 NodeKind::VariableDeclaration { initializer, .. } => {
1184 initializer.as_ref().and_then(|init| find_binary_node(init, op))
1185 }
1186 _ => None,
1187 }
1188 }
1189
1190 if let Some(add_node) = find_binary_node(&ast, "+") {
1192 let inferred = analyzer.infer_type(add_node);
1193 assert_eq!(inferred, Some("number".to_string()), "Arithmetic should infer to number");
1194 }
1195
1196 if let Some(concat_node) = find_binary_node(&ast, ".") {
1198 let inferred = analyzer.infer_type(concat_node);
1199 assert_eq!(
1200 inferred,
1201 Some("string".to_string()),
1202 "Concatenation should infer to string"
1203 );
1204 }
1205
1206 Ok(())
1207 }
1208
1209 #[test]
1210 fn test_anonymous_subroutine_hover_info() -> Result<(), Box<dyn std::error::Error>> {
1211 let code = r#"
1212# This is a closure
1213my $adder = sub {
1214 my ($x, $y) = @_;
1215 return $x + $y;
1216};
1217"#;
1218
1219 let mut parser = Parser::new(code);
1220 let ast = parser.parse()?;
1221 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1222
1223 let sub_position = code.find("sub {").ok_or("sub { not found")?;
1225 let hover = analyzer
1226 .hover_info
1227 .iter()
1228 .find(|(loc, _)| loc.start <= sub_position && loc.end >= sub_position)
1229 .map(|(_, h)| h);
1230
1231 assert!(hover.is_some(), "Should have hover info");
1232
1233 if let Some(h) = hover {
1234 assert!(h.signature.contains("sub"), "Hover signature should contain 'sub'");
1235 assert!(
1236 h.details.iter().any(|d| d.contains("Anonymous")),
1237 "Hover details should mention anonymous subroutine"
1238 );
1239 if let Some(doc) = &h.documentation {
1243 assert!(
1244 doc.contains("closure"),
1245 "If documentation found, it should mention closure"
1246 );
1247 }
1248 }
1249 Ok(())
1250 }
1251
1252 #[test]
1254 fn test_substitution_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
1255 let code = r#"
1256my $str = "hello world";
1257$str =~ s/world/Perl/;
1258"#;
1259 let mut parser = Parser::new(code);
1260 let ast = parser.parse()?;
1261 let analyzer = SemanticAnalyzer::analyze(&ast);
1262
1263 let tokens = analyzer.semantic_tokens();
1264 let operator_tokens: Vec<_> =
1265 tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1266
1267 assert!(!operator_tokens.is_empty(), "Should have operator tokens for substitution");
1268 Ok(())
1269 }
1270
1271 #[test]
1272 fn test_transliteration_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
1273 let code = r#"
1274my $str = "hello";
1275$str =~ tr/el/ol/;
1276"#;
1277 let mut parser = Parser::new(code);
1278 let ast = parser.parse()?;
1279 let analyzer = SemanticAnalyzer::analyze(&ast);
1280
1281 let tokens = analyzer.semantic_tokens();
1282 let operator_tokens: Vec<_> =
1283 tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1284
1285 assert!(!operator_tokens.is_empty(), "Should have operator tokens for transliteration");
1286 Ok(())
1287 }
1288
1289 #[test]
1290 fn test_reference_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
1291 let code = r#"
1292my $x = 42;
1293my $ref = \$x;
1294"#;
1295 let mut parser = Parser::new(code);
1296 let ast = parser.parse()?;
1297 let analyzer = SemanticAnalyzer::analyze(&ast);
1298
1299 let tokens = analyzer.semantic_tokens();
1300 let operator_tokens: Vec<_> =
1301 tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1302
1303 assert!(!operator_tokens.is_empty(), "Should have operator tokens for reference operator");
1304 Ok(())
1305 }
1306
1307 #[test]
1308 fn test_postfix_loop_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
1309 let code = r#"
1310my @list = (1, 2, 3);
1311print $_ for @list;
1312my $x = 0;
1313$x++ while $x < 10;
1314"#;
1315 let mut parser = Parser::new(code);
1316 let ast = parser.parse()?;
1317 let analyzer = SemanticAnalyzer::analyze(&ast);
1318
1319 let tokens = analyzer.semantic_tokens();
1320 let control_tokens: Vec<_> = tokens
1321 .iter()
1322 .filter(|t| matches!(t.token_type, SemanticTokenType::KeywordControl))
1323 .collect();
1324
1325 assert!(!control_tokens.is_empty(), "Should have control keyword tokens for postfix loops");
1326 Ok(())
1327 }
1328
1329 #[test]
1330 fn test_file_test_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
1331 let code = r#"
1332my $file = "test.txt";
1333if (-e $file) {
1334 print "exists";
1335}
1336if (-d $file) {
1337 print "directory";
1338}
1339if (-f $file) {
1340 print "file";
1341}
1342"#;
1343 let mut parser = Parser::new(code);
1344 let ast = parser.parse()?;
1345 let analyzer = SemanticAnalyzer::analyze(&ast);
1346
1347 let tokens = analyzer.semantic_tokens();
1348 let operator_tokens: Vec<_> =
1349 tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1350
1351 assert!(!operator_tokens.is_empty(), "Should have operator tokens for file test operators");
1352 Ok(())
1353 }
1354
1355 #[test]
1356 fn test_all_file_test_operators_recognized() -> Result<(), Box<dyn std::error::Error>> {
1357 let file_test_ops = vec![
1359 "-e", "-d", "-f", "-r", "-w", "-x", "-s", "-z", "-T", "-B", "-M", "-A", "-C", "-l",
1360 "-p", "-S", "-u", "-g", "-k", "-t", "-O", "-G", "-R", "-b", "-c",
1361 ];
1362
1363 for op in file_test_ops {
1364 assert!(
1365 SemanticAnalyzer::is_file_test_operator(op),
1366 "Operator {} should be recognized as file test operator",
1367 op
1368 );
1369 }
1370
1371 assert!(
1373 !SemanticAnalyzer::is_file_test_operator("+"),
1374 "Operator '+' should not be recognized as file test operator"
1375 );
1376 assert!(
1377 !SemanticAnalyzer::is_file_test_operator("-"),
1378 "Operator '-' should not be recognized as file test operator"
1379 );
1380 assert!(
1381 !SemanticAnalyzer::is_file_test_operator("++"),
1382 "Operator '++' should not be recognized as file test operator"
1383 );
1384
1385 Ok(())
1386 }
1387
1388 #[test]
1389 fn test_postfix_loop_modifiers() -> Result<(), Box<dyn std::error::Error>> {
1390 let code = r#"
1391my @items = (1, 2, 3);
1392print $_ for @items;
1393print $_ foreach @items;
1394my $x = 0;
1395$x++ while $x < 10;
1396$x-- until $x < 0;
1397"#;
1398 let mut parser = Parser::new(code);
1399 let ast = parser.parse()?;
1400 let analyzer = SemanticAnalyzer::analyze(&ast);
1401
1402 let tokens = analyzer.semantic_tokens();
1403 let control_tokens: Vec<_> = tokens
1404 .iter()
1405 .filter(|t| matches!(t.token_type, SemanticTokenType::KeywordControl))
1406 .collect();
1407
1408 assert!(
1410 control_tokens.len() >= 4,
1411 "Should have at least 4 control keyword tokens for postfix loop modifiers"
1412 );
1413 Ok(())
1414 }
1415
1416 #[test]
1417 fn test_substitution_with_modifiers() -> Result<(), Box<dyn std::error::Error>> {
1418 let code = r#"
1419my $str = "hello world";
1420$str =~ s/world/Perl/gi;
1421"#;
1422 let mut parser = Parser::new(code);
1423 let ast = parser.parse()?;
1424 let analyzer = SemanticAnalyzer::analyze(&ast);
1425
1426 let tokens = analyzer.semantic_tokens();
1427 let operator_tokens: Vec<_> =
1428 tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1429
1430 assert!(
1431 !operator_tokens.is_empty(),
1432 "Should have operator tokens for substitution with modifiers"
1433 );
1434 Ok(())
1435 }
1436
1437 #[test]
1438 fn test_transliteration_y_operator() -> Result<(), Box<dyn std::error::Error>> {
1439 let code = r#"
1440my $str = "hello";
1441$str =~ y/hello/world/;
1442"#;
1443 let mut parser = Parser::new(code);
1444 let ast = parser.parse()?;
1445 let analyzer = SemanticAnalyzer::analyze(&ast);
1446
1447 let tokens = analyzer.semantic_tokens();
1448 let operator_tokens: Vec<_> =
1449 tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
1450
1451 assert!(
1452 !operator_tokens.is_empty(),
1453 "Should have operator tokens for y/// transliteration"
1454 );
1455 Ok(())
1456 }
1457
1458 #[test]
1459 fn test_builtin_documentation_coverage() -> Result<(), Box<dyn std::error::Error>> {
1460 let builtins = [
1462 "print", "say", "push", "pop", "shift", "unshift", "map", "grep", "sort", "reverse",
1463 "split", "join", "chomp", "chop", "length", "substr", "index", "rindex", "lc", "uc",
1464 "die", "warn", "eval", "open", "close", "read", "keys", "values", "exists", "delete",
1465 "defined", "ref", "bless", "sprintf", "chr", "ord",
1466 ];
1467
1468 for name in &builtins {
1469 let doc = get_builtin_documentation(name);
1470 assert!(doc.is_some(), "Built-in '{}' should have documentation", name);
1471 let doc = doc.unwrap();
1472 assert!(
1473 !doc.signature.is_empty(),
1474 "Built-in '{}' should have a non-empty signature",
1475 name
1476 );
1477 assert!(
1478 !doc.description.is_empty(),
1479 "Built-in '{}' should have a non-empty description",
1480 name
1481 );
1482 }
1483 Ok(())
1484 }
1485
1486 #[test]
1487 fn test_builtin_hover_for_function_call() -> Result<(), Box<dyn std::error::Error>> {
1488 let code = r#"
1489my @items = (3, 1, 4);
1490push @items, 5;
1491"#;
1492 let mut parser = Parser::new(code);
1493 let ast = parser.parse()?;
1494 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1495
1496 let push_pos = code.find("push").ok_or("push not found")?;
1498 let hover_for_push =
1499 analyzer.hover_info.iter().find(|(loc, _)| loc.start <= push_pos && loc.end > push_pos);
1500
1501 assert!(hover_for_push.is_some(), "Should have hover info for 'push' builtin");
1502 let (_, hover) = hover_for_push.unwrap();
1503 assert!(
1504 hover.signature.contains("push"),
1505 "Hover signature should contain 'push', got: {}",
1506 hover.signature
1507 );
1508 assert!(hover.documentation.is_some(), "Hover for 'push' should have documentation");
1509 Ok(())
1510 }
1511
1512 #[test]
1513 fn test_core_prefixed_builtin_hover_for_function_call() -> Result<(), Box<dyn std::error::Error>>
1514 {
1515 let code = r#"
1516my $value = "abc";
1517CORE::length($value);
1518"#;
1519 let mut parser = Parser::new(code);
1520 let ast = parser.parse()?;
1521 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1522
1523 let length_pos = code.find("CORE::length").ok_or("CORE::length not found")?;
1524 let hover = analyzer
1525 .hover_info
1526 .iter()
1527 .find(|(loc, _)| loc.start <= length_pos && loc.end > length_pos);
1528
1529 assert!(hover.is_some(), "Should have hover info for CORE::length builtin");
1530 let (_, hover) = hover.ok_or("missing hover for CORE::length")?;
1531 assert!(
1532 hover.signature.contains("length"),
1533 "Hover signature should contain 'length', got: {}",
1534 hover.signature
1535 );
1536 Ok(())
1537 }
1538
1539 #[test]
1540 fn test_package_hover_with_pod_name_section() -> Result<(), Box<dyn std::error::Error>> {
1541 let code = r#"
1542=head1 NAME
1543
1544My::Module - A great module for testing
1545
1546=head1 DESCRIPTION
1547
1548This module does great things.
1549
1550=cut
1551
1552package My::Module;
1553
1554sub new { bless {}, shift }
1555
15561;
1557"#;
1558 let mut parser = Parser::new(code);
1559 let ast = parser.parse()?;
1560 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1561
1562 let pkg_symbols = analyzer.symbol_table().symbols.get("My::Module");
1564 assert!(pkg_symbols.is_some(), "Should find My::Module in symbol table");
1565
1566 let pkg = &pkg_symbols.unwrap()[0];
1567 let hover = analyzer.hover_at(pkg.location);
1568 assert!(hover.is_some(), "Should have hover info for package");
1569
1570 let hover = hover.unwrap();
1571 assert!(
1572 hover.signature.contains("package My::Module"),
1573 "Package hover signature should contain 'package My::Module', got: {}",
1574 hover.signature
1575 );
1576 if let Some(doc) = &hover.documentation {
1578 assert!(
1579 doc.contains("A great module for testing"),
1580 "Package hover should contain POD NAME content, got: {}",
1581 doc
1582 );
1583 }
1584 Ok(())
1585 }
1586
1587 #[test]
1588 fn test_package_documentation_via_symbol() -> Result<(), Box<dyn std::error::Error>> {
1589 let code = r#"
1590=head1 NAME
1591
1592Utils - Utility functions
1593
1594=cut
1595
1596package Utils;
1597
1598sub helper { 1 }
1599
16001;
1601"#;
1602 let mut parser = Parser::new(code);
1603 let ast = parser.parse()?;
1604 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1605
1606 let pkg_symbols = analyzer.symbol_table().symbols.get("Utils");
1607 assert!(pkg_symbols.is_some(), "Should find Utils package");
1608
1609 let pkg = &pkg_symbols.unwrap()[0];
1610 assert!(
1612 pkg.documentation.is_some(),
1613 "Package symbol should have documentation from POD NAME section"
1614 );
1615 let doc = pkg.documentation.as_ref().unwrap();
1616 assert!(
1617 doc.contains("Utility functions"),
1618 "Package doc should contain 'Utility functions', got: {}",
1619 doc
1620 );
1621 Ok(())
1622 }
1623
1624 #[test]
1625 fn test_subroutine_with_pod_docs_hover() -> Result<(), Box<dyn std::error::Error>> {
1626 let code = r#"
1627=head2 process
1628
1629Processes input data and returns the result.
1630
1631=cut
1632
1633sub process {
1634 my ($input) = @_;
1635 return $input * 2;
1636}
1637"#;
1638 let mut parser = Parser::new(code);
1639 let ast = parser.parse()?;
1640 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1641
1642 let sub_symbols = analyzer.symbol_table().find_symbol("process", 0, SymbolKind::Subroutine);
1643 assert!(!sub_symbols.is_empty(), "Should find sub process");
1644
1645 let hover = analyzer.hover_at(sub_symbols[0].location);
1646 assert!(hover.is_some(), "Should have hover for sub process");
1647
1648 let hover = hover.unwrap();
1649 assert!(
1650 hover.signature.contains("sub process"),
1651 "Hover should show sub signature, got: {}",
1652 hover.signature
1653 );
1654 if let Some(doc) = &hover.documentation {
1656 assert!(
1657 doc.contains("process") || doc.contains("Processes"),
1658 "Sub hover should contain POD documentation, got: {}",
1659 doc
1660 );
1661 }
1662 Ok(())
1663 }
1664
1665 #[test]
1666 fn test_variable_hover_shows_declaration_type() -> Result<(), Box<dyn std::error::Error>> {
1667 let code = r#"my $count = 42;
1668my @items = (1, 2, 3);
1669my %config = (key => "value");
1670"#;
1671 let mut parser = Parser::new(code);
1672 let ast = parser.parse()?;
1673 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1674
1675 let scalar_pos = code.find("$count").ok_or("$count not found")?;
1677 let scalar_hover = analyzer
1678 .hover_info
1679 .iter()
1680 .find(|(loc, _)| loc.start <= scalar_pos && loc.end > scalar_pos);
1681 assert!(scalar_hover.is_some(), "Should have hover for $count");
1682 let (_, hover) = scalar_hover.unwrap();
1683 assert!(
1684 hover.signature.contains("$count"),
1685 "Scalar hover should show variable name, got: {}",
1686 hover.signature
1687 );
1688
1689 let array_pos = code.find("@items").ok_or("@items not found")?;
1691 let array_hover = analyzer
1692 .hover_info
1693 .iter()
1694 .find(|(loc, _)| loc.start <= array_pos && loc.end > array_pos);
1695 assert!(array_hover.is_some(), "Should have hover for @items");
1696 let (_, hover) = array_hover.unwrap();
1697 assert!(
1698 hover.signature.contains("@items"),
1699 "Array hover should show variable name, got: {}",
1700 hover.signature
1701 );
1702
1703 let hash_pos = code.find("%config").ok_or("%config not found")?;
1705 let hash_hover =
1706 analyzer.hover_info.iter().find(|(loc, _)| loc.start <= hash_pos && loc.end > hash_pos);
1707 assert!(hash_hover.is_some(), "Should have hover for %config");
1708 let (_, hover) = hash_hover.unwrap();
1709 assert!(
1710 hover.signature.contains("%config"),
1711 "Hash hover should show variable name, got: {}",
1712 hover.signature
1713 );
1714 Ok(())
1715 }
1716
1717 #[test]
1722 fn test_signature_hover_shows_param_names() -> Result<(), Box<dyn std::error::Error>> {
1723 let code = "sub add($x, $y) { $x + $y }";
1725 let mut parser = Parser::new(code);
1726 let ast = parser.parse()?;
1727 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1728
1729 let sub_symbols =
1730 analyzer.symbol_table().find_symbol("add", 0, crate::symbol::SymbolKind::Subroutine);
1731 assert!(!sub_symbols.is_empty(), "symbol 'add' not found");
1732
1733 let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1734 assert!(
1735 hover.signature.contains("$x"),
1736 "hover signature should contain '$x', got: {}",
1737 hover.signature
1738 );
1739 assert!(
1740 hover.signature.contains("$y"),
1741 "hover signature should contain '$y', got: {}",
1742 hover.signature
1743 );
1744 assert!(
1745 !hover.signature.contains("(...)"),
1746 "hover signature must not fall back to '(...)', got: {}",
1747 hover.signature
1748 );
1749 Ok(())
1750 }
1751
1752 #[test]
1753 fn test_signature_hover_with_optional_param() -> Result<(), Box<dyn std::error::Error>> {
1754 let code = "sub greet($name, $greeting = 'Hello') { \"$greeting, $name\" }";
1756 let mut parser = Parser::new(code);
1757 let ast = parser.parse()?;
1758 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1759
1760 let sub_symbols =
1761 analyzer.symbol_table().find_symbol("greet", 0, crate::symbol::SymbolKind::Subroutine);
1762 assert!(!sub_symbols.is_empty(), "symbol 'greet' not found");
1763
1764 let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1765 assert!(
1766 hover.signature.contains("$name"),
1767 "hover signature should contain '$name', got: {}",
1768 hover.signature
1769 );
1770 assert!(
1771 hover.signature.contains("$greeting"),
1772 "hover signature should contain '$greeting', got: {}",
1773 hover.signature
1774 );
1775 Ok(())
1776 }
1777
1778 #[test]
1779 fn test_signature_hover_with_slurpy_param() -> Result<(), Box<dyn std::error::Error>> {
1780 let code = "sub log_all($level, @messages) { print \"$level: @messages\" }";
1782 let mut parser = Parser::new(code);
1783 let ast = parser.parse()?;
1784 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1785
1786 let sub_symbols = analyzer.symbol_table().find_symbol(
1787 "log_all",
1788 0,
1789 crate::symbol::SymbolKind::Subroutine,
1790 );
1791 assert!(!sub_symbols.is_empty(), "symbol 'log_all' not found");
1792
1793 let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
1794 assert!(
1795 hover.signature.contains("@messages"),
1796 "hover signature should contain '@messages', got: {}",
1797 hover.signature
1798 );
1799 Ok(())
1800 }
1801
1802 #[test]
1803 fn test_find_definition_returns_method_kind_for_native_method()
1804 -> Result<(), Box<dyn std::error::Error>> {
1805 let code = "class Foo {\n method bar { return 1; }\n}\n";
1806 let mut parser = Parser::new(code);
1807 let ast = parser.parse()?;
1808
1809 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1810
1811 let line1 = code.lines().nth(1).ok_or("no line 1")?;
1813 let line0_len = code.lines().next().ok_or("no line 0")?.len() + 1;
1814 let col = line1.find("bar").ok_or("bar not found on line 1")?;
1815 let offset = line0_len + col;
1816
1817 let sym = analyzer.find_definition(offset).ok_or("no symbol found at 'bar'")?;
1818 assert_eq!(sym.name, "bar", "symbol name should be 'bar'");
1819 assert_eq!(
1820 sym.kind,
1821 SymbolKind::Method,
1822 "native method should have SymbolKind::Method, got {:?}",
1823 sym.kind
1824 );
1825 Ok(())
1826 }
1827
1828 #[test]
1829 fn test_find_definition_redirects_method_modifier_to_target_method()
1830 -> Result<(), Box<dyn std::error::Error>> {
1831 let code = include_str!(
1832 "../../../../perl-lsp-rs/tests/fixtures/frameworks/moo_method_modifiers.pl"
1833 );
1834 let mut parser = Parser::new(code);
1835 let ast = parser.parse()?;
1836
1837 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
1838
1839 for target_line in [8, 13, 18] {
1841 let line = code.lines().nth(target_line).ok_or("missing modifier line")?;
1842 let col = line.find("save").ok_or("modifier target not found")?;
1843 let mut offset = 0;
1844 for line in code.lines().take(target_line) {
1845 offset += line.len() + 1;
1846 }
1847 offset += col;
1848
1849 let sym = analyzer
1850 .find_definition(offset)
1851 .ok_or("no symbol found at method modifier target")?;
1852 assert_eq!(sym.name, "save", "modifier target should resolve to save");
1853 let method_start = code.find("sub save").ok_or("method declaration not found")?;
1854 assert_eq!(
1855 sym.location.start, method_start,
1856 "modifier target should resolve to the underlying method declaration"
1857 );
1858 assert_eq!(
1859 sym.declaration, None,
1860 "definition should land on the real method, not the synthetic modifier"
1861 );
1862 }
1863
1864 Ok(())
1865 }
1866
1867 #[test]
1868 fn test_resolve_inherited_method_location_limits_dfs_depth()
1869 -> Result<(), Box<dyn std::error::Error>> {
1870 let chain_len = MAX_MRO_TRAVERSAL_DEPTH + 10;
1871 let mut code = String::new();
1872 for i in 0..chain_len {
1873 code.push_str(&format!("package P{i}; use parent 'P{}';\n", i + 1));
1874 }
1875 code.push_str(&format!("package P{chain_len}; sub target {{ 1 }}\n"));
1876
1877 let mut parser = Parser::new(&code);
1878 let ast = parser.parse()?;
1879 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, &code);
1880
1881 let location = analyzer.resolve_inherited_method_location("P0", "target");
1882 assert!(location.is_none(), "DFS traversal should stop at depth limit");
1883 Ok(())
1884 }
1885
1886 #[test]
1887 fn test_resolve_inherited_method_location_limits_c3_depth()
1888 -> Result<(), Box<dyn std::error::Error>> {
1889 let chain_len = MAX_MRO_TRAVERSAL_DEPTH + 10;
1890 let mut code = String::new();
1891 code.push_str("package P0; use mro 'c3'; use parent 'P1';\n");
1892 for i in 1..chain_len {
1893 code.push_str(&format!("package P{i}; use parent 'P{}';\n", i + 1));
1894 }
1895 code.push_str(&format!("package P{chain_len}; sub target {{ 1 }}\n"));
1896
1897 let mut parser = Parser::new(&code);
1898 let ast = parser.parse()?;
1899 let analyzer = SemanticAnalyzer::analyze_with_source(&ast, &code);
1900
1901 let location = analyzer.resolve_inherited_method_location("P0", "target");
1902 assert!(location.is_none(), "C3 traversal should stop at depth limit");
1903 Ok(())
1904 }
1905}