use crate::SourceLocation;
use crate::ast::{Node, NodeKind};
use regex::Regex;
use std::collections::{HashMap, HashSet};
use std::sync::OnceLock;
const UNIVERSAL_METHODS: [&str; 4] = ["can", "isa", "DOES", "VERSION"];
pub use perl_symbol::{SymbolKind, VarKind};
#[derive(Debug, Clone)]
pub struct Symbol {
pub name: String,
pub qualified_name: String,
pub kind: SymbolKind,
pub location: SourceLocation,
pub scope_id: ScopeId,
pub declaration: Option<String>,
pub documentation: Option<String>,
pub attributes: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct SymbolReference {
pub name: String,
pub kind: SymbolKind,
pub location: SourceLocation,
pub scope_id: ScopeId,
pub is_write: bool,
}
pub type ScopeId = usize;
#[derive(Debug, Clone)]
pub struct Scope {
pub id: ScopeId,
pub parent: Option<ScopeId>,
pub kind: ScopeKind,
pub location: SourceLocation,
pub symbols: HashSet<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScopeKind {
Global,
Package,
Subroutine,
Block,
Eval,
}
#[derive(Debug, Default)]
pub struct SymbolTable {
pub symbols: HashMap<String, Vec<Symbol>>,
pub references: HashMap<String, Vec<SymbolReference>>,
pub scopes: HashMap<ScopeId, Scope>,
scope_stack: Vec<ScopeId>,
next_scope_id: ScopeId,
current_package: String,
}
pub fn is_universal_method(method_name: &str) -> bool {
UNIVERSAL_METHODS.contains(&method_name)
}
impl SymbolTable {
pub fn new() -> Self {
let mut table = SymbolTable {
symbols: HashMap::new(),
references: HashMap::new(),
scopes: HashMap::new(),
scope_stack: vec![0],
next_scope_id: 1,
current_package: "main".to_string(),
};
table.scopes.insert(
0,
Scope {
id: 0,
parent: None,
kind: ScopeKind::Global,
location: SourceLocation { start: 0, end: 0 },
symbols: HashSet::new(),
},
);
table
}
fn current_scope(&self) -> ScopeId {
*self.scope_stack.last().unwrap_or(&0)
}
fn push_scope(&mut self, kind: ScopeKind, location: SourceLocation) -> ScopeId {
let parent = self.current_scope();
let scope_id = self.next_scope_id;
self.next_scope_id += 1;
let scope =
Scope { id: scope_id, parent: Some(parent), kind, location, symbols: HashSet::new() };
self.scopes.insert(scope_id, scope);
self.scope_stack.push(scope_id);
scope_id
}
fn pop_scope(&mut self) {
self.scope_stack.pop();
}
fn add_symbol(&mut self, symbol: Symbol) {
if symbol.name.is_empty() {
return;
}
let name = symbol.name.clone();
if let Some(scope) = self.scopes.get_mut(&symbol.scope_id) {
scope.symbols.insert(name.clone());
}
self.symbols.entry(name).or_default().push(symbol);
}
fn add_reference(&mut self, reference: SymbolReference) {
if reference.name.is_empty() {
return;
}
let name = reference.name.clone();
self.references.entry(name).or_default().push(reference);
}
pub fn find_symbol(&self, name: &str, from_scope: ScopeId, kind: SymbolKind) -> Vec<&Symbol> {
let mut results = Vec::new();
let mut current_scope_id = Some(from_scope);
while let Some(scope_id) = current_scope_id {
if let Some(scope) = self.scopes.get(&scope_id) {
if scope.symbols.contains(name) {
if let Some(symbols) = self.symbols.get(name) {
for symbol in symbols {
if symbol.scope_id == scope_id && symbol.kind == kind {
results.push(symbol);
}
}
}
}
if scope.kind != ScopeKind::Package {
if let Some(symbols) = self.symbols.get(name) {
for symbol in symbols {
if symbol.declaration.as_deref() == Some("our") && symbol.kind == kind {
results.push(symbol);
}
}
}
}
current_scope_id = scope.parent;
} else {
break;
}
}
results
}
pub fn find_references(&self, symbol: &Symbol) -> Vec<&SymbolReference> {
self.references
.get(&symbol.name)
.map(|refs| refs.iter().filter(|r| r.kind == symbol.kind).collect())
.unwrap_or_default()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameworkKind {
Moo,
MooRole,
Moose,
MooseRole,
RoleTiny,
RoleTinyWith,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WebFrameworkKind {
Dancer,
Dancer2,
MojoliciousLite,
PlackBuilder,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AsyncFrameworkKind {
AnyEvent,
EV,
Future,
FutureXS,
Promise,
PromiseXS,
POE,
IOAsync,
MojoRedis,
MojoPg,
}
#[derive(Debug, Clone, Default)]
pub struct FrameworkFlags {
pub moo: bool,
pub class_accessor: bool,
pub kind: Option<FrameworkKind>,
pub web_framework: Option<WebFrameworkKind>,
pub async_framework: Option<AsyncFrameworkKind>,
pub catalyst_controller: bool,
}
pub struct SymbolExtractor {
table: SymbolTable,
source: String,
framework_flags: HashMap<String, FrameworkFlags>,
const_fast_enabled: bool,
readonly_enabled: bool,
}
impl Default for SymbolExtractor {
fn default() -> Self {
Self::new()
}
}
impl SymbolExtractor {
pub fn new() -> Self {
SymbolExtractor {
table: SymbolTable::new(),
source: String::new(),
framework_flags: HashMap::new(),
const_fast_enabled: false,
readonly_enabled: false,
}
}
pub fn new_with_source(source: &str) -> Self {
SymbolExtractor {
table: SymbolTable::new(),
source: source.to_string(),
framework_flags: HashMap::new(),
const_fast_enabled: false,
readonly_enabled: false,
}
}
pub fn extract(mut self, node: &Node) -> SymbolTable {
self.visit_node(node);
self.upgrade_package_symbols_from_framework_flags();
self.table
}
fn upgrade_package_symbols_from_framework_flags(&mut self) {
for (pkg_name, flags) in &self.framework_flags {
let Some(kind) = flags.kind else {
continue;
};
let new_kind = match kind {
FrameworkKind::Moo | FrameworkKind::Moose | FrameworkKind::RoleTinyWith => {
SymbolKind::Class
}
FrameworkKind::MooRole | FrameworkKind::MooseRole | FrameworkKind::RoleTiny => {
SymbolKind::Role
}
};
if let Some(symbols) = self.table.symbols.get_mut(pkg_name) {
for symbol in symbols.iter_mut() {
if symbol.kind == SymbolKind::Package {
symbol.kind = new_kind;
}
}
}
}
}
fn visit_node(&mut self, node: &Node) {
match &node.kind {
NodeKind::Program { statements } => {
self.visit_statement_list(statements);
}
NodeKind::VariableDeclaration { declarator, variable, attributes, initializer } => {
let doc = self.extract_leading_comment(node.location.start);
self.handle_variable_declaration(
declarator,
variable,
attributes,
variable.location,
doc,
);
if let Some(init) = initializer {
self.visit_node(init);
}
}
NodeKind::VariableListDeclaration {
declarator,
variables,
attributes,
initializer,
} => {
let doc = self.extract_leading_comment(node.location.start);
for var in variables {
self.handle_variable_declaration(
declarator,
var,
attributes,
var.location,
doc.clone(),
);
}
if let Some(init) = initializer {
self.visit_node(init);
}
}
NodeKind::Variable { sigil, name } => {
let kind = match sigil.as_str() {
"$" => SymbolKind::scalar(),
"@" => SymbolKind::array(),
"%" => SymbolKind::hash(),
_ => return,
};
let reference = SymbolReference {
name: name.clone(),
kind,
location: node.location,
scope_id: self.table.current_scope(),
is_write: false, };
self.table.add_reference(reference);
}
NodeKind::Subroutine {
name,
prototype: _,
signature,
attributes,
body,
name_span: _,
} => {
let sub_name =
name.as_ref().map(|n| n.to_string()).unwrap_or_else(|| "<anon>".to_string());
if name.is_some() {
let documentation = self.extract_leading_comment(node.location.start);
let mut symbol_attributes = attributes.clone();
let documentation = if self.current_package_is_catalyst_controller()
&& let Some((action_kind, action_details)) =
Self::catalyst_action_metadata(attributes)
{
symbol_attributes.push("framework=Catalyst".to_string());
symbol_attributes.push("catalyst_controller=true".to_string());
symbol_attributes.push("catalyst_action=true".to_string());
symbol_attributes.push(format!("catalyst_action_kind={action_kind}"));
if !action_details.is_empty() {
symbol_attributes.push(format!(
"catalyst_action_attributes={}",
action_details.join(", ")
));
}
let action_doc = if action_details.is_empty() {
format!("Catalyst action ({action_kind})")
} else {
format!(
"Catalyst action ({action_kind}; {})",
action_details.join(", ")
)
};
match documentation {
Some(doc) => Some(format!("{doc}\n{action_doc}")),
None => Some(action_doc),
}
} else {
documentation
};
let symbol = Symbol {
name: sub_name.clone(),
qualified_name: format!("{}::{}", self.table.current_package, sub_name),
kind: SymbolKind::Subroutine,
location: node.location,
scope_id: self.table.current_scope(),
declaration: None,
documentation,
attributes: symbol_attributes,
};
self.table.add_symbol(symbol);
}
self.table.push_scope(ScopeKind::Subroutine, node.location);
if let Some(sig) = signature {
self.register_signature_params(sig);
}
self.visit_node(body);
self.table.pop_scope();
}
NodeKind::Package { name, block, name_span: _ } => {
let old_package = self.table.current_package.clone();
self.table.current_package = name.clone();
if Self::is_catalyst_controller_package_name(name) {
self.mark_catalyst_controller_package(name);
}
let documentation = self.extract_package_documentation(name, node.location);
let symbol = Symbol {
name: name.clone(),
qualified_name: name.clone(),
kind: SymbolKind::Package,
location: node.location,
scope_id: self.table.current_scope(),
declaration: None,
documentation,
attributes: vec![],
};
self.table.add_symbol(symbol);
if let Some(block_node) = block {
self.table.push_scope(ScopeKind::Package, node.location);
self.visit_node(block_node);
self.table.pop_scope();
self.table.current_package = old_package;
}
}
NodeKind::Block { statements } => {
self.table.push_scope(ScopeKind::Block, node.location);
self.visit_statement_list(statements);
self.table.pop_scope();
}
NodeKind::If { condition, then_branch, elsif_branches: _, else_branch } => {
self.visit_node(condition);
self.table.push_scope(ScopeKind::Block, then_branch.location);
self.visit_node(then_branch);
self.table.pop_scope();
if let Some(else_node) = else_branch {
self.table.push_scope(ScopeKind::Block, else_node.location);
self.visit_node(else_node);
self.table.pop_scope();
}
}
NodeKind::While { condition, body, continue_block: _ } => {
self.visit_node(condition);
self.table.push_scope(ScopeKind::Block, body.location);
self.visit_node(body);
self.table.pop_scope();
}
NodeKind::For { init, condition, update, body, .. } => {
self.table.push_scope(ScopeKind::Block, node.location);
if let Some(init_node) = init {
self.visit_node(init_node);
}
if let Some(cond_node) = condition {
self.visit_node(cond_node);
}
if let Some(update_node) = update {
self.visit_node(update_node);
}
self.visit_node(body);
self.table.pop_scope();
}
NodeKind::Foreach { variable, list, body, continue_block: _ } => {
self.table.push_scope(ScopeKind::Block, node.location);
self.handle_variable_declaration("my", variable, &[], variable.location, None);
self.visit_node(list);
self.visit_node(body);
self.table.pop_scope();
}
NodeKind::Assignment { lhs, rhs, .. } => {
self.mark_write_reference(lhs);
self.visit_node(lhs);
self.visit_node(rhs);
}
NodeKind::Binary { left, right, .. } => {
self.visit_node(left);
self.visit_node(right);
}
NodeKind::Unary { operand, .. } => {
self.visit_node(operand);
}
NodeKind::FunctionCall { name, args } => {
if self.const_fast_enabled
&& name == "const"
&& self.try_extract_const_fast_declaration(args)
{
return;
}
if self.readonly_enabled
&& name == "Readonly"
&& self.try_extract_readonly_declaration(args)
{
return;
}
let reference = SymbolReference {
name: name.clone(),
kind: SymbolKind::Subroutine,
location: node.location,
scope_id: self.table.current_scope(),
is_write: false,
};
self.table.add_reference(reference);
self.synthesize_plack_builder_symbols(name, args);
self.synthesize_ev_symbols(name, node.location);
for arg in args {
self.visit_node(arg);
}
}
NodeKind::MethodCall { object, method, args } => {
let location = self.method_reference_location(node, object, method);
self.table.add_reference(SymbolReference {
name: method.clone(),
kind: SymbolKind::Subroutine,
location,
scope_id: self.table.current_scope(),
is_write: false,
});
self.synthesize_async_framework_class_symbol(object);
self.synthesize_future_api_symbols(object, method, node.location);
self.visit_node(object);
for arg in args {
self.visit_node(arg);
}
}
NodeKind::ArrayLiteral { elements } => {
for elem in elements {
self.visit_node(elem);
}
}
NodeKind::HashLiteral { pairs } => {
for (key, value) in pairs {
self.visit_node(key);
self.visit_node(value);
}
}
NodeKind::Ternary { condition, then_expr, else_expr } => {
self.visit_node(condition);
self.visit_node(then_expr);
self.visit_node(else_expr);
}
NodeKind::LabeledStatement { label, statement } => {
let symbol = Symbol {
name: label.clone(),
qualified_name: label.clone(),
kind: SymbolKind::Label,
location: node.location,
scope_id: self.table.current_scope(),
declaration: None,
documentation: None,
attributes: vec![],
};
self.table.add_symbol(symbol);
{
self.visit_node(statement);
}
}
NodeKind::String { value, interpolated } => {
if *interpolated {
self.extract_vars_from_string(value, node.location);
}
}
NodeKind::Use { module, args, .. } => {
self.update_framework_context(module, args);
if module == "Const::Fast" {
self.const_fast_enabled = true;
}
if module == "Readonly" {
self.readonly_enabled = true;
}
if module == "EV" {
self.synthesize_ev_framework_symbol(node.location);
}
if module == "constant" {
self.synthesize_use_constant_symbols(args, node.location);
}
}
NodeKind::No { module: _, args: _, .. } => {
}
NodeKind::PhaseBlock { phase, phase_span: _, block } => {
let symbol = Symbol {
name: phase.clone(),
qualified_name: format!("{}::{}", self.table.current_package, phase),
kind: SymbolKind::Subroutine,
location: node.location,
scope_id: self.table.current_scope(),
declaration: None,
documentation: None,
attributes: vec![],
};
self.table.add_symbol(symbol);
self.table.push_scope(ScopeKind::Block, node.location);
self.visit_node(block);
self.table.pop_scope();
}
NodeKind::StatementModifier { statement, modifier: _, condition } => {
self.visit_node(statement);
self.visit_node(condition);
}
NodeKind::Do { block } | NodeKind::Eval { block } | NodeKind::Defer { block } => {
self.visit_node(block);
}
NodeKind::Try { body, catch_blocks, finally_block } => {
self.visit_node(body);
for (catch_var, catch_block) in catch_blocks {
self.table.push_scope(ScopeKind::Block, catch_block.location);
if let Some(full_name) = catch_var.as_deref() {
self.register_catch_variable(full_name, catch_block.location);
}
self.visit_node(catch_block);
self.table.pop_scope();
}
if let Some(finally) = finally_block {
self.visit_node(finally);
}
}
NodeKind::Given { expr, body } => {
self.visit_node(expr);
self.visit_node(body);
}
NodeKind::When { condition, body } => {
self.visit_node(condition);
self.visit_node(body);
}
NodeKind::Default { body } => {
self.visit_node(body);
}
NodeKind::Class { name, parents, body } => {
let documentation = self.extract_leading_comment(node.location.start);
if Self::is_catalyst_controller_package_name(name)
|| parents.iter().any(|parent| parent == "Catalyst::Controller")
{
self.mark_catalyst_controller_package(name);
}
let symbol = Symbol {
name: name.clone(),
qualified_name: name.clone(),
kind: SymbolKind::Package, location: node.location,
scope_id: self.table.current_scope(),
declaration: None,
documentation,
attributes: vec![],
};
self.table.add_symbol(symbol);
self.table.push_scope(ScopeKind::Package, node.location);
self.visit_node(body);
self.table.pop_scope();
}
NodeKind::Method { name, signature, attributes, body } => {
let documentation = self.extract_leading_comment(node.location.start);
let mut symbol_attributes = Vec::with_capacity(attributes.len() + 1);
symbol_attributes.push("method".to_string());
symbol_attributes.extend(attributes.iter().cloned());
let symbol = Symbol {
name: name.clone(),
qualified_name: format!("{}::{}", self.table.current_package, name),
kind: SymbolKind::Method,
location: node.location,
scope_id: self.table.current_scope(),
declaration: None,
documentation,
attributes: symbol_attributes,
};
self.table.add_symbol(symbol);
self.table.push_scope(ScopeKind::Subroutine, node.location);
if let Some(sig) = signature {
self.register_signature_params(sig);
}
self.visit_node(body);
self.table.pop_scope();
}
NodeKind::Format { name, body: _ } => {
let symbol = Symbol {
name: name.clone(),
qualified_name: format!("{}::{}", self.table.current_package, name),
kind: SymbolKind::Format,
location: node.location,
scope_id: self.table.current_scope(),
declaration: None,
documentation: None,
attributes: vec![],
};
self.table.add_symbol(symbol);
}
NodeKind::Return { value } => {
if let Some(val) = value {
self.visit_node(val);
}
}
NodeKind::Tie { variable, package, args } => {
self.visit_node(variable);
self.visit_node(package);
for arg in args {
self.visit_node(arg);
}
}
NodeKind::Untie { variable } => {
self.visit_node(variable);
}
NodeKind::Goto { target } => match &target.kind {
NodeKind::Identifier { name } => {
self.table.add_reference(SymbolReference {
name: name.clone(),
kind: SymbolKind::Label,
location: target.location,
scope_id: self.table.current_scope(),
is_write: false,
});
}
NodeKind::Variable { sigil, name } if sigil == "&" => {
self.table.add_reference(SymbolReference {
name: name.clone(),
kind: SymbolKind::Subroutine,
location: target.location,
scope_id: self.table.current_scope(),
is_write: false,
});
}
_ => self.visit_node(target),
},
NodeKind::Regex { .. } => {}
NodeKind::Match { expr, .. } => {
self.visit_node(expr);
}
NodeKind::Substitution { expr, .. } => {
self.visit_node(expr);
}
NodeKind::Transliteration { expr, .. } => {
self.visit_node(expr);
}
NodeKind::IndirectCall { method, object, args } => {
self.table.add_reference(SymbolReference {
name: method.clone(),
kind: SymbolKind::Subroutine,
location: node.location,
scope_id: self.table.current_scope(),
is_write: false,
});
self.visit_node(object);
for arg in args {
self.visit_node(arg);
}
}
NodeKind::ExpressionStatement { expression } => {
self.visit_node(expression);
}
NodeKind::Number { .. }
| NodeKind::Heredoc { .. }
| NodeKind::Undef
| NodeKind::Diamond
| NodeKind::Ellipsis
| NodeKind::Glob { .. }
| NodeKind::Readline { .. }
| NodeKind::Identifier { .. }
| NodeKind::Typeglob { .. }
| NodeKind::DataSection { .. }
| NodeKind::LoopControl { .. }
| NodeKind::MissingExpression
| NodeKind::MissingStatement
| NodeKind::MissingIdentifier
| NodeKind::MissingBlock
| NodeKind::UnknownRest => {
}
NodeKind::Error { partial, .. } => {
if let Some(partial_node) = partial {
self.visit_node(partial_node);
}
}
_ => {
tracing::warn!(kind = ?node.kind, "Unhandled node type in symbol extractor");
}
}
}
fn visit_statement_list(&mut self, statements: &[Node]) {
let mut idx = 0;
while idx < statements.len() {
if let Some(consumed) = self.try_extract_framework_declarations(statements, idx) {
idx += consumed;
continue;
}
self.visit_node(&statements[idx]);
idx += 1;
}
}
fn try_extract_framework_declarations(
&mut self,
statements: &[Node],
idx: usize,
) -> Option<usize> {
let flags = self.framework_flags.get(&self.table.current_package).cloned();
let flags = flags.as_ref();
let is_moo = flags.is_some_and(|f| f.moo);
if is_moo {
if let Some(consumed) = self.try_extract_moo_has_declaration(statements, idx) {
return Some(consumed);
}
if let Some(consumed) = self.try_extract_method_modifier(statements, idx) {
return Some(consumed);
}
if let Some(consumed) = self.try_extract_extends_with(statements, idx) {
return Some(consumed);
}
if let Some(consumed) = self.try_extract_role_requires(statements, idx) {
return Some(consumed);
}
}
if flags.is_some_and(|f| f.class_accessor)
&& self.try_extract_class_accessor_declaration(&statements[idx])
{
self.visit_node(&statements[idx]);
return Some(1);
}
if flags.is_some_and(|f| f.web_framework.is_some()) {
if let Some(consumed) = self.try_extract_web_route_declaration(statements, idx) {
return Some(consumed);
}
}
None
}
fn try_extract_moo_has_declaration(
&mut self,
statements: &[Node],
idx: usize,
) -> Option<usize> {
let first = &statements[idx];
if idx + 1 < statements.len() {
let second = &statements[idx + 1];
let is_has_marker = matches!(
&first.kind,
NodeKind::ExpressionStatement { expression }
if matches!(&expression.kind, NodeKind::Identifier { name } if name == "has")
);
if is_has_marker {
if let NodeKind::ExpressionStatement { expression } = &second.kind {
let has_location =
SourceLocation { start: first.location.start, end: second.location.end };
match &expression.kind {
NodeKind::HashLiteral { pairs } => {
self.synthesize_moo_has_pairs(pairs, has_location, false);
self.visit_node(second);
return Some(2);
}
NodeKind::ArrayLiteral { elements } => {
if let Some(Node { kind: NodeKind::HashLiteral { pairs }, .. }) =
elements.last()
{
let mut names = Vec::new();
for el in elements.iter().take(elements.len() - 1) {
names.extend(Self::collect_symbol_names(el));
}
if !names.is_empty() {
self.synthesize_moo_has_attrs_with_options(
&names,
pairs,
has_location,
);
self.visit_node(second);
return Some(2);
}
}
}
_ => {}
}
}
}
}
if let NodeKind::ExpressionStatement { expression } = &first.kind
&& let NodeKind::HashLiteral { pairs } = &expression.kind
{
let has_embedded_marker = pairs.iter().any(|(key_node, _)| {
matches!(
&key_node.kind,
NodeKind::Binary { op, left, .. }
if op == "[]" && matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
)
});
if has_embedded_marker {
self.synthesize_moo_has_pairs(pairs, first.location, true);
self.visit_node(first);
return Some(1);
}
}
if let NodeKind::ExpressionStatement { expression } = &first.kind
&& let NodeKind::FunctionCall { name, args } = &expression.kind
&& name == "has"
&& !args.is_empty()
{
let options_hash_idx =
args.iter().rposition(|a| matches!(a.kind, NodeKind::HashLiteral { .. }));
if let Some(opts_idx) = options_hash_idx {
if let NodeKind::HashLiteral { pairs } = &args[opts_idx].kind {
let names: Vec<String> =
args[..opts_idx].iter().flat_map(Self::collect_symbol_names).collect();
if !names.is_empty() {
self.synthesize_moo_has_attrs_with_options(&names, pairs, first.location);
self.visit_node(first);
return Some(1);
}
}
}
}
None
}
fn try_extract_method_modifier(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
let first = &statements[idx];
if let NodeKind::ExpressionStatement { expression } = &first.kind
&& let NodeKind::FunctionCall { name, args } = &expression.kind
&& Self::is_moose_method_modifier(name)
{
let modifier_name = name.as_str();
let method_names: Vec<String> =
args.iter().flat_map(Self::collect_symbol_names).collect();
if !method_names.is_empty() {
let scope_id = self.table.current_scope();
let package = self.table.current_package.clone();
for method_name in method_names {
self.table.add_symbol(Symbol {
name: method_name.clone(),
qualified_name: format!("{package}::{method_name}"),
kind: SymbolKind::Subroutine,
location: first.location,
scope_id,
declaration: Some(modifier_name.to_string()),
documentation: Some(format!(
"Method modifier `{modifier_name}` for `{method_name}`"
)),
attributes: vec![format!("modifier={modifier_name}")],
});
}
return Some(1);
}
}
if idx + 1 >= statements.len() {
return None;
}
let second = &statements[idx + 1];
let modifier_name = match &first.kind {
NodeKind::ExpressionStatement { expression } => match &expression.kind {
NodeKind::Identifier { name } if Self::is_moose_method_modifier(name) => {
name.as_str()
}
_ => return None,
},
_ => return None,
};
let NodeKind::ExpressionStatement { expression } = &second.kind else {
return None;
};
let NodeKind::HashLiteral { pairs } = &expression.kind else {
return None;
};
let modifier_location =
SourceLocation { start: first.location.start, end: second.location.end };
let scope_id = self.table.current_scope();
let package = self.table.current_package.clone();
for (key_node, _value_node) in pairs {
let method_names = Self::collect_symbol_names(key_node);
for method_name in method_names {
self.table.add_symbol(Symbol {
name: method_name.clone(),
qualified_name: format!("{package}::{method_name}"),
kind: SymbolKind::Subroutine,
location: modifier_location,
scope_id,
declaration: Some(modifier_name.to_string()),
documentation: Some(format!(
"Method modifier `{modifier_name}` for `{method_name}`"
)),
attributes: vec![format!("modifier={modifier_name}")],
});
}
}
self.visit_node(second);
Some(2)
}
fn is_moose_method_modifier(name: &str) -> bool {
matches!(name, "before" | "after" | "around" | "override" | "augment")
}
fn try_extract_extends_with(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
let first = &statements[idx];
if let NodeKind::ExpressionStatement { expression } = &first.kind
&& let NodeKind::FunctionCall { name, args } = &expression.kind
&& matches!(name.as_str(), "extends" | "with")
{
let keyword = name.as_str();
let names: Vec<String> = args.iter().flat_map(Self::collect_symbol_names).collect();
if !names.is_empty() {
if names.iter().any(|name| name == "Catalyst::Controller") {
let package = self.table.current_package.clone();
self.mark_catalyst_controller_package(&package);
}
let ref_kind =
if keyword == "extends" { SymbolKind::Class } else { SymbolKind::Role };
for ref_name in names {
self.table.add_reference(SymbolReference {
name: ref_name,
kind: ref_kind,
location: first.location,
scope_id: self.table.current_scope(),
is_write: false,
});
}
return Some(1);
}
}
if idx + 1 >= statements.len() {
return None;
}
let second = &statements[idx + 1];
let keyword = match &first.kind {
NodeKind::ExpressionStatement { expression } => match &expression.kind {
NodeKind::Identifier { name } if matches!(name.as_str(), "extends" | "with") => {
name.as_str()
}
_ => return None,
},
_ => return None,
};
let NodeKind::ExpressionStatement { expression } = &second.kind else {
return None;
};
let names = Self::collect_symbol_names(expression);
if names.is_empty() {
return None;
}
if names.iter().any(|name| name == "Catalyst::Controller") {
let package = self.table.current_package.clone();
self.mark_catalyst_controller_package(&package);
}
let ref_location = SourceLocation { start: first.location.start, end: second.location.end };
let ref_kind = if keyword == "extends" { SymbolKind::Class } else { SymbolKind::Role };
for name in names {
self.table.add_reference(SymbolReference {
name,
kind: ref_kind,
location: ref_location,
scope_id: self.table.current_scope(),
is_write: false,
});
}
Some(2)
}
fn try_extract_role_requires(&mut self, statements: &[Node], idx: usize) -> Option<usize> {
let first = &statements[idx];
if let NodeKind::ExpressionStatement { expression } = &first.kind
&& let NodeKind::FunctionCall { name, args } = &expression.kind
&& name == "requires"
{
let names: Vec<String> = args.iter().flat_map(Self::collect_symbol_names).collect();
if !names.is_empty() {
let scope_id = self.table.current_scope();
let package = self.table.current_package.clone();
for method_name in names {
self.table.add_symbol(Symbol {
name: method_name.clone(),
qualified_name: format!("{package}::{method_name}"),
kind: SymbolKind::Subroutine,
location: first.location,
scope_id,
declaration: Some("requires".to_string()),
documentation: Some(format!("Required method `{method_name}` from role")),
attributes: vec!["requires=true".to_string()],
});
}
return Some(1);
}
}
if idx + 1 >= statements.len() {
return None;
}
let second = &statements[idx + 1];
let is_requires = match &first.kind {
NodeKind::ExpressionStatement { expression } => {
matches!(&expression.kind, NodeKind::Identifier { name } if name == "requires")
}
_ => false,
};
if !is_requires {
return None;
}
let NodeKind::ExpressionStatement { expression } = &second.kind else {
return None;
};
let names = Self::collect_symbol_names(expression);
if names.is_empty() {
return None;
}
let location = SourceLocation { start: first.location.start, end: second.location.end };
let scope_id = self.table.current_scope();
let package = self.table.current_package.clone();
for name in names {
self.table.add_symbol(Symbol {
name: name.clone(),
qualified_name: format!("{package}::{name}"),
kind: SymbolKind::Subroutine,
location,
scope_id,
declaration: Some("requires".to_string()),
documentation: Some(format!("Required method `{name}` from role")),
attributes: vec!["requires=true".to_string()],
});
}
Some(2)
}
fn synthesize_moo_has_pairs(
&mut self,
pairs: &[(Node, Node)],
has_location: SourceLocation,
require_embedded_marker: bool,
) {
for (attr_expr, options_expr) in pairs {
let Some(attr_expr) = Self::moo_attribute_expr(attr_expr, require_embedded_marker)
else {
continue;
};
let attribute_names = Self::collect_symbol_names(attr_expr);
if attribute_names.is_empty() {
continue;
}
if let NodeKind::HashLiteral { pairs: option_pairs } = &options_expr.kind {
self.synthesize_moo_has_attrs_with_options(
&attribute_names,
option_pairs,
has_location,
);
}
}
}
fn synthesize_moo_has_attrs_with_options(
&mut self,
attribute_names: &[String],
option_pairs: &[(Node, Node)],
has_location: SourceLocation,
) {
let scope_id = self.table.current_scope();
let package = self.table.current_package.clone();
let options_expr = Node {
kind: NodeKind::HashLiteral { pairs: option_pairs.to_vec() },
location: has_location,
};
let option_map = Self::extract_hash_options(&options_expr);
let metadata = Self::attribute_metadata(&option_map);
let generated_methods =
Self::moo_accessor_names(attribute_names, &option_map, &options_expr);
for attribute_name in attribute_names {
self.table.add_symbol(Symbol {
name: attribute_name.clone(),
qualified_name: format!("{package}::{attribute_name}"),
kind: SymbolKind::scalar(),
location: has_location,
scope_id,
declaration: Some("has".to_string()),
documentation: Some(format!("Moo/Moose attribute `{attribute_name}`")),
attributes: metadata.clone(),
});
}
let accessor_doc = Self::moo_accessor_doc(&option_map);
for method_name in generated_methods {
self.table.add_symbol(Symbol {
name: method_name.clone(),
qualified_name: format!("{package}::{method_name}"),
kind: SymbolKind::Subroutine,
location: has_location,
scope_id,
declaration: Some("has".to_string()),
documentation: Some(accessor_doc.clone()),
attributes: metadata.clone(),
});
}
}
fn moo_attribute_expr(attr_expr: &Node, require_embedded_marker: bool) -> Option<&Node> {
if let NodeKind::Binary { op, left, right } = &attr_expr.kind
&& op == "[]"
&& matches!(&left.kind, NodeKind::Identifier { name } if name == "has")
{
return Some(right.as_ref());
}
if require_embedded_marker { None } else { Some(attr_expr) }
}
fn try_extract_web_route_declaration(
&mut self,
statements: &[Node],
idx: usize,
) -> Option<usize> {
let web_framework = self
.framework_flags
.get(&self.table.current_package)
.and_then(|flags| flags.web_framework);
let first = &statements[idx];
if let NodeKind::ExpressionStatement { expression } = &first.kind
&& let NodeKind::FunctionCall { name, args } = &expression.kind
&& matches!(name.as_str(), "get" | "post" | "put" | "del" | "delete" | "patch" | "any")
{
let method_name = name.as_str();
if let Some(path_node) = args.first() {
if let NodeKind::String { value, .. } = &path_node.kind {
if let Some(path) = Self::normalize_symbol_name(value) {
let http_method = match method_name {
"get" => "GET",
"post" => "POST",
"put" => "PUT",
"del" | "delete" => "DELETE",
"patch" => "PATCH",
"any" => "ANY",
_ => method_name,
};
let scope_id = self.table.current_scope();
self.table.add_symbol(Symbol {
name: path.clone(),
qualified_name: path.clone(),
kind: SymbolKind::Subroutine,
location: first.location,
scope_id,
declaration: Some(method_name.to_string()),
documentation: Some(format!("{http_method} {path}")),
attributes: vec![format!("http_method={http_method}")],
});
if matches!(
web_framework,
Some(WebFrameworkKind::Dancer | WebFrameworkKind::Dancer2)
) && let Some(target_node) = args.get(1)
{
if let Some(target_name) =
Self::collect_symbol_names(target_node).first().cloned()
{
self.table.add_reference(SymbolReference {
name: target_name,
kind: SymbolKind::Subroutine,
location: target_node.location,
scope_id: self.table.current_scope(),
is_write: false,
});
}
}
self.visit_node(first);
return Some(1);
}
}
}
}
if idx + 1 >= statements.len() {
return None;
}
let second = &statements[idx + 1];
let method_name = match &first.kind {
NodeKind::ExpressionStatement { expression } => match &expression.kind {
NodeKind::Identifier { name }
if matches!(
name.as_str(),
"get" | "post" | "put" | "del" | "delete" | "patch" | "any"
) =>
{
name.as_str()
}
_ => return None,
},
_ => return None,
};
let NodeKind::ExpressionStatement { expression } = &second.kind else {
return None;
};
let NodeKind::HashLiteral { pairs } = &expression.kind else {
return None;
};
let (path_node, _handler_node) = pairs.first()?;
let path = match &path_node.kind {
NodeKind::String { value, .. } => Self::normalize_symbol_name(value)?,
_ => return None,
};
let http_method = match method_name {
"get" => "GET",
"post" => "POST",
"put" => "PUT",
"del" | "delete" => "DELETE",
"patch" => "PATCH",
"any" => "ANY",
_ => method_name,
};
let route_location =
SourceLocation { start: first.location.start, end: second.location.end };
let scope_id = self.table.current_scope();
self.table.add_symbol(Symbol {
name: path.clone(),
qualified_name: path.clone(),
kind: SymbolKind::Subroutine,
location: route_location,
scope_id,
declaration: Some(method_name.to_string()),
documentation: Some(format!("{http_method} {path}")),
attributes: vec![format!("http_method={http_method}")],
});
self.visit_node(second);
Some(2)
}
fn synthesize_plack_builder_symbols(&mut self, name: &str, args: &[Node]) {
let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
return;
};
if flags.web_framework != Some(WebFrameworkKind::PlackBuilder) || name != "builder" {
return;
}
let Some(block) = args.first() else {
return;
};
let NodeKind::Block { statements } = &block.kind else {
return;
};
let scope_id = self.table.current_scope();
let package = self.table.current_package.clone();
for statement in statements {
let NodeKind::ExpressionStatement { expression } = &statement.kind else {
continue;
};
let NodeKind::FunctionCall { name: stmt_name, args: stmt_args } = &expression.kind
else {
continue;
};
match stmt_name.as_str() {
"enable" => {
self.synthesize_plack_enable_symbol(statement, stmt_args, scope_id, &package);
}
"mount" => {
self.synthesize_plack_mount_symbol(statement, stmt_args, scope_id, &package);
}
_ => {}
}
}
}
fn synthesize_plack_enable_symbol(
&mut self,
statement: &Node,
args: &[Node],
scope_id: ScopeId,
_package: &str,
) {
let Some(first) = args.first() else {
return;
};
let Some(raw_name) = Self::single_symbol_name(first) else {
return;
};
let middleware_name = if raw_name.contains("::") {
raw_name
} else {
format!("Plack::Middleware::{raw_name}")
};
if middleware_name.is_empty() {
return;
}
if self.table.symbols.get(&middleware_name).is_some_and(|symbols| {
symbols.iter().any(|symbol| {
symbol.kind == SymbolKind::Package
&& symbol.declaration.as_deref() == Some("enable")
&& symbol
.attributes
.iter()
.any(|attr| attr == &format!("middleware={middleware_name}"))
})
}) {
return;
}
self.table.add_symbol(Symbol {
name: middleware_name.clone(),
qualified_name: middleware_name.clone(),
kind: SymbolKind::Package,
location: statement.location,
scope_id,
declaration: Some("enable".to_string()),
documentation: Some(format!("PSGI middleware {middleware_name}")),
attributes: vec![
"framework=Plack::Builder".to_string(),
format!("middleware={middleware_name}"),
],
});
}
fn synthesize_plack_mount_symbol(
&mut self,
statement: &Node,
args: &[Node],
scope_id: ScopeId,
_package: &str,
) {
let Some(path_node) = args.first() else {
return;
};
let Some(path) = Self::single_symbol_name(path_node) else {
return;
};
if path.is_empty() {
return;
}
let target = args
.get(1)
.map(Self::value_summary)
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "$app".to_string());
if self.table.symbols.get(&path).is_some_and(|symbols| {
symbols.iter().any(|symbol| {
symbol.kind == SymbolKind::Subroutine
&& symbol.declaration.as_deref() == Some("mount")
&& symbol.attributes.iter().any(|attr| attr == &format!("mount_path={path}"))
})
}) {
return;
}
self.table.add_symbol(Symbol {
name: path.clone(),
qualified_name: path.clone(),
kind: SymbolKind::Subroutine,
location: statement.location,
scope_id,
declaration: Some("mount".to_string()),
documentation: Some(format!("PSGI mount {path} -> {target}")),
attributes: vec![
"framework=Plack::Builder".to_string(),
format!("mount_path={path}"),
format!("mount_target={target}"),
],
});
}
fn try_extract_class_accessor_declaration(&mut self, statement: &Node) -> bool {
let NodeKind::ExpressionStatement { expression } = &statement.kind else {
return false;
};
let NodeKind::MethodCall { method, args, .. } = &expression.kind else {
return false;
};
let is_accessor_generator = matches!(
method.as_str(),
"mk_accessors" | "mk_ro_accessors" | "mk_rw_accessors" | "mk_wo_accessors"
);
if !is_accessor_generator {
return false;
}
let mut accessor_names = Vec::new();
for arg in args {
accessor_names.extend(Self::collect_symbol_names(arg));
}
if accessor_names.is_empty() {
return false;
}
let mut seen = HashSet::new();
let scope_id = self.table.current_scope();
let package = self.table.current_package.clone();
for accessor_name in accessor_names {
if !seen.insert(accessor_name.clone()) {
continue;
}
self.table.add_symbol(Symbol {
name: accessor_name.clone(),
qualified_name: format!("{package}::{accessor_name}"),
kind: SymbolKind::Subroutine,
location: statement.location,
scope_id,
declaration: Some(method.clone()),
documentation: Some("Generated accessor (Class::Accessor)".to_string()),
attributes: vec!["framework=Class::Accessor".to_string()],
});
}
true
}
fn synthesize_async_framework_class_symbol(&mut self, object: &Node) -> bool {
let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
return false;
};
let (module_name, framework_name, exact_match) = match flags.async_framework {
Some(AsyncFrameworkKind::AnyEvent) => ("AnyEvent", "AnyEvent", false),
Some(AsyncFrameworkKind::EV) => ("EV", "EV", true),
Some(AsyncFrameworkKind::Future) => ("Future", "Future", true),
Some(AsyncFrameworkKind::FutureXS) => ("Future::XS", "Future::XS", true),
Some(AsyncFrameworkKind::Promise) => ("Promise", "Promise", true),
Some(AsyncFrameworkKind::PromiseXS) => ("Promise::XS", "Promise::XS", true),
Some(AsyncFrameworkKind::POE) => ("POE", "POE", false),
Some(AsyncFrameworkKind::IOAsync) => ("IO::Async", "IO::Async", false),
Some(AsyncFrameworkKind::MojoRedis) => ("Mojo::Redis", "Mojo::Redis", true),
Some(AsyncFrameworkKind::MojoPg) => ("Mojo::Pg", "Mojo::Pg", true),
None => return false,
};
let Some(name) = Self::single_symbol_name(object) else {
return false;
};
if flags.async_framework == Some(AsyncFrameworkKind::AnyEvent) {
if !matches!(
name.as_str(),
"AnyEvent" | "AnyEvent::CondVar" | "AnyEvent::Timer" | "AnyEvent::IO"
) {
return false;
}
} else if exact_match {
if name != module_name {
return false;
}
} else if !name.starts_with(&format!("{module_name}::")) {
return false;
}
let already_synthesized = self.table.symbols.get(&name).is_some_and(|symbols| {
symbols.iter().any(|symbol| {
symbol.kind == SymbolKind::Class
&& symbol.declaration.as_deref() == Some(&format!("framework={framework_name}"))
})
});
if already_synthesized {
return true;
}
let framework_attr = format!("framework={framework_name}");
self.table.add_symbol(Symbol {
name: name.clone(),
qualified_name: name.clone(),
kind: SymbolKind::Class,
location: object.location,
scope_id: self.table.current_scope(),
declaration: Some(framework_attr.clone()),
documentation: Some(format!("Synthetic {framework_name} class")),
attributes: vec![framework_attr],
});
true
}
fn synthesize_ev_framework_symbol(&mut self, location: SourceLocation) {
let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
return;
};
if flags.async_framework != Some(AsyncFrameworkKind::EV) {
return;
}
let name = "EV";
if self.table.symbols.get(name).is_some_and(|symbols| {
symbols.iter().any(|symbol| {
symbol.kind == SymbolKind::Class
&& symbol.declaration.as_deref() == Some("framework=EV")
})
}) {
return;
}
self.table.add_symbol(Symbol {
name: name.to_string(),
qualified_name: name.to_string(),
kind: SymbolKind::Class,
location,
scope_id: self.table.current_scope(),
declaration: Some("framework=EV".to_string()),
documentation: Some("Synthetic EV namespace".to_string()),
attributes: vec!["framework=EV".to_string()],
});
}
fn synthesize_ev_symbols(&mut self, name: &str, location: SourceLocation) -> bool {
let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
return false;
};
if flags.async_framework != Some(AsyncFrameworkKind::EV) {
return false;
}
let Some(ev_suffix) = name.strip_prefix("EV::") else {
return false;
};
if !matches!(ev_suffix, "timer" | "io" | "signal" | "idle") {
return false;
}
let already_synthesized = self.table.symbols.get(name).is_some_and(|symbols| {
symbols.iter().any(|symbol| {
symbol.kind == SymbolKind::Subroutine
&& symbol.declaration.as_deref() == Some("framework=EV")
})
});
if already_synthesized {
return true;
}
self.table.add_symbol(Symbol {
name: name.to_string(),
qualified_name: name.to_string(),
kind: SymbolKind::Subroutine,
location,
scope_id: self.table.current_scope(),
declaration: Some("framework=EV".to_string()),
documentation: Some(format!("Synthetic EV API `{ev_suffix}`")),
attributes: vec!["framework=EV".to_string(), format!("ev_api={ev_suffix}")],
});
true
}
fn synthesize_future_api_symbols(
&mut self,
object: &Node,
method: &str,
location: SourceLocation,
) -> bool {
let Some(flags) = self.framework_flags.get(&self.table.current_package) else {
return false;
};
let (framework_name, root_name, chain_methods, class_entrypoints) =
match flags.async_framework {
Some(AsyncFrameworkKind::Future) => (
"Future",
"Future",
vec!["then", "catch", "finally", "get", "is_done", "is_ready"],
vec!["new", "done", "fail", "wait_all", "needs_all", "needs_any"],
),
Some(AsyncFrameworkKind::FutureXS) => (
"Future::XS",
"Future::XS",
vec!["then", "catch", "finally", "get", "is_done", "is_ready"],
vec!["new", "done", "fail", "wait_all", "needs_all", "needs_any"],
),
Some(AsyncFrameworkKind::Promise) => (
"Promise",
"Promise",
vec!["then", "catch", "finally", "resolve", "reject"],
vec!["new", "all", "race", "any"],
),
Some(AsyncFrameworkKind::PromiseXS) => (
"Promise::XS",
"Promise::XS",
vec!["then", "catch", "finally", "resolve", "reject"],
vec!["new", "all", "race", "any"],
),
_ => return false,
};
let object_name = Self::single_symbol_name(object);
let should_synthesize = if chain_methods.contains(&method) {
true
} else if class_entrypoints.contains(&method) {
object_name.is_some_and(|name| name == root_name)
} else {
false
};
if !should_synthesize {
return false;
}
let already_synthesized = self.table.symbols.get(method).is_some_and(|symbols| {
symbols.iter().any(|symbol| {
symbol.kind == SymbolKind::Subroutine
&& symbol.declaration.as_deref() == Some(&format!("framework={framework_name}"))
&& symbol.attributes.iter().any(|attr| attr == &format!("future_api={method}"))
})
});
if already_synthesized {
return true;
}
self.table.add_symbol(Symbol {
name: method.to_string(),
qualified_name: format!("{framework_name}::{method}"),
kind: SymbolKind::Subroutine,
location,
scope_id: self.table.current_scope(),
declaration: Some(format!("framework={framework_name}")),
documentation: Some(format!("Synthetic {framework_name} API `{method}`")),
attributes: vec![format!("framework={framework_name}"), format!("future_api={method}")],
});
true
}
fn update_framework_context(&mut self, module: &str, args: &[String]) {
let pkg = self.table.current_package.clone();
let framework_kind = match module {
"Moo" | "Mouse" => Some(FrameworkKind::Moo),
"Moo::Role" | "Mouse::Role" => Some(FrameworkKind::MooRole),
"Moose" => Some(FrameworkKind::Moose),
"Moose::Role" => Some(FrameworkKind::MooseRole),
"Role::Tiny" => Some(FrameworkKind::RoleTiny),
"Role::Tiny::With" => Some(FrameworkKind::RoleTinyWith),
_ => None,
};
if let Some(kind) = framework_kind {
let flags = self.framework_flags.entry(pkg.clone()).or_default();
flags.moo = true;
flags.kind = Some(kind);
return;
}
if module == "Class::Accessor" {
self.framework_flags.entry(pkg.clone()).or_default().class_accessor = true;
return;
}
let web_kind = match module {
"Dancer" => Some(WebFrameworkKind::Dancer),
"Dancer2" | "Dancer2::Core" => Some(WebFrameworkKind::Dancer2),
"Mojolicious::Lite" => Some(WebFrameworkKind::MojoliciousLite),
"Plack::Builder" => Some(WebFrameworkKind::PlackBuilder),
_ => None,
};
if let Some(kind) = web_kind {
self.framework_flags.entry(pkg.clone()).or_default().web_framework = Some(kind);
return;
}
if module == "IO::Async" || module.starts_with("IO::Async::") {
self.framework_flags.entry(pkg.clone()).or_default().async_framework =
Some(AsyncFrameworkKind::IOAsync);
return;
}
if module == "AnyEvent" {
self.framework_flags.entry(pkg.clone()).or_default().async_framework =
Some(AsyncFrameworkKind::AnyEvent);
return;
}
if module == "EV" {
self.framework_flags.entry(pkg.clone()).or_default().async_framework =
Some(AsyncFrameworkKind::EV);
return;
}
if module == "Future" {
self.framework_flags.entry(pkg.clone()).or_default().async_framework =
Some(AsyncFrameworkKind::Future);
return;
}
if module == "Future::XS" {
self.framework_flags.entry(pkg.clone()).or_default().async_framework =
Some(AsyncFrameworkKind::FutureXS);
return;
}
if module == "Promise" {
self.framework_flags.entry(pkg.clone()).or_default().async_framework =
Some(AsyncFrameworkKind::Promise);
return;
}
if module == "Promise::XS" {
self.framework_flags.entry(pkg.clone()).or_default().async_framework =
Some(AsyncFrameworkKind::PromiseXS);
return;
}
if module == "POE" || module.starts_with("POE::") {
self.framework_flags.entry(pkg.clone()).or_default().async_framework =
Some(AsyncFrameworkKind::POE);
return;
}
if module == "Mojo::Redis" {
self.framework_flags.entry(pkg.clone()).or_default().async_framework =
Some(AsyncFrameworkKind::MojoRedis);
return;
}
if module == "Mojo::Pg" {
self.framework_flags.entry(pkg.clone()).or_default().async_framework =
Some(AsyncFrameworkKind::MojoPg);
return;
}
if matches!(module, "base" | "parent") {
let has_class_accessor_parent = args
.iter()
.filter_map(|arg| Self::normalize_symbol_name(arg))
.any(|arg| arg == "Class::Accessor");
if has_class_accessor_parent {
self.framework_flags.entry(pkg.clone()).or_default().class_accessor = true;
}
let has_catalyst_controller_parent = args
.iter()
.filter_map(|arg| Self::normalize_symbol_name(arg))
.any(|arg| arg == "Catalyst::Controller");
if has_catalyst_controller_parent {
self.mark_catalyst_controller_package(&pkg);
}
}
}
fn mark_catalyst_controller_package(&mut self, package: &str) {
self.framework_flags.entry(package.to_string()).or_default().catalyst_controller = true;
}
fn current_package_is_catalyst_controller(&self) -> bool {
self.framework_flags
.get(&self.table.current_package)
.is_some_and(|flags| flags.catalyst_controller)
|| Self::is_catalyst_controller_package_name(&self.table.current_package)
}
fn is_catalyst_controller_package_name(package: &str) -> bool {
package.contains("::Controller::") || package.ends_with("::Controller")
}
fn catalyst_action_metadata(attributes: &[String]) -> Option<(String, Vec<String>)> {
let mut kind = None;
let mut details = Vec::new();
let mut seen = HashSet::new();
for attr in attributes {
let attr_name = Self::attribute_base_name(attr);
if !Self::is_catalyst_action_attribute(&attr_name) {
continue;
}
if kind.is_none()
|| matches!(kind.as_deref(), Some("Args" | "CaptureArgs" | "PathPart"))
{
if matches!(attr_name.as_str(), "Path" | "Local" | "Global" | "Regex" | "Chained") {
kind = Some(attr_name.clone());
} else if kind.is_none() {
kind = Some(attr_name.clone());
}
}
if seen.insert(attr.clone()) {
details.push(attr.clone());
}
}
if let Some(action_kind) = kind.as_deref()
&& matches!(action_kind, "Path" | "Local" | "Global" | "Regex" | "Chained")
{
details.retain(|attr| Self::attribute_base_name(attr) != action_kind);
}
kind.map(|kind| (kind, details))
}
fn is_catalyst_action_attribute(attr_name: &str) -> bool {
matches!(
attr_name,
"Path" | "Local" | "Global" | "Regex" | "Chained" | "PathPart" | "Args" | "CaptureArgs"
)
}
fn attribute_base_name(attr: &str) -> String {
attr.trim_start_matches(':')
.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == ':'))
.next()
.unwrap_or("")
.to_string()
}
fn extract_hash_options(node: &Node) -> HashMap<String, String> {
let mut options = HashMap::new();
let NodeKind::HashLiteral { pairs } = &node.kind else {
return options;
};
for (key_node, value_node) in pairs {
let Some(key_name) = Self::single_symbol_name(key_node) else {
continue;
};
let value_text = Self::value_summary(value_node);
options.insert(key_name, value_text);
}
options
}
fn attribute_metadata(option_map: &HashMap<String, String>) -> Vec<String> {
let preferred_order = [
"is",
"isa",
"required",
"lazy",
"builder",
"default",
"reader",
"writer",
"accessor",
"predicate",
"clearer",
"handles",
];
let mut metadata = Vec::new();
for key in preferred_order {
if let Some(value) = option_map.get(key) {
metadata.push(format!("{key}={value}"));
}
}
metadata
}
fn moo_accessor_doc(option_map: &HashMap<String, String>) -> String {
let mut parts = Vec::new();
if let Some(isa) = option_map.get("isa") {
parts.push(format!("isa: {isa}"));
}
if let Some(is) = option_map.get("is") {
parts.push(is.clone());
}
if parts.is_empty() {
"Generated accessor from Moo/Moose `has`".to_string()
} else {
format!("Moo/Moose accessor ({})", parts.join(", "))
}
}
fn moo_accessor_names(
attribute_names: &[String],
option_map: &HashMap<String, String>,
options_expr: &Node,
) -> Vec<String> {
let mut methods = Vec::new();
let mut seen = HashSet::new();
for key in ["accessor", "reader", "writer", "predicate", "clearer", "builder"] {
for name in Self::option_method_names(options_expr, key, attribute_names) {
if seen.insert(name.clone()) {
methods.push(name);
}
}
}
for name in Self::handles_method_names(options_expr) {
if seen.insert(name.clone()) {
methods.push(name);
}
}
let has_explicit_accessor = option_map.contains_key("accessor")
|| option_map.contains_key("reader")
|| option_map.contains_key("writer");
if !has_explicit_accessor {
for attribute_name in attribute_names {
if seen.insert(attribute_name.clone()) {
methods.push(attribute_name.clone());
}
}
}
methods
}
fn find_hash_option_value<'a>(options_expr: &'a Node, key: &str) -> Option<&'a Node> {
let NodeKind::HashLiteral { pairs } = &options_expr.kind else {
return None;
};
for (key_node, value_node) in pairs {
if Self::single_symbol_name(key_node).as_deref() == Some(key) {
return Some(value_node);
}
}
None
}
fn option_method_names(
options_expr: &Node,
key: &str,
attribute_names: &[String],
) -> Vec<String> {
let Some(value_node) = Self::find_hash_option_value(options_expr, key) else {
return Vec::new();
};
let mut names = Self::collect_symbol_names(value_node);
if !names.is_empty() {
names.sort();
names.dedup();
return names;
}
if !Self::is_truthy_shorthand(value_node) {
return Vec::new();
}
match key {
"predicate" => attribute_names.iter().map(|name| format!("has_{name}")).collect(),
"clearer" => attribute_names.iter().map(|name| format!("clear_{name}")).collect(),
"builder" => attribute_names.iter().map(|name| format!("_build_{name}")).collect(),
_ => Vec::new(),
}
}
fn is_truthy_shorthand(node: &Node) -> bool {
match &node.kind {
NodeKind::Number { value } => value.trim() == "1",
NodeKind::Identifier { name } => {
let lower = name.trim().to_ascii_lowercase();
lower == "1" || lower == "true"
}
NodeKind::String { value, .. } => {
Self::normalize_symbol_name(value).is_some_and(|value| {
let lower = value.to_ascii_lowercase();
value == "1" || lower == "true"
})
}
_ => false,
}
}
fn handles_method_names(options_expr: &Node) -> Vec<String> {
let Some(handles_node) = Self::find_hash_option_value(options_expr, "handles") else {
return Vec::new();
};
let mut names = Vec::new();
match &handles_node.kind {
NodeKind::HashLiteral { pairs } => {
for (key_node, _) in pairs {
names.extend(Self::collect_symbol_names(key_node));
}
}
_ => {
names.extend(Self::collect_symbol_names(handles_node));
}
}
names.sort();
names.dedup();
names
}
fn collect_symbol_names(node: &Node) -> Vec<String> {
match &node.kind {
NodeKind::String { value, .. } => {
Self::normalize_symbol_name(value).into_iter().collect()
}
NodeKind::Identifier { name } => {
Self::normalize_symbol_name(name).into_iter().collect()
}
NodeKind::ArrayLiteral { elements } => {
let mut names = Vec::new();
for element in elements {
names.extend(Self::collect_symbol_names(element));
}
names
}
_ => Vec::new(),
}
}
fn single_symbol_name(node: &Node) -> Option<String> {
Self::collect_symbol_names(node).into_iter().next()
}
fn normalize_symbol_name(raw: &str) -> Option<String> {
let trimmed = raw.trim().trim_matches('\'').trim_matches('"').trim();
if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }
}
fn value_summary(node: &Node) -> String {
match &node.kind {
NodeKind::String { value, .. } => {
Self::normalize_symbol_name(value).unwrap_or_else(|| value.clone())
}
NodeKind::Identifier { name } => name.clone(),
NodeKind::Variable { sigil, name } => format!("{sigil}{name}"),
NodeKind::Number { value } => value.clone(),
NodeKind::ArrayLiteral { elements } => {
let mut entries = Vec::new();
for element in elements {
entries.extend(Self::collect_symbol_names(element));
}
entries.sort();
entries.dedup();
if entries.is_empty() {
"array".to_string()
} else {
format!("[{}]", entries.join(","))
}
}
NodeKind::HashLiteral { pairs } => {
let mut entries = Vec::new();
for (key_node, value_node) in pairs {
let Some(key_name) = Self::single_symbol_name(key_node) else {
continue;
};
if let Some(value_name) = Self::single_symbol_name(value_node) {
entries.push(format!("{key_name}->{value_name}"));
} else {
entries.push(key_name);
}
}
entries.sort();
entries.dedup();
if entries.is_empty() {
"hash".to_string()
} else {
format!("{{{}}}", entries.join(","))
}
}
NodeKind::Undef => "undef".to_string(),
_ => "expr".to_string(),
}
}
fn method_reference_location(
&self,
call_node: &Node,
object: &Node,
method_name: &str,
) -> SourceLocation {
if self.source.is_empty() {
return call_node.location;
}
let search_start = object.location.end.min(self.source.len());
let search_end = search_start.saturating_add(160).min(self.source.len());
if search_start >= search_end || !self.source.is_char_boundary(search_start) {
return call_node.location;
}
let window = &self.source[search_start..search_end];
let Some(arrow_idx) = window.find("->") else {
return call_node.location;
};
let mut idx = arrow_idx + 2;
while idx < window.len() {
let b = window.as_bytes()[idx];
if b.is_ascii_whitespace() {
idx += 1;
} else {
break;
}
}
let suffix = &window[idx..];
if suffix.starts_with(method_name) {
let method_start = search_start + idx;
return SourceLocation { start: method_start, end: method_start + method_name.len() };
}
if let Some(rel_idx) = suffix.find(method_name) {
let method_start = search_start + idx + rel_idx;
return SourceLocation { start: method_start, end: method_start + method_name.len() };
}
call_node.location
}
fn extract_leading_comment(&self, start: usize) -> Option<String> {
if self.source.is_empty() || start == 0 {
return None;
}
let mut end = start.min(self.source.len());
let bytes = self.source.as_bytes();
while end > 0 && bytes[end - 1].is_ascii_whitespace() {
end -= 1;
}
while end > 0 && !self.source.is_char_boundary(end) {
end -= 1;
}
let prefix = &self.source[..end];
let mut lines = prefix.lines().rev();
let mut docs = Vec::new();
for line in &mut lines {
let trimmed = line.trim_start();
if trimmed.starts_with('#') {
let content = trimmed.trim_start_matches('#').trim_start();
docs.push(content);
} else {
break;
}
}
if docs.is_empty() {
None
} else {
docs.reverse();
let total_len: usize =
docs.iter().map(|s| s.len()).sum::<usize>() + docs.len().saturating_sub(1);
let mut result = String::with_capacity(total_len);
for (i, doc) in docs.iter().enumerate() {
if i > 0 {
result.push('\n');
}
result.push_str(doc);
}
Some(result)
}
}
fn extract_package_documentation(
&self,
package_name: &str,
location: SourceLocation,
) -> Option<String> {
let leading = self.extract_leading_comment(location.start);
if leading.is_some() {
return leading;
}
if self.source.is_empty() {
return None;
}
let mut in_name_section = false;
let mut name_lines: Vec<&str> = Vec::new();
for line in self.source.lines() {
let trimmed = line.trim();
if trimmed.starts_with("=head1") {
if in_name_section {
break;
}
let heading = trimmed.strip_prefix("=head1").map(|s| s.trim());
if heading == Some("NAME") {
in_name_section = true;
continue;
}
} else if trimmed.starts_with("=cut") && in_name_section {
break;
} else if trimmed.starts_with('=') && in_name_section {
break;
} else if in_name_section && !trimmed.is_empty() {
name_lines.push(trimmed);
}
}
if !name_lines.is_empty() {
let name_doc = name_lines.join(" ");
if name_doc.contains(package_name)
|| name_doc.contains(&package_name.replace("::", "-"))
{
return Some(name_doc);
}
}
None
}
fn register_signature_params(&mut self, sig: &Node) {
let NodeKind::Signature { parameters } = &sig.kind else {
return;
};
for param in parameters {
let variable = match ¶m.kind {
NodeKind::MandatoryParameter { variable } => variable.as_ref(),
NodeKind::OptionalParameter { variable, .. } => variable.as_ref(),
NodeKind::SlurpyParameter { variable } => variable.as_ref(),
NodeKind::NamedParameter { variable } => variable.as_ref(),
_ => continue,
};
self.handle_variable_declaration("my", variable, &[], variable.location, None);
}
}
fn handle_variable_declaration(
&mut self,
declarator: &str,
variable: &Node,
attributes: &[String],
location: SourceLocation,
documentation: Option<String>,
) {
if let NodeKind::Variable { sigil, name } = &variable.kind {
let kind = match sigil.as_str() {
"$" => SymbolKind::scalar(),
"@" => SymbolKind::array(),
"%" => SymbolKind::hash(),
_ => return,
};
let symbol = Symbol {
name: name.clone(),
qualified_name: if declarator == "our" {
format!("{}::{}", self.table.current_package, name)
} else {
name.clone()
},
kind,
location,
scope_id: self.table.current_scope(),
declaration: Some(declarator.to_string()),
documentation,
attributes: attributes.to_vec(),
};
self.table.add_symbol(symbol);
}
}
fn try_extract_const_fast_declaration(&mut self, args: &[Node]) -> bool {
let mut matched = false;
for arg in args {
match &arg.kind {
NodeKind::VariableDeclaration { declarator, variable, .. } => {
if self.add_constant_wrapper_symbol(
variable,
&[],
declarator,
"const",
"Const::Fast read-only variable",
) {
matched = true;
}
}
NodeKind::VariableListDeclaration { declarator, variables, attributes, .. } => {
let mut saw_decl = false;
for variable in variables {
if self.add_constant_wrapper_symbol(
variable,
attributes,
declarator,
"const",
"Const::Fast read-only variable",
) {
saw_decl = true;
}
}
matched |= saw_decl;
}
_ => self.visit_node(arg),
}
}
matched
}
fn try_extract_readonly_declaration(&mut self, args: &[Node]) -> bool {
let mut matched = false;
for arg in args {
match &arg.kind {
NodeKind::VariableDeclaration { declarator, variable, attributes, .. } => {
if self.add_constant_wrapper_symbol(
variable,
attributes,
declarator,
"Readonly",
"Readonly read-only variable",
) {
matched = true;
}
}
NodeKind::VariableListDeclaration { declarator, variables, attributes, .. } => {
let mut saw_decl = false;
for variable in variables {
if self.add_constant_wrapper_symbol(
variable,
attributes,
declarator,
"Readonly",
"Readonly read-only variable",
) {
saw_decl = true;
}
}
matched |= saw_decl;
}
_ => self.visit_node(arg),
}
}
matched
}
fn add_constant_wrapper_symbol(
&mut self,
variable: &Node,
attributes: &[String],
scope_declarator: &str,
declarator: &str,
documentation: &str,
) -> bool {
match &variable.kind {
NodeKind::Variable { name, .. } => {
self.table.add_symbol(Symbol {
name: name.clone(),
qualified_name: if scope_declarator == "our" {
format!("{}::{}", self.table.current_package, name)
} else {
name.clone()
},
kind: SymbolKind::Constant,
location: variable.location,
scope_id: self.table.current_scope(),
declaration: Some(declarator.to_string()),
documentation: Some(documentation.to_string()),
attributes: attributes.to_vec(),
});
true
}
NodeKind::VariableWithAttributes { variable, attributes: inner_attributes } => {
let mut merged = attributes.to_vec();
merged.extend(inner_attributes.iter().cloned());
self.add_constant_wrapper_symbol(
variable,
&merged,
scope_declarator,
declarator,
documentation,
)
}
_ => false,
}
}
fn synthesize_use_constant_symbols(&mut self, args: &[String], location: SourceLocation) {
let constant_names = extract_constant_names_from_use_args(args);
for name in constant_names {
self.table.add_symbol(Symbol {
name: name.clone(),
qualified_name: format!("{}::{}", self.table.current_package, name),
kind: SymbolKind::Constant,
location,
scope_id: self.table.current_scope(),
declaration: Some("constant".to_string()),
documentation: Some("use constant declaration".to_string()),
attributes: vec![],
});
}
}
fn register_catch_variable(&mut self, full_name: &str, catch_block_location: SourceLocation) {
let (sigil, name) = split_variable_name(full_name);
let kind = match sigil {
"$" => SymbolKind::scalar(),
"@" => SymbolKind::array(),
"%" => SymbolKind::hash(),
_ => return,
};
if name.is_empty() || name.contains("::") {
return;
}
let location = self
.find_catch_variable_location(catch_block_location.start, full_name)
.unwrap_or(SourceLocation {
start: catch_block_location.start,
end: catch_block_location.start,
});
self.table.add_symbol(Symbol {
name: name.to_string(),
qualified_name: name.to_string(),
kind,
location,
scope_id: self.table.current_scope(),
declaration: Some("my".to_string()),
documentation: Some("Exception variable bound by catch".to_string()),
attributes: vec![],
});
}
fn find_catch_variable_location(
&self,
catch_body_start: usize,
full_name: &str,
) -> Option<SourceLocation> {
if self.source.is_empty()
|| full_name.is_empty()
|| catch_body_start == 0
|| catch_body_start > self.source.len()
{
return None;
}
let window_start = catch_body_start.saturating_sub(256);
let window = self.source.get(window_start..catch_body_start)?;
let catch_start = window.rfind("catch")?;
let search_start = catch_start + "catch".len();
let var_offset = window[search_start..].rfind(full_name)? + search_start;
let start = window_start + var_offset;
let end = start + full_name.len();
Some(SourceLocation { start, end })
}
fn mark_write_reference(&mut self, node: &Node) {
if let NodeKind::Variable { .. } = &node.kind {
}
}
fn extract_vars_from_string(&mut self, value: &str, string_location: SourceLocation) {
static SCALAR_RE: OnceLock<Result<Regex, regex::Error>> = OnceLock::new();
let scalar_re = match SCALAR_RE
.get_or_init(|| {
Regex::new(
r"\$((?:[a-zA-Z_]\w*(?:::[a-zA-Z_]\w*)*)|\{(?:[a-zA-Z_]\w*(?:::[a-zA-Z_]\w*)*)\})",
)
})
.as_ref()
{
Ok(re) => re,
Err(_) => return, };
let content = if value.len() >= 2 { &value[1..value.len() - 1] } else { value };
for cap in scalar_re.captures_iter(content) {
if let Some(m) = cap.get(0) {
let var_name = if m.as_str().starts_with("${") && m.as_str().ends_with("}") {
&m.as_str()[2..m.as_str().len() - 1]
} else {
&m.as_str()[1..]
};
let start_offset = string_location.start + 1 + m.start(); let end_offset = start_offset + m.len();
let reference = SymbolReference {
name: var_name.to_string(),
kind: SymbolKind::scalar(),
location: SourceLocation { start: start_offset, end: end_offset },
scope_id: self.table.current_scope(),
is_write: false,
};
self.table.add_reference(reference);
}
}
}
}
fn split_variable_name(full_name: &str) -> (&str, &str) {
full_name
.char_indices()
.next()
.map(|(idx, ch)| (&full_name[idx..idx + ch.len_utf8()], &full_name[idx + ch.len_utf8()..]))
.unwrap_or(("", ""))
}
fn extract_constant_names_from_use_args(args: &[String]) -> Vec<String> {
fn push_unique(names: &mut Vec<String>, seen: &mut HashSet<String>, candidate: &str) {
if seen.insert(candidate.to_string()) {
names.push(candidate.to_string());
}
}
fn normalize_constant_name(token: &str) -> Option<&str> {
let stripped = token.trim_matches(|c: char| {
matches!(c, '\'' | '"' | '(' | ')' | '[' | ']' | '{' | '}' | ',' | ';')
});
if stripped.is_empty() || stripped.starts_with('-') {
return None;
}
stripped.chars().all(|c| c.is_alphanumeric() || c == '_').then_some(stripped)
}
let mut names = Vec::new();
let mut seen = HashSet::new();
let Some(first) = args.first().map(String::as_str) else {
return names;
};
if first.starts_with("qw") {
let (qw_words, remainder) = extract_qw_words(first);
if remainder.trim().is_empty() {
for word in qw_words {
if let Some(candidate) = normalize_constant_name(&word) {
push_unique(&mut names, &mut seen, candidate);
}
}
return names;
}
let content = first.trim_start_matches("qw").trim_start();
let content = content
.trim_start_matches(|c: char| "([{/<|!".contains(c))
.trim_end_matches(|c: char| ")]}/|!>".contains(c));
for word in content.split_whitespace() {
if let Some(candidate) = normalize_constant_name(word) {
push_unique(&mut names, &mut seen, candidate);
}
}
return names;
}
let starts_hash_form = first == "{"
|| first == "+{"
|| (first == "+" && args.get(1).map(String::as_str) == Some("{"));
if starts_hash_form {
let mut skipped_leading_plus = false;
let mut iter = args.iter().peekable();
while let Some(arg) = iter.next() {
if arg == "+{" {
skipped_leading_plus = true;
continue;
}
if arg == "+" && !skipped_leading_plus {
skipped_leading_plus = true;
continue;
}
if arg == "{" || arg == "}" || arg == "," || arg == "=>" {
continue;
}
if let Some(candidate) = normalize_constant_name(arg)
&& iter.peek().map(|s| s.as_str()) == Some("=>")
{
push_unique(&mut names, &mut seen, candidate);
}
}
return names;
}
if let Some(candidate) = normalize_constant_name(first) {
push_unique(&mut names, &mut seen, candidate);
}
names
}
fn extract_qw_words(input: &str) -> (Vec<String>, String) {
let chars: Vec<char> = input.chars().collect();
let mut i = 0;
let mut words = Vec::new();
let mut remainder = String::new();
while i < chars.len() {
if chars[i] == 'q'
&& i + 1 < chars.len()
&& chars[i + 1] == 'w'
&& (i == 0 || !chars[i - 1].is_alphanumeric())
{
let mut j = i + 2;
while j < chars.len() && chars[j].is_whitespace() {
j += 1;
}
if j >= chars.len() {
remainder.push(chars[i]);
i += 1;
continue;
}
let open = chars[j];
let (close, is_paired_delimiter) = match open {
'(' => (')', true),
'[' => (']', true),
'{' => ('}', true),
'<' => ('>', true),
_ => (open, false),
};
if open.is_alphanumeric() || open == '_' || open == '\'' || open == '"' {
remainder.push(chars[i]);
i += 1;
continue;
}
let mut k = j + 1;
if is_paired_delimiter {
let mut depth = 1usize;
while k < chars.len() && depth > 0 {
if chars[k] == open {
depth += 1;
} else if chars[k] == close {
depth -= 1;
}
k += 1;
}
if depth != 0 {
remainder.extend(chars[i..].iter());
break;
}
k -= 1;
} else {
while k < chars.len() && chars[k] != close {
k += 1;
}
if k >= chars.len() {
remainder.extend(chars[i..].iter());
break;
}
}
let content: String = chars[j + 1..k].iter().collect();
for word in content.split_whitespace() {
if !word.is_empty() {
words.push(word.to_string());
}
}
i = k + 1;
continue;
}
remainder.push(chars[i]);
i += 1;
}
(words, remainder)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::Parser;
use perl_tdd_support::{must, must_some};
#[test]
fn test_symbol_extraction() {
let code = r#"
package Foo;
my $x = 42;
our $y = "hello";
sub bar {
my $z = $x + $y;
return $z;
}
"#;
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let extractor = SymbolExtractor::new_with_source(code);
let table = extractor.extract(&ast);
assert!(table.symbols.contains_key("Foo"));
let foo_symbols = &table.symbols["Foo"];
assert_eq!(foo_symbols.len(), 1);
assert_eq!(foo_symbols[0].kind, SymbolKind::Package);
assert!(table.symbols.contains_key("x"));
assert!(table.symbols.contains_key("y"));
assert!(table.symbols.contains_key("z"));
assert!(table.symbols.contains_key("bar"));
let bar_symbols = &table.symbols["bar"];
assert_eq!(bar_symbols.len(), 1);
assert_eq!(bar_symbols[0].kind, SymbolKind::Subroutine);
}
#[test]
fn test_method_node_uses_symbol_kind_method() {
let code = r#"
class MyClass {
method greet {
return "hello";
}
}
"#;
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let extractor = SymbolExtractor::new_with_source(code);
let table = extractor.extract(&ast);
assert!(table.symbols.contains_key("greet"), "expected 'greet' in symbol table");
let greet_symbols = &table.symbols["greet"];
assert_eq!(greet_symbols.len(), 1);
assert_eq!(
greet_symbols[0].kind,
SymbolKind::Method,
"NodeKind::Method should produce SymbolKind::Method, not Subroutine"
);
assert!(
greet_symbols[0].attributes.contains(&"method".to_string()),
"method symbol should have 'method' attribute"
);
}
#[test]
fn test_subroutine_mandatory_params_in_symbol_table() {
let code = r#"
sub foo ($x, $y) {
return $x + $y;
}
"#;
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let extractor = SymbolExtractor::new_with_source(code);
let table = extractor.extract(&ast);
assert!(
table.symbols.contains_key("x"),
"mandatory parameter $x should be in the symbol table"
);
assert!(
table.symbols.contains_key("y"),
"mandatory parameter $y should be in the symbol table"
);
let x_symbols = &table.symbols["x"];
assert_eq!(x_symbols.len(), 1);
assert_eq!(
x_symbols[0].declaration,
Some("my".to_string()),
"$x should be declared as 'my'"
);
let y_symbols = &table.symbols["y"];
assert_eq!(y_symbols.len(), 1);
assert_eq!(
y_symbols[0].declaration,
Some("my".to_string()),
"$y should be declared as 'my'"
);
}
#[test]
fn test_subroutine_optional_param_in_symbol_table() {
let code = r#"
sub bar ($x, $y = 0) {
return $x + $y;
}
"#;
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let extractor = SymbolExtractor::new_with_source(code);
let table = extractor.extract(&ast);
assert!(
table.symbols.contains_key("x"),
"mandatory parameter $x should be in the symbol table"
);
assert!(
table.symbols.contains_key("y"),
"optional parameter $y should be in the symbol table"
);
assert_eq!(
table.symbols["y"][0].declaration,
Some("my".to_string()),
"optional parameter $y should be declared as 'my'"
);
}
#[test]
fn test_subroutine_slurpy_param_in_symbol_table() {
let code = r#"
sub baz ($x, @rest) {
return scalar @rest;
}
"#;
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let extractor = SymbolExtractor::new_with_source(code);
let table = extractor.extract(&ast);
assert!(
table.symbols.contains_key("x"),
"mandatory parameter $x should be in the symbol table"
);
assert!(
table.symbols.contains_key("rest"),
"slurpy parameter @rest should be in the symbol table"
);
assert_eq!(
table.symbols["rest"][0].declaration,
Some("my".to_string()),
"slurpy parameter @rest should be declared as 'my'"
);
}
#[test]
fn test_method_signature_params_in_symbol_table() {
let code = r#"
class Foo {
method greet ($name) {
return $name;
}
}
"#;
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let extractor = SymbolExtractor::new_with_source(code);
let table = extractor.extract(&ast);
assert!(
table.symbols.contains_key("name"),
"method signature parameter $name should be in the symbol table"
);
assert_eq!(
table.symbols["name"][0].declaration,
Some("my".to_string()),
"method parameter $name should be declared as 'my'"
);
}
#[test]
fn test_empty_signature_no_crash() {
let code = r#"
sub foo () {
return 1;
}
"#;
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let extractor = SymbolExtractor::new_with_source(code);
let table = extractor.extract(&ast);
assert!(table.symbols.contains_key("foo"), "sub foo should be in the symbol table");
assert_eq!(
table.symbols.len(),
1,
"only 'foo' should be in the symbol table for an empty-signature sub"
);
}
#[test]
fn test_hash_slurpy_param_in_symbol_table() {
let code = r#"
sub configure ($x, %opts) {
return $opts{key};
}
"#;
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let extractor = SymbolExtractor::new_with_source(code);
let table = extractor.extract(&ast);
assert!(
table.symbols.contains_key("opts"),
"hash slurpy parameter %opts should be in the symbol table"
);
assert_eq!(
table.symbols["opts"][0].declaration,
Some("my".to_string()),
"hash slurpy parameter %opts should be declared as 'my'"
);
}
#[test]
fn test_optional_param_location_is_variable_span() {
let code = "sub bar ($x, $y = 0) { $x + $y }";
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let extractor = SymbolExtractor::new_with_source(code);
let table = extractor.extract(&ast);
let y_sym = &table.symbols["y"][0];
let span_len = y_sym.location.end - y_sym.location.start;
assert_eq!(
span_len, 2,
"symbol location should cover just '$y' (2 chars), not the full '$y = 0' (6 chars)"
);
}
#[test]
fn test_goto_label_creates_label_reference() {
let code = r#"
sub run {
goto FINISH;
FINISH:
return 1;
}
"#;
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let extractor = SymbolExtractor::new_with_source(code);
let table = extractor.extract(&ast);
let references = must_some(table.references.get("FINISH"));
assert!(
references.iter().any(|reference| reference.kind == SymbolKind::Label),
"goto FINISH should produce a label reference"
);
}
#[test]
fn test_goto_ampersand_creates_subroutine_reference() {
let code = r#"
sub target { return 42; }
sub jump {
goto ⌖
}
"#;
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let extractor = SymbolExtractor::new_with_source(code);
let table = extractor.extract(&ast);
let references = must_some(table.references.get("target"));
assert!(
references.iter().any(|reference| reference.kind == SymbolKind::Subroutine),
"goto &target should produce a subroutine reference"
);
}
}