use crate::types::VarKind;
use perl_ast::{Node, NodeKind};
#[derive(Debug, Clone, PartialEq)]
pub enum SymbolRefKind {
Variable(VarKind),
SubroutineCall,
MethodCall,
StaticMethodCall,
CoderefReference,
TypeglobReference,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SymbolRef {
pub kind: SymbolRefKind,
pub name: String,
pub qualified_name: String,
pub sigil: Option<String>,
pub package_qualifier: Option<String>,
pub full_span: (usize, usize),
pub anchor_span: Option<(usize, usize)>,
}
pub fn extract_symbol_refs(root: &Node) -> Vec<SymbolRef> {
let mut out = Vec::new();
walk(root, &mut out);
out
}
fn walk(node: &Node, out: &mut Vec<SymbolRef>) {
match &node.kind {
NodeKind::VariableDeclaration { initializer, .. }
| NodeKind::VariableListDeclaration { initializer, .. } => {
if let Some(init) = initializer {
walk(init, out);
}
}
NodeKind::MandatoryParameter { .. }
| NodeKind::SlurpyParameter { .. }
| NodeKind::NamedParameter { .. } => {
}
NodeKind::OptionalParameter { default_value, .. } => {
walk(default_value, out);
}
NodeKind::Goto { target } => {
if !push_coderef_target(target, (node.location.start, node.location.end), out) {
walk(target, out);
}
}
NodeKind::Unary { op, operand } if op == "\\" => {
if !push_coderef_target(operand, (node.location.start, node.location.end), out) {
walk(operand, out);
}
}
NodeKind::Variable { sigil, name } => {
push_variable_like_ref(node, sigil, name, out);
}
NodeKind::Typeglob { name } => {
let (package_qualifier, bare_name, qualified_name) = split_qualified_name(name);
out.push(SymbolRef {
kind: SymbolRefKind::TypeglobReference,
name: bare_name,
qualified_name,
sigil: Some("*".to_string()),
package_qualifier,
full_span: (node.location.start, node.location.end),
anchor_span: Some((node.location.start, node.location.end)),
});
}
NodeKind::FunctionCall { name, args } => {
let is_sentinel = matches!(name.as_str(), "->()" | "&{}" | "field");
if !is_sentinel {
let (package_qualifier, bare_name, qualified_name) = split_qualified_name(name);
out.push(SymbolRef {
kind: SymbolRefKind::SubroutineCall,
name: bare_name,
qualified_name,
sigil: None,
package_qualifier,
full_span: (node.location.start, node.location.end),
anchor_span: Some((node.location.start, node.location.end)),
});
}
for arg in args {
walk(arg, out);
}
}
NodeKind::MethodCall { object, method, args } => {
let (package_qualifier, qualified_name, kind) = static_method_target(object, method)
.map(|(package, qualified)| {
(Some(package), qualified, SymbolRefKind::StaticMethodCall)
})
.unwrap_or_else(|| (None, method.clone(), SymbolRefKind::MethodCall));
out.push(SymbolRef {
kind,
name: method.clone(),
qualified_name,
sigil: None,
package_qualifier,
full_span: (node.location.start, node.location.end),
anchor_span: None,
});
walk(object, out);
for arg in args {
walk(arg, out);
}
}
_ => {
node.for_each_child(|child| walk(child, out));
}
}
}
fn static_method_target(object: &Node, method: &str) -> Option<(String, String)> {
let NodeKind::Identifier { name } = &object.kind else {
return None;
};
if name.is_empty() || method.is_empty() {
return None;
}
Some((name.clone(), format!("{name}::{method}")))
}
fn push_variable_like_ref(node: &Node, sigil: &str, name: &str, out: &mut Vec<SymbolRef>) {
let kind = match sigil {
"&" => SymbolRefKind::CoderefReference,
"*" => SymbolRefKind::TypeglobReference,
_ => {
let Some(var_kind) = var_kind_from_sigil(sigil) else {
return;
};
SymbolRefKind::Variable(var_kind)
}
};
let (package_qualifier, bare_name, qualified_name) = split_qualified_name(name);
out.push(SymbolRef {
kind,
name: bare_name,
qualified_name,
sigil: Some(sigil.to_string()),
package_qualifier,
full_span: (node.location.start, node.location.end),
anchor_span: Some((node.location.start, node.location.end)),
});
}
fn push_coderef_target(node: &Node, full_span: (usize, usize), out: &mut Vec<SymbolRef>) -> bool {
let Some(name) = coderef_target_name(node) else {
return false;
};
let (package_qualifier, bare_name, qualified_name) = split_qualified_name(name);
out.push(SymbolRef {
kind: SymbolRefKind::CoderefReference,
name: bare_name,
qualified_name,
sigil: Some("&".to_string()),
package_qualifier,
full_span,
anchor_span: Some((node.location.start, node.location.end)),
});
true
}
fn coderef_target_name(node: &Node) -> Option<&str> {
match &node.kind {
NodeKind::Variable { sigil, name } if sigil == "&" => Some(name),
NodeKind::FunctionCall { name, args }
if args.is_empty() && has_parser_ampersand_span(node, name) =>
{
Some(name)
}
_ => None,
}
}
fn has_parser_ampersand_span(node: &Node, name: &str) -> bool {
node.location.end.saturating_sub(node.location.start) == name.len() + 1
}
fn split_qualified_name(name: &str) -> (Option<String>, String, String) {
if let Some((package, bare)) = name.rsplit_once("::")
&& !package.is_empty()
&& !bare.is_empty()
{
return (Some(package.to_owned()), bare.to_owned(), name.to_owned());
}
(None, name.to_owned(), name.to_owned())
}
fn var_kind_from_sigil(sigil: &str) -> Option<VarKind> {
match sigil {
"$" => Some(VarKind::Scalar),
"$#" => Some(VarKind::Scalar),
"@" => Some(VarKind::Array),
"%" => Some(VarKind::Hash),
_ => None,
}
}