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