mod builtins;
mod exporter_metadata;
mod hover;
mod model;
mod node_analysis;
mod query_facade;
mod references;
mod tokens;
pub use builtins::{
BuiltinDoc, ExceptionContext, PragmaDoc, get_attribute_documentation,
get_builtin_documentation, get_exception_context, get_moose_type_documentation,
get_operator_documentation, get_pragma_documentation, is_exception_function,
};
pub use exporter_metadata::{ExportedSubroutine, FileExportMetadata, PackageExportMetadata};
pub use hover::HoverInfo;
pub use model::SemanticModel;
pub use query_facade::{
DefinitionLocation, EffectivePragmaState, ParentChain, ResolvedSymbol, SemanticQueryFacade,
VisibleImport,
};
pub use tokens::{SemanticToken, SemanticTokenModifier, SemanticTokenType};
use crate::SourceLocation;
use crate::analysis::class_model::{ClassModel, ClassModelBuilder, MethodResolutionOrder};
use crate::ast::Node;
use crate::symbol::{Symbol, SymbolExtractor, SymbolTable, is_universal_method};
use std::collections::{HashMap, HashSet};
const MAX_MRO_TRAVERSAL_DEPTH: usize = 1024;
#[derive(Debug)]
pub struct SemanticAnalyzer {
pub(super) symbol_table: SymbolTable,
pub(super) semantic_tokens: Vec<SemanticToken>,
pub(super) hover_info: HashMap<SourceLocation, HoverInfo>,
pub(super) source: String,
pub class_models: Vec<ClassModel>,
pub export_metadata: FileExportMetadata,
}
impl SemanticAnalyzer {
pub fn analyze(ast: &Node) -> Self {
Self::analyze_with_source(ast, "")
}
pub fn analyze_with_source(ast: &Node, source: &str) -> Self {
let symbol_table = SymbolExtractor::new_with_source(source).extract(ast);
let class_models = ClassModelBuilder::new().build(ast);
let export_metadata = exporter_metadata::ExportMetadataBuilder::new().build(ast);
let mut analyzer = SemanticAnalyzer {
symbol_table,
semantic_tokens: Vec::new(),
hover_info: HashMap::new(),
source: source.to_string(),
class_models,
export_metadata,
};
analyzer.analyze_node(ast, 0);
analyzer
}
pub fn symbol_table(&self) -> &SymbolTable {
&self.symbol_table
}
pub fn semantic_tokens(&self) -> &[SemanticToken] {
&self.semantic_tokens
}
pub fn export_metadata(&self) -> &FileExportMetadata {
&self.export_metadata
}
pub fn hover_at(&self, location: SourceLocation) -> Option<&HoverInfo> {
self.hover_info.get(&location)
}
pub fn all_hover_entries(&self) -> impl Iterator<Item = &HoverInfo> {
self.hover_info.values()
}
pub fn symbol_at(&self, location: SourceLocation) -> Option<&Symbol> {
let mut best: Option<&Symbol> = None;
let mut best_span = usize::MAX;
for symbols in self.symbol_table.symbols.values() {
for symbol in symbols {
if symbol.location.start <= location.start && symbol.location.end >= location.end {
let span = symbol.location.end - symbol.location.start;
if span < best_span {
best = Some(symbol);
best_span = span;
}
}
}
}
best
}
pub fn find_definition(&self, position: usize) -> Option<&Symbol> {
for refs in self.symbol_table.references.values() {
for reference in refs {
if reference.location.start <= position && reference.location.end >= position {
let symbols = self.resolve_reference_to_symbols(reference);
if let Some(first_symbol) = symbols.first() {
return Some(self.resolve_definition_target(first_symbol));
}
}
}
}
self.symbol_at(SourceLocation { start: position, end: position })
.map(|symbol| self.resolve_definition_target(symbol))
}
fn resolve_definition_target<'a>(&'a self, symbol: &'a Symbol) -> &'a Symbol {
if let Some(target) = self.resolve_method_modifier_target(symbol) { target } else { symbol }
}
fn resolve_method_modifier_target<'a>(&'a self, symbol: &'a Symbol) -> Option<&'a Symbol> {
if !matches!(
symbol.declaration.as_deref(),
Some("before" | "after" | "around" | "override" | "augment")
) {
return None;
}
self.symbol_table
.find_symbol(&symbol.name, symbol.scope_id, crate::symbol::SymbolKind::Subroutine)
.into_iter()
.find(|candidate| {
candidate.location != symbol.location
&& !matches!(
candidate.declaration.as_deref(),
Some("before" | "after" | "around" | "override" | "augment")
)
})
}
pub fn is_file_test_operator(op: &str) -> bool {
builtins::is_file_test_operator(op)
}
pub fn resolve_inherited_method_hover(
&self,
receiver_class: &str,
method_name: &str,
) -> Option<HoverInfo> {
self.resolve_inherited_method_hover_ordered(receiver_class, method_name)
}
pub fn resolve_inherited_method_location(
&self,
receiver_class: &str,
method_name: &str,
) -> Option<SourceLocation> {
let models_by_name: HashMap<&str, &ClassModel> =
self.class_models.iter().map(|model| (model.name.as_str(), model)).collect();
let receiver_model = models_by_name.get(receiver_class).copied()?;
let ancestor_order = match receiver_model.mro {
MethodResolutionOrder::Dfs => self.dfs_ancestor_order(receiver_class, &models_by_name),
MethodResolutionOrder::C3 => self.c3_ancestor_order(receiver_class, &models_by_name),
};
for ancestor in ancestor_order {
if let Some(model) = models_by_name.get(ancestor.as_str()).copied()
&& let Some(location) = self.method_location_in_model(model, method_name)
{
return Some(location);
}
}
if is_universal_method(method_name) {
return self
.symbol_table
.symbols
.get(method_name)
.and_then(|symbols| {
symbols.iter().find(|symbol| {
symbol.kind == crate::symbol::SymbolKind::Subroutine
&& symbol.qualified_name == format!("UNIVERSAL::{method_name}")
})
})
.map(|symbol| symbol.location);
}
None
}
pub fn resolve_parent_chain(&self, receiver_class: &str) -> Option<Vec<String>> {
let models_by_name: HashMap<&str, &ClassModel> =
self.class_models.iter().map(|model| (model.name.as_str(), model)).collect();
let receiver_model = models_by_name.get(receiver_class).copied()?;
let chain = match receiver_model.mro {
MethodResolutionOrder::Dfs => self.dfs_ancestor_order(receiver_class, &models_by_name),
MethodResolutionOrder::C3 => self.c3_ancestor_order(receiver_class, &models_by_name),
};
Some(chain)
}
fn resolve_inherited_method_hover_ordered(
&self,
receiver_class: &str,
method_name: &str,
) -> Option<HoverInfo> {
let models_by_name: HashMap<&str, &ClassModel> =
self.class_models.iter().map(|model| (model.name.as_str(), model)).collect();
let Some(receiver_model) = models_by_name.get(receiver_class).copied() else {
return self.resolve_plain_package_method_hover(receiver_class, method_name);
};
if let Some(hover) =
self.hover_for_model_method(receiver_model, receiver_class, method_name)
{
return Some(hover);
}
let ancestor_order = match receiver_model.mro {
MethodResolutionOrder::Dfs => self.dfs_ancestor_order(receiver_class, &models_by_name),
MethodResolutionOrder::C3 => self.c3_ancestor_order(receiver_class, &models_by_name),
};
for ancestor in ancestor_order {
if let Some(model) = models_by_name.get(ancestor.as_str()).copied() {
if let Some(hover) = self.hover_for_model_method(model, receiver_class, method_name)
{
return Some(hover);
}
} else if let Some(hover) =
self.resolve_plain_package_method_hover(&ancestor, method_name)
{
return Some(hover);
}
}
if is_universal_method(method_name) {
return Some(HoverInfo {
signature: format!("sub UNIVERSAL::{method_name}"),
documentation: None,
details: vec!["Defined in UNIVERSAL".to_string()],
});
}
None
}
fn hover_for_model_method(
&self,
model: &ClassModel,
receiver_class: &str,
method_name: &str,
) -> Option<HoverInfo> {
if model.methods.iter().any(|m| m.name == method_name) {
let is_direct = model.name == receiver_class;
let details = if is_direct {
vec![format!("Defined in {}", model.name)]
} else {
vec![format!("Inherited from {}", model.name)]
};
return Some(HoverInfo {
signature: format!("sub {}::{}", model.name, method_name),
documentation: None,
details,
});
}
if model.methods.iter().any(|m| m.name == "AUTOLOAD") {
let is_direct = model.name == receiver_class;
let details = if is_direct {
vec![
format!("Resolved via AUTOLOAD in {}", model.name),
format!("Requested method: {method_name}"),
]
} else {
vec![
format!("Resolved via inherited AUTOLOAD from {}", model.name),
format!("Requested method: {method_name}"),
]
};
return Some(HoverInfo {
signature: format!("sub {}::AUTOLOAD", model.name),
documentation: None,
details,
});
}
None
}
fn method_location_in_model(
&self,
model: &ClassModel,
method_name: &str,
) -> Option<SourceLocation> {
model
.methods
.iter()
.find(|method| method.name == method_name)
.or_else(|| model.methods.iter().find(|method| method.name == "AUTOLOAD"))
.map(|method| method.location)
}
fn resolve_plain_package_method_hover(
&self,
package_name: &str,
method_name: &str,
) -> Option<HoverInfo> {
let qualified = format!("{}::{}", package_name, method_name);
let found_in_table = self.symbol_table.symbols.get(method_name).is_some_and(|syms| {
syms.iter().any(|s| {
matches!(s.kind, crate::symbol::SymbolKind::Subroutine)
&& s.qualified_name == qualified
})
}) || self.symbol_table.symbols.contains_key(&qualified);
if found_in_table {
return Some(HoverInfo {
signature: format!("sub {}::{}", package_name, method_name),
documentation: None,
details: vec![format!("Inherited from {}", package_name)],
});
}
let qualified_autoload = format!("{}::AUTOLOAD", package_name);
let autoload_in_table = self.symbol_table.symbols.get("AUTOLOAD").is_some_and(|syms| {
syms.iter().any(|s| {
matches!(s.kind, crate::symbol::SymbolKind::Subroutine)
&& s.qualified_name == qualified_autoload
})
}) || self.symbol_table.symbols.contains_key(&qualified_autoload);
if autoload_in_table {
return Some(HoverInfo {
signature: format!("sub {}::AUTOLOAD", package_name),
documentation: None,
details: vec![
format!("Resolved via AUTOLOAD in {}", package_name),
format!("Requested method: {}", method_name),
],
});
}
if is_universal_method(method_name) {
return Some(HoverInfo {
signature: format!("sub UNIVERSAL::{method_name}"),
documentation: None,
details: vec!["Defined in UNIVERSAL".to_string()],
});
}
None
}
fn dfs_ancestor_order(
&self,
package: &str,
models_by_name: &HashMap<&str, &ClassModel>,
) -> Vec<String> {
fn walk(
package: &str,
models_by_name: &HashMap<&str, &ClassModel>,
seen: &mut HashSet<String>,
out: &mut Vec<String>,
depth: usize,
) {
if depth >= MAX_MRO_TRAVERSAL_DEPTH {
return;
}
let Some(model) = models_by_name.get(package).copied() else {
return;
};
for parent in &model.parents {
if seen.insert(parent.clone()) {
out.push(parent.clone());
walk(parent, models_by_name, seen, out, depth + 1);
}
}
}
let mut seen = HashSet::from([package.to_string()]);
let mut out = Vec::new();
walk(package, models_by_name, &mut seen, &mut out, 0);
out
}
fn c3_ancestor_order(
&self,
package: &str,
models_by_name: &HashMap<&str, &ClassModel>,
) -> Vec<String> {
fn linearize(
package: &str,
models_by_name: &HashMap<&str, &ClassModel>,
visited: &mut HashSet<String>,
depth: usize,
) -> Vec<String> {
if depth >= MAX_MRO_TRAVERSAL_DEPTH {
return vec![package.to_string()];
}
if !visited.insert(package.to_string()) {
return vec![];
}
let Some(model) = models_by_name.get(package).copied() else {
return vec![package.to_string()];
};
let parents = model.parents.clone();
if parents.is_empty() {
return vec![package.to_string()];
}
let mut parent_mros: Vec<Vec<String>> = parents
.iter()
.map(|parent| linearize(parent, models_by_name, &mut visited.clone(), depth + 1))
.collect();
parent_mros.push(parents.clone());
let mut result = vec![package.to_string()];
loop {
parent_mros.retain(|list| !list.is_empty());
if parent_mros.is_empty() {
break;
}
let chosen = parent_mros.iter().find_map(|list| {
let candidate = list.first()?;
let in_tail = parent_mros
.iter()
.any(|other| other.iter().skip(1).any(|name| name == candidate));
if in_tail { None } else { Some(candidate.clone()) }
});
match chosen {
Some(name) => {
if !result.contains(&name) {
result.push(name.clone());
}
for list in &mut parent_mros {
if list.first().is_some_and(|head| head == &name) {
list.remove(0);
}
}
}
None => {
for list in parent_mros {
if let Some(head) = list.first()
&& !result.contains(head)
{
result.push(head.clone());
}
}
break;
}
}
}
result
}
linearize(package, models_by_name, &mut HashSet::new(), 0).into_iter().skip(1).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{Node, NodeKind};
use crate::parser::Parser;
use crate::symbol::SymbolKind;
#[test]
fn test_semantic_tokens() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my $x = 42;
print $x;
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze(&ast);
let tokens = analyzer.semantic_tokens();
let x_tokens: Vec<_> = tokens
.iter()
.filter(|t| {
matches!(
t.token_type,
SemanticTokenType::Variable | SemanticTokenType::VariableDeclaration
)
})
.collect();
assert!(!x_tokens.is_empty());
assert!(x_tokens[0].modifiers.contains(&SemanticTokenModifier::Declaration));
Ok(())
}
#[test]
fn test_hover_info() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
sub foo {
return 42;
}
my $result = foo();
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze(&ast);
assert!(!analyzer.hover_info.is_empty());
Ok(())
}
#[test]
fn test_hover_doc_from_pod() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
# This is foo
# More docs
sub foo {
return 1;
}
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let sym = analyzer.symbol_table().symbols.get("foo").ok_or("symbol not found")?[0].clone();
let hover = analyzer.hover_at(sym.location).ok_or("hover not found")?;
assert!(hover.documentation.as_ref().ok_or("doc not found")?.contains("This is foo"));
Ok(())
}
#[test]
fn test_comment_doc_extraction() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
# Adds two numbers
sub add { 1 }
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let sub_symbols =
analyzer.symbol_table().find_symbol("add", 0, crate::symbol::SymbolKind::Subroutine);
assert!(!sub_symbols.is_empty());
let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
assert_eq!(hover.documentation.as_deref(), Some("Adds two numbers"));
Ok(())
}
#[test]
fn test_extract_documentation_with_out_of_bounds_offset()
-> Result<(), Box<dyn std::error::Error>> {
let code = "sub add { 1 }\n";
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
assert_eq!(analyzer.extract_documentation(code.len() + 1), None);
Ok(())
}
#[test]
fn test_cross_package_navigation() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
package Foo {
# bar sub
sub bar { 42 }
}
package main;
Foo::bar();
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let pos = code.find("Foo::bar").ok_or("Foo::bar not found")? + 5; let def = analyzer.find_definition(pos).ok_or("definition")?;
assert_eq!(def.name, "bar");
let hover = analyzer.hover_at(def.location).ok_or("hover not found")?;
assert!(hover.documentation.as_ref().ok_or("doc not found")?.contains("bar sub"));
Ok(())
}
#[test]
fn test_universal_method_hover_fallback() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
package UNIVERSAL;
sub can { 1 }
sub isa { 1 }
package Foo;
sub new { bless {}, shift }
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let hover = analyzer
.resolve_inherited_method_hover("Foo", "can")
.ok_or("expected UNIVERSAL hover fallback")?;
assert!(
hover.signature.contains("UNIVERSAL::can"),
"expected UNIVERSAL hover signature, got: {}",
hover.signature
);
assert!(
hover.details.iter().any(|detail| detail.contains("UNIVERSAL")),
"expected UNIVERSAL hover details, got: {:?}",
hover.details
);
Ok(())
}
#[test]
fn test_autoload_hover_fallback() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
package Foo;
sub AUTOLOAD { 1 }
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let hover = analyzer
.resolve_inherited_method_hover("Foo", "dynamic_method")
.ok_or("expected AUTOLOAD hover fallback")?;
assert!(
hover.signature.contains("Foo::AUTOLOAD"),
"expected AUTOLOAD hover signature, got: {}",
hover.signature
);
assert!(
hover.details.iter().any(|detail| detail.contains("AUTOLOAD")),
"expected AUTOLOAD hover details, got: {:?}",
hover.details
);
assert!(
hover.details.iter().any(|detail| detail.contains("dynamic_method")),
"expected requested method detail, got: {:?}",
hover.details
);
Ok(())
}
#[test]
fn test_scope_identification() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my $x = 0;
package Foo {
my $x = 1;
sub bar { return $x; }
}
my $y = $x;
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let inner_ref_pos = code.find("return $x").ok_or("return $x not found")? + "return ".len();
let inner_def = analyzer.find_definition(inner_ref_pos).ok_or("inner def not found")?;
let expected_inner = code.find("my $x = 1").ok_or("my $x = 1 not found")? + 3;
assert_eq!(inner_def.location.start, expected_inner);
let outer_ref_pos = code.rfind("$x;").ok_or("$x; not found")?;
let outer_def = analyzer.find_definition(outer_ref_pos).ok_or("outer def not found")?;
let expected_outer = code.find("my $x = 0").ok_or("my $x = 0 not found")? + 3;
assert_eq!(outer_def.location.start, expected_outer);
Ok(())
}
#[test]
fn test_pod_documentation_extraction() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"# Simple comment before sub
sub documented_with_comment {
return "test";
}
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let sub_symbols = analyzer.symbol_table().find_symbol(
"documented_with_comment",
0,
crate::symbol::SymbolKind::Subroutine,
);
assert!(!sub_symbols.is_empty());
let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
let doc = hover.documentation.as_ref().ok_or("doc not found")?;
assert!(doc.contains("Simple comment before sub"));
Ok(())
}
#[test]
fn test_empty_source_handling() -> Result<(), Box<dyn std::error::Error>> {
let code = "";
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
assert!(analyzer.semantic_tokens().is_empty());
assert!(analyzer.hover_info.is_empty());
Ok(())
}
#[test]
fn test_multiple_comment_lines() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
# First comment
# Second comment
# Third comment
sub multi_commented {
1;
}
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let sub_symbols = analyzer.symbol_table().find_symbol(
"multi_commented",
0,
crate::symbol::SymbolKind::Subroutine,
);
assert!(!sub_symbols.is_empty());
let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
let doc = hover.documentation.as_ref().ok_or("doc not found")?;
assert!(doc.contains("First comment"));
assert!(doc.contains("Second comment"));
assert!(doc.contains("Third comment"));
Ok(())
}
#[test]
fn test_semantic_model_build_and_tokens() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my $x = 42;
my $y = 10;
$x + $y;
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let model = SemanticModel::build(&ast, code);
let tokens = model.tokens();
assert!(!tokens.is_empty(), "SemanticModel should provide tokens");
let var_tokens: Vec<_> = tokens
.iter()
.filter(|t| {
matches!(
t.token_type,
SemanticTokenType::Variable | SemanticTokenType::VariableDeclaration
)
})
.collect();
assert!(var_tokens.len() >= 2, "Should have at least 2 variable tokens");
Ok(())
}
#[test]
fn test_semantic_model_symbol_table_access() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my $x = 42;
sub foo {
my $y = $x;
}
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let model = SemanticModel::build(&ast, code);
let symbol_table = model.symbol_table();
let x_symbols = symbol_table.find_symbol("x", 0, SymbolKind::scalar());
assert!(!x_symbols.is_empty(), "Should find $x in symbol table");
let foo_symbols = symbol_table.find_symbol("foo", 0, SymbolKind::Subroutine);
assert!(!foo_symbols.is_empty(), "Should find sub foo in symbol table");
Ok(())
}
#[test]
fn test_semantic_model_hover_info() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
# This is a documented variable
my $documented = 42;
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let model = SemanticModel::build(&ast, code);
let symbol_table = model.symbol_table();
let symbols = symbol_table.find_symbol("documented", 0, SymbolKind::scalar());
assert!(!symbols.is_empty(), "Should find $documented");
if let Some(hover) = model.hover_info_at(symbols[0].location) {
assert!(hover.signature.contains("documented"), "Hover should contain variable name");
}
Ok(())
}
#[test]
fn test_analyzer_find_definition_scalar() -> Result<(), Box<dyn std::error::Error>> {
let code = "my $x = 1;\n$x + 2;\n";
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let ref_line = code.lines().nth(1).ok_or("line 2 not found")?;
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")?;
let ref_pos = line_offset + col_in_line;
let symbol =
analyzer.find_definition(ref_pos).ok_or("definition not found for $x reference")?;
assert_eq!(symbol.name, "x");
assert_eq!(symbol.kind, SymbolKind::scalar());
assert!(
symbol.location.start < ref_pos,
"Declaration {:?} should precede reference at byte {}",
symbol.location.start,
ref_pos
);
Ok(())
}
#[test]
fn test_semantic_model_definition_at() -> Result<(), Box<dyn std::error::Error>> {
let code = "my $x = 1;\n$x + 2;\n";
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let model = SemanticModel::build(&ast, code);
let ref_line_index = 1;
let ref_line = code.lines().nth(ref_line_index).ok_or("line not found")?;
let col_in_line = ref_line.find("$x").ok_or("could not find $x")?;
let byte_offset = code
.lines()
.take(ref_line_index)
.map(|l| l.len() + 1) .sum::<usize>()
+ col_in_line;
let definition = model.definition_at(byte_offset);
assert!(
definition.is_some(),
"definition_at returned None for $x reference at {}",
byte_offset
);
if let Some(symbol) = definition {
assert_eq!(symbol.name, "x");
assert_eq!(symbol.kind, SymbolKind::scalar());
assert!(
symbol.location.start < byte_offset,
"Declaration {:?} should precede reference at byte {}",
symbol.location.start,
byte_offset
);
}
Ok(())
}
#[test]
fn test_analyzer_find_definition_goto_label() -> Result<(), Box<dyn std::error::Error>> {
let code = "START: while (1) {\n goto START;\n}\n";
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let ref_pos = code.find("START;\n").ok_or("could not find goto label")?;
let symbol = analyzer
.find_definition(ref_pos)
.ok_or("definition not found for goto label reference")?;
assert_eq!(symbol.name, "START");
assert_eq!(symbol.kind, SymbolKind::Label);
assert!(
symbol.location.start < ref_pos,
"Label definition {:?} should precede goto reference at byte {}",
symbol.location.start,
ref_pos
);
Ok(())
}
#[test]
fn test_anonymous_subroutine_semantic_tokens() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my $closure = sub {
my $x = 42;
return $x + 1;
};
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let tokens = analyzer.semantic_tokens();
let sub_keywords: Vec<_> =
tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Keyword)).collect();
assert!(!sub_keywords.is_empty(), "Should have keyword token for 'sub'");
let sub_position = code.find("sub {").ok_or("sub { not found")?;
let hover_exists = analyzer
.hover_info
.iter()
.any(|(loc, _)| loc.start <= sub_position && loc.end >= sub_position);
assert!(hover_exists, "Should have hover info for anonymous subroutine");
Ok(())
}
#[test]
fn test_infer_type_for_literals() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my $num = 42;
my $str = "hello";
my @arr = (1, 2, 3);
my %hash = (a => 1);
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
fn find_number_node(node: &Node) -> Option<&Node> {
match &node.kind {
NodeKind::Number { .. } => Some(node),
NodeKind::Program { statements } | NodeKind::Block { statements } => {
for stmt in statements {
if let Some(found) = find_number_node(stmt) {
return Some(found);
}
}
None
}
NodeKind::VariableDeclaration { initializer, .. } => {
initializer.as_ref().and_then(|init| find_number_node(init))
}
_ => None,
}
}
if let Some(num_node) = find_number_node(&ast) {
let inferred = analyzer.infer_type(num_node);
assert_eq!(inferred, Some("number".to_string()), "Should infer number type");
}
Ok(())
}
#[test]
fn test_infer_type_for_binary_operations() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"my $sum = 10 + 20;
my $concat = "a" . "b";
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
fn find_binary_node<'a>(node: &'a Node, op: &str) -> Option<&'a Node> {
match &node.kind {
NodeKind::Binary { op: node_op, .. } if node_op == op => Some(node),
NodeKind::Program { statements } | NodeKind::Block { statements } => {
for stmt in statements {
if let Some(found) = find_binary_node(stmt, op) {
return Some(found);
}
}
None
}
NodeKind::VariableDeclaration { initializer, .. } => {
initializer.as_ref().and_then(|init| find_binary_node(init, op))
}
_ => None,
}
}
if let Some(add_node) = find_binary_node(&ast, "+") {
let inferred = analyzer.infer_type(add_node);
assert_eq!(inferred, Some("number".to_string()), "Arithmetic should infer to number");
}
if let Some(concat_node) = find_binary_node(&ast, ".") {
let inferred = analyzer.infer_type(concat_node);
assert_eq!(
inferred,
Some("string".to_string()),
"Concatenation should infer to string"
);
}
Ok(())
}
#[test]
fn test_anonymous_subroutine_hover_info() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
# This is a closure
my $adder = sub {
my ($x, $y) = @_;
return $x + $y;
};
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let sub_position = code.find("sub {").ok_or("sub { not found")?;
let hover = analyzer
.hover_info
.iter()
.find(|(loc, _)| loc.start <= sub_position && loc.end >= sub_position)
.map(|(_, h)| h);
assert!(hover.is_some(), "Should have hover info");
if let Some(h) = hover {
assert!(h.signature.contains("sub"), "Hover signature should contain 'sub'");
assert!(
h.details.iter().any(|d| d.contains("Anonymous")),
"Hover details should mention anonymous subroutine"
);
if let Some(doc) = &h.documentation {
assert!(
doc.contains("closure"),
"If documentation found, it should mention closure"
);
}
}
Ok(())
}
#[test]
fn test_substitution_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my $str = "hello world";
$str =~ s/world/Perl/;
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze(&ast);
let tokens = analyzer.semantic_tokens();
let operator_tokens: Vec<_> =
tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
assert!(!operator_tokens.is_empty(), "Should have operator tokens for substitution");
Ok(())
}
#[test]
fn test_transliteration_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my $str = "hello";
$str =~ tr/el/ol/;
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze(&ast);
let tokens = analyzer.semantic_tokens();
let operator_tokens: Vec<_> =
tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
assert!(!operator_tokens.is_empty(), "Should have operator tokens for transliteration");
Ok(())
}
#[test]
fn test_reference_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my $x = 42;
my $ref = \$x;
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze(&ast);
let tokens = analyzer.semantic_tokens();
let operator_tokens: Vec<_> =
tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
assert!(!operator_tokens.is_empty(), "Should have operator tokens for reference operator");
Ok(())
}
#[test]
fn test_postfix_loop_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my @list = (1, 2, 3);
print $_ for @list;
my $x = 0;
$x++ while $x < 10;
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze(&ast);
let tokens = analyzer.semantic_tokens();
let control_tokens: Vec<_> = tokens
.iter()
.filter(|t| matches!(t.token_type, SemanticTokenType::KeywordControl))
.collect();
assert!(!control_tokens.is_empty(), "Should have control keyword tokens for postfix loops");
Ok(())
}
#[test]
fn test_file_test_operator_semantic_token() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my $file = "test.txt";
if (-e $file) {
print "exists";
}
if (-d $file) {
print "directory";
}
if (-f $file) {
print "file";
}
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze(&ast);
let tokens = analyzer.semantic_tokens();
let operator_tokens: Vec<_> =
tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
assert!(!operator_tokens.is_empty(), "Should have operator tokens for file test operators");
Ok(())
}
#[test]
fn test_all_file_test_operators_recognized() -> Result<(), Box<dyn std::error::Error>> {
let file_test_ops = vec![
"-e", "-d", "-f", "-r", "-w", "-x", "-s", "-z", "-T", "-B", "-M", "-A", "-C", "-l",
"-p", "-S", "-u", "-g", "-k", "-t", "-O", "-G", "-R", "-b", "-c",
];
for op in file_test_ops {
assert!(
SemanticAnalyzer::is_file_test_operator(op),
"Operator {} should be recognized as file test operator",
op
);
}
assert!(
!SemanticAnalyzer::is_file_test_operator("+"),
"Operator '+' should not be recognized as file test operator"
);
assert!(
!SemanticAnalyzer::is_file_test_operator("-"),
"Operator '-' should not be recognized as file test operator"
);
assert!(
!SemanticAnalyzer::is_file_test_operator("++"),
"Operator '++' should not be recognized as file test operator"
);
Ok(())
}
#[test]
fn test_postfix_loop_modifiers() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my @items = (1, 2, 3);
print $_ for @items;
print $_ foreach @items;
my $x = 0;
$x++ while $x < 10;
$x-- until $x < 0;
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze(&ast);
let tokens = analyzer.semantic_tokens();
let control_tokens: Vec<_> = tokens
.iter()
.filter(|t| matches!(t.token_type, SemanticTokenType::KeywordControl))
.collect();
assert!(
control_tokens.len() >= 4,
"Should have at least 4 control keyword tokens for postfix loop modifiers"
);
Ok(())
}
#[test]
fn test_substitution_with_modifiers() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my $str = "hello world";
$str =~ s/world/Perl/gi;
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze(&ast);
let tokens = analyzer.semantic_tokens();
let operator_tokens: Vec<_> =
tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
assert!(
!operator_tokens.is_empty(),
"Should have operator tokens for substitution with modifiers"
);
Ok(())
}
#[test]
fn test_transliteration_y_operator() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my $str = "hello";
$str =~ y/hello/world/;
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze(&ast);
let tokens = analyzer.semantic_tokens();
let operator_tokens: Vec<_> =
tokens.iter().filter(|t| matches!(t.token_type, SemanticTokenType::Operator)).collect();
assert!(
!operator_tokens.is_empty(),
"Should have operator tokens for y/// transliteration"
);
Ok(())
}
#[test]
fn test_builtin_documentation_coverage() -> Result<(), Box<dyn std::error::Error>> {
let builtins = [
"print", "say", "push", "pop", "shift", "unshift", "map", "grep", "sort", "reverse",
"split", "join", "chomp", "chop", "length", "substr", "index", "rindex", "lc", "uc",
"die", "warn", "eval", "open", "close", "read", "keys", "values", "exists", "delete",
"defined", "ref", "bless", "sprintf", "chr", "ord",
];
for name in &builtins {
let doc = get_builtin_documentation(name);
assert!(doc.is_some(), "Built-in '{}' should have documentation", name);
let doc = doc.unwrap();
assert!(
!doc.signature.is_empty(),
"Built-in '{}' should have a non-empty signature",
name
);
assert!(
!doc.description.is_empty(),
"Built-in '{}' should have a non-empty description",
name
);
}
Ok(())
}
#[test]
fn test_builtin_hover_for_function_call() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
my @items = (3, 1, 4);
push @items, 5;
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let push_pos = code.find("push").ok_or("push not found")?;
let hover_for_push =
analyzer.hover_info.iter().find(|(loc, _)| loc.start <= push_pos && loc.end > push_pos);
assert!(hover_for_push.is_some(), "Should have hover info for 'push' builtin");
let (_, hover) = hover_for_push.unwrap();
assert!(
hover.signature.contains("push"),
"Hover signature should contain 'push', got: {}",
hover.signature
);
assert!(hover.documentation.is_some(), "Hover for 'push' should have documentation");
Ok(())
}
#[test]
fn test_core_prefixed_builtin_hover_for_function_call() -> Result<(), Box<dyn std::error::Error>>
{
let code = r#"
my $value = "abc";
CORE::length($value);
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let length_pos = code.find("CORE::length").ok_or("CORE::length not found")?;
let hover = analyzer
.hover_info
.iter()
.find(|(loc, _)| loc.start <= length_pos && loc.end > length_pos);
assert!(hover.is_some(), "Should have hover info for CORE::length builtin");
let (_, hover) = hover.ok_or("missing hover for CORE::length")?;
assert!(
hover.signature.contains("length"),
"Hover signature should contain 'length', got: {}",
hover.signature
);
Ok(())
}
#[test]
fn test_package_hover_with_pod_name_section() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
=head1 NAME
My::Module - A great module for testing
=head1 DESCRIPTION
This module does great things.
=cut
package My::Module;
sub new { bless {}, shift }
1;
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let pkg_symbols = analyzer.symbol_table().symbols.get("My::Module");
assert!(pkg_symbols.is_some(), "Should find My::Module in symbol table");
let pkg = &pkg_symbols.unwrap()[0];
let hover = analyzer.hover_at(pkg.location);
assert!(hover.is_some(), "Should have hover info for package");
let hover = hover.unwrap();
assert!(
hover.signature.contains("package My::Module"),
"Package hover signature should contain 'package My::Module', got: {}",
hover.signature
);
if let Some(doc) = &hover.documentation {
assert!(
doc.contains("A great module for testing"),
"Package hover should contain POD NAME content, got: {}",
doc
);
}
Ok(())
}
#[test]
fn test_package_documentation_via_symbol() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
=head1 NAME
Utils - Utility functions
=cut
package Utils;
sub helper { 1 }
1;
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let pkg_symbols = analyzer.symbol_table().symbols.get("Utils");
assert!(pkg_symbols.is_some(), "Should find Utils package");
let pkg = &pkg_symbols.unwrap()[0];
assert!(
pkg.documentation.is_some(),
"Package symbol should have documentation from POD NAME section"
);
let doc = pkg.documentation.as_ref().unwrap();
assert!(
doc.contains("Utility functions"),
"Package doc should contain 'Utility functions', got: {}",
doc
);
Ok(())
}
#[test]
fn test_subroutine_with_pod_docs_hover() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"
=head2 process
Processes input data and returns the result.
=cut
sub process {
my ($input) = @_;
return $input * 2;
}
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let sub_symbols = analyzer.symbol_table().find_symbol("process", 0, SymbolKind::Subroutine);
assert!(!sub_symbols.is_empty(), "Should find sub process");
let hover = analyzer.hover_at(sub_symbols[0].location);
assert!(hover.is_some(), "Should have hover for sub process");
let hover = hover.unwrap();
assert!(
hover.signature.contains("sub process"),
"Hover should show sub signature, got: {}",
hover.signature
);
if let Some(doc) = &hover.documentation {
assert!(
doc.contains("process") || doc.contains("Processes"),
"Sub hover should contain POD documentation, got: {}",
doc
);
}
Ok(())
}
#[test]
fn test_variable_hover_shows_declaration_type() -> Result<(), Box<dyn std::error::Error>> {
let code = r#"my $count = 42;
my @items = (1, 2, 3);
my %config = (key => "value");
"#;
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let scalar_pos = code.find("$count").ok_or("$count not found")?;
let scalar_hover = analyzer
.hover_info
.iter()
.find(|(loc, _)| loc.start <= scalar_pos && loc.end > scalar_pos);
assert!(scalar_hover.is_some(), "Should have hover for $count");
let (_, hover) = scalar_hover.unwrap();
assert!(
hover.signature.contains("$count"),
"Scalar hover should show variable name, got: {}",
hover.signature
);
let array_pos = code.find("@items").ok_or("@items not found")?;
let array_hover = analyzer
.hover_info
.iter()
.find(|(loc, _)| loc.start <= array_pos && loc.end > array_pos);
assert!(array_hover.is_some(), "Should have hover for @items");
let (_, hover) = array_hover.unwrap();
assert!(
hover.signature.contains("@items"),
"Array hover should show variable name, got: {}",
hover.signature
);
let hash_pos = code.find("%config").ok_or("%config not found")?;
let hash_hover =
analyzer.hover_info.iter().find(|(loc, _)| loc.start <= hash_pos && loc.end > hash_pos);
assert!(hash_hover.is_some(), "Should have hover for %config");
let (_, hover) = hash_hover.unwrap();
assert!(
hover.signature.contains("%config"),
"Hash hover should show variable name, got: {}",
hover.signature
);
Ok(())
}
#[test]
fn test_signature_hover_shows_param_names() -> Result<(), Box<dyn std::error::Error>> {
let code = "sub add($x, $y) { $x + $y }";
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let sub_symbols =
analyzer.symbol_table().find_symbol("add", 0, crate::symbol::SymbolKind::Subroutine);
assert!(!sub_symbols.is_empty(), "symbol 'add' not found");
let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
assert!(
hover.signature.contains("$x"),
"hover signature should contain '$x', got: {}",
hover.signature
);
assert!(
hover.signature.contains("$y"),
"hover signature should contain '$y', got: {}",
hover.signature
);
assert!(
!hover.signature.contains("(...)"),
"hover signature must not fall back to '(...)', got: {}",
hover.signature
);
Ok(())
}
#[test]
fn test_signature_hover_with_optional_param() -> Result<(), Box<dyn std::error::Error>> {
let code = "sub greet($name, $greeting = 'Hello') { \"$greeting, $name\" }";
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let sub_symbols =
analyzer.symbol_table().find_symbol("greet", 0, crate::symbol::SymbolKind::Subroutine);
assert!(!sub_symbols.is_empty(), "symbol 'greet' not found");
let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
assert!(
hover.signature.contains("$name"),
"hover signature should contain '$name', got: {}",
hover.signature
);
assert!(
hover.signature.contains("$greeting"),
"hover signature should contain '$greeting', got: {}",
hover.signature
);
Ok(())
}
#[test]
fn test_signature_hover_with_slurpy_param() -> Result<(), Box<dyn std::error::Error>> {
let code = "sub log_all($level, @messages) { print \"$level: @messages\" }";
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let sub_symbols = analyzer.symbol_table().find_symbol(
"log_all",
0,
crate::symbol::SymbolKind::Subroutine,
);
assert!(!sub_symbols.is_empty(), "symbol 'log_all' not found");
let hover = analyzer.hover_at(sub_symbols[0].location).ok_or("hover not found")?;
assert!(
hover.signature.contains("@messages"),
"hover signature should contain '@messages', got: {}",
hover.signature
);
Ok(())
}
#[test]
fn test_find_definition_returns_method_kind_for_native_method()
-> Result<(), Box<dyn std::error::Error>> {
let code = "class Foo {\n method bar { return 1; }\n}\n";
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
let line1 = code.lines().nth(1).ok_or("no line 1")?;
let line0_len = code.lines().next().ok_or("no line 0")?.len() + 1;
let col = line1.find("bar").ok_or("bar not found on line 1")?;
let offset = line0_len + col;
let sym = analyzer.find_definition(offset).ok_or("no symbol found at 'bar'")?;
assert_eq!(sym.name, "bar", "symbol name should be 'bar'");
assert_eq!(
sym.kind,
SymbolKind::Method,
"native method should have SymbolKind::Method, got {:?}",
sym.kind
);
Ok(())
}
#[test]
fn test_find_definition_redirects_method_modifier_to_target_method()
-> Result<(), Box<dyn std::error::Error>> {
let code = include_str!(
"../../../../perl-lsp-rs/tests/fixtures/frameworks/moo_method_modifiers.pl"
);
let mut parser = Parser::new(code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, code);
for target_line in [8, 13, 18] {
let line = code.lines().nth(target_line).ok_or("missing modifier line")?;
let col = line.find("save").ok_or("modifier target not found")?;
let mut offset = 0;
for line in code.lines().take(target_line) {
offset += line.len() + 1;
}
offset += col;
let sym = analyzer
.find_definition(offset)
.ok_or("no symbol found at method modifier target")?;
assert_eq!(sym.name, "save", "modifier target should resolve to save");
let method_start = code.find("sub save").ok_or("method declaration not found")?;
assert_eq!(
sym.location.start, method_start,
"modifier target should resolve to the underlying method declaration"
);
assert_eq!(
sym.declaration, None,
"definition should land on the real method, not the synthetic modifier"
);
}
Ok(())
}
#[test]
fn test_resolve_inherited_method_location_limits_dfs_depth()
-> Result<(), Box<dyn std::error::Error>> {
let chain_len = MAX_MRO_TRAVERSAL_DEPTH + 10;
let mut code = String::new();
for i in 0..chain_len {
code.push_str(&format!("package P{i}; use parent 'P{}';\n", i + 1));
}
code.push_str(&format!("package P{chain_len}; sub target {{ 1 }}\n"));
let mut parser = Parser::new(&code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, &code);
let location = analyzer.resolve_inherited_method_location("P0", "target");
assert!(location.is_none(), "DFS traversal should stop at depth limit");
Ok(())
}
#[test]
fn test_resolve_inherited_method_location_limits_c3_depth()
-> Result<(), Box<dyn std::error::Error>> {
let chain_len = MAX_MRO_TRAVERSAL_DEPTH + 10;
let mut code = String::new();
code.push_str("package P0; use mro 'c3'; use parent 'P1';\n");
for i in 1..chain_len {
code.push_str(&format!("package P{i}; use parent 'P{}';\n", i + 1));
}
code.push_str(&format!("package P{chain_len}; sub target {{ 1 }}\n"));
let mut parser = Parser::new(&code);
let ast = parser.parse()?;
let analyzer = SemanticAnalyzer::analyze_with_source(&ast, &code);
let location = analyzer.resolve_inherited_method_location("P0", "target");
assert!(location.is_none(), "C3 traversal should stop at depth limit");
Ok(())
}
}