use crate::types::{SymbolKind, VarKind};
use perl_ast::{Node, NodeKind};
#[derive(Debug, Clone, PartialEq)]
pub struct SymbolDecl {
pub kind: SymbolKind,
pub name: String,
pub qualified_name: String,
pub full_span: (usize, usize),
pub anchor_span: Option<(usize, usize)>,
pub container: Option<String>,
pub declarator: Option<String>,
}
pub fn extract_symbol_decls(root: &Node, current_package: Option<&str>) -> Vec<SymbolDecl> {
let mut out = Vec::new();
let mut ctx = WalkCtx {
current_package: current_package.map(str::to_owned),
const_fast_enabled: false,
readonly_enabled: false,
};
walk(root, &mut ctx, &mut out);
out
}
struct WalkCtx {
current_package: Option<String>,
const_fast_enabled: bool,
readonly_enabled: bool,
}
impl WalkCtx {
fn qualify(&self, name: &str) -> String {
match &self.current_package {
Some(pkg) => format!("{}::{}", pkg, name),
None => name.to_owned(),
}
}
}
fn walk(node: &Node, ctx: &mut WalkCtx, out: &mut Vec<SymbolDecl>) {
match &node.kind {
NodeKind::Package { name, name_span, block } => {
let anchor = Some((name_span.start, name_span.end));
let container = ctx.current_package.clone();
out.push(SymbolDecl {
kind: SymbolKind::Package,
name: name.clone(),
qualified_name: name.clone(),
full_span: (node.location.start, node.location.end),
anchor_span: anchor,
container,
declarator: None,
});
if let Some(blk) = block {
let saved = ctx.current_package.replace(name.clone());
walk(blk, ctx, out);
ctx.current_package = saved;
} else {
ctx.current_package = Some(name.clone());
}
}
NodeKind::Class { name, body, .. } => {
let container = ctx.current_package.clone();
out.push(SymbolDecl {
kind: SymbolKind::Class,
name: name.clone(),
qualified_name: ctx.qualify(name),
full_span: (node.location.start, node.location.end),
anchor_span: None, container,
declarator: None,
});
let saved = ctx.current_package.replace(name.clone());
walk(body, ctx, out);
ctx.current_package = saved;
}
NodeKind::Subroutine { name: Some(sub_name), name_span, body, .. } => {
let anchor = name_span.as_ref().map(|s| (s.start, s.end));
let container = ctx.current_package.clone();
let qualified_name = ctx.qualify(sub_name);
out.push(SymbolDecl {
kind: SymbolKind::Subroutine,
name: sub_name.clone(),
qualified_name,
full_span: (node.location.start, node.location.end),
anchor_span: anchor,
container,
declarator: None,
});
walk(body, ctx, out);
}
NodeKind::Subroutine { name: None, body, .. } => {
walk(body, ctx, out);
}
NodeKind::Method { name: method_name, body, .. } => {
let container = ctx.current_package.clone();
let qualified_name = ctx.qualify(method_name);
out.push(SymbolDecl {
kind: SymbolKind::Method,
name: method_name.clone(),
qualified_name,
full_span: (node.location.start, node.location.end),
anchor_span: None, container,
declarator: None,
});
walk(body, ctx, out);
}
NodeKind::Format { name, .. } => {
let container = ctx.current_package.clone();
out.push(SymbolDecl {
kind: SymbolKind::Format,
name: name.clone(),
qualified_name: ctx.qualify(name),
full_span: (node.location.start, node.location.end),
anchor_span: None, container,
declarator: None,
});
}
NodeKind::LabeledStatement { label, statement } => {
let container = ctx.current_package.clone();
out.push(SymbolDecl {
kind: SymbolKind::Label,
name: label.clone(),
qualified_name: label.clone(), full_span: (node.location.start, node.location.end),
anchor_span: None, container,
declarator: None,
});
walk(statement, ctx, out);
}
NodeKind::VariableDeclaration { declarator, variable, initializer, .. } => {
if let Some(decl) = variable_decl_from_node(variable, node, ctx, declarator) {
out.push(decl);
}
if let Some(init) = initializer {
walk(init, ctx, out);
}
}
NodeKind::VariableListDeclaration { declarator, variables, initializer, .. } => {
for var in variables {
if let Some(decl) = variable_decl_from_node(var, node, ctx, declarator) {
out.push(decl);
}
}
if let Some(init) = initializer {
walk(init, ctx, out);
}
}
NodeKind::Use { module, args, .. } if module == "constant" => {
for const_name in constant_names_from_use_args(args) {
let container = ctx.current_package.clone();
out.push(SymbolDecl {
kind: SymbolKind::Constant,
name: const_name.clone(),
qualified_name: ctx.qualify(&const_name),
full_span: (node.location.start, node.location.end),
anchor_span: None, container,
declarator: None,
});
}
}
NodeKind::Use { module, .. } if module == "Const::Fast" => {
ctx.const_fast_enabled = true;
}
NodeKind::Use { module, .. } if module == "Readonly" => {
ctx.readonly_enabled = true;
}
NodeKind::FunctionCall { name, args } if ctx.const_fast_enabled && name == "const" => {
for arg in args {
push_const_fast_decl(arg, node, ctx, out);
}
}
NodeKind::FunctionCall { name, args } if ctx.readonly_enabled && name == "Readonly" => {
for arg in args {
push_readonly_decl(arg, node, ctx, out);
}
}
NodeKind::Program { statements } | NodeKind::Block { statements } => {
walk_statements(statements, ctx, out);
}
NodeKind::ExpressionStatement { expression } => {
walk(expression, ctx, out);
}
_ => {}
}
}
fn constant_names_from_use_args(args: &[String]) -> Vec<String> {
let mut names = Vec::new();
let mut brace_depth = 0usize;
let mut fallback_name: Option<String> = None;
for (idx, arg) in args.iter().enumerate() {
match arg.as_str() {
"{" => {
brace_depth += 1;
continue;
}
"}" => {
brace_depth = brace_depth.saturating_sub(1);
continue;
}
"+" | "," => continue,
_ => {}
}
if let Some(qw_names) = qw_names(arg) {
for name in qw_names {
push_unique(&mut names, name);
}
continue;
}
if is_constant_name_candidate(arg) {
if brace_depth == 1 && args.get(idx + 1).is_some_and(|next| next == "=>") {
push_unique(&mut names, arg.clone());
continue;
}
if brace_depth == 0 && fallback_name.is_none() {
fallback_name = Some(arg.clone());
}
}
}
if names.is_empty() {
if let Some(name) = fallback_name {
names.push(name);
}
}
names
}
fn qw_names(arg: &str) -> Option<Vec<String>> {
let content = arg.strip_prefix("qw").map(str::trim_start).and_then(|rest| {
rest.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
.or_else(|| rest.strip_prefix('[').and_then(|s| s.strip_suffix(']')))
.or_else(|| rest.strip_prefix('{').and_then(|s| s.strip_suffix('}')))
.or_else(|| rest.strip_prefix('<').and_then(|s| s.strip_suffix('>')))
});
content.map(|text| {
text.split_whitespace().filter(|name| !name.is_empty()).map(str::to_owned).collect()
})
}
fn is_constant_name_candidate(arg: &str) -> bool {
!arg.is_empty()
&& arg != "=>"
&& !arg.starts_with('{')
&& !arg.starts_with('}')
&& !arg.starts_with('-')
&& !arg.starts_with('$')
&& !arg.starts_with('@')
&& !arg.starts_with('%')
}
fn push_unique(names: &mut Vec<String>, name: String) {
if !names.iter().any(|existing| existing == &name) {
names.push(name);
}
}
fn push_const_fast_decl(arg: &Node, call_node: &Node, ctx: &WalkCtx, out: &mut Vec<SymbolDecl>) {
match &arg.kind {
NodeKind::VariableDeclaration { variable, .. } => {
if let Some(decl) = constant_wrapper_decl_from_node(variable, call_node, ctx, "const") {
out.push(decl);
}
}
NodeKind::VariableListDeclaration { variables, .. } => {
for variable in variables {
if let Some(decl) =
constant_wrapper_decl_from_node(variable, call_node, ctx, "const")
{
out.push(decl);
}
}
}
_ => {}
}
}
fn push_readonly_decl(arg: &Node, call_node: &Node, ctx: &WalkCtx, out: &mut Vec<SymbolDecl>) {
match &arg.kind {
NodeKind::VariableDeclaration { variable, .. } => {
if let Some(decl) =
constant_wrapper_decl_from_node(variable, call_node, ctx, "Readonly")
{
out.push(decl);
}
}
NodeKind::VariableListDeclaration { variables, .. } => {
for variable in variables {
if let Some(decl) =
constant_wrapper_decl_from_node(variable, call_node, ctx, "Readonly")
{
out.push(decl);
}
}
}
_ => {}
}
}
fn constant_wrapper_decl_from_node(
var_node: &Node,
call_node: &Node,
ctx: &WalkCtx,
declarator: &str,
) -> Option<SymbolDecl> {
match &var_node.kind {
NodeKind::Variable { name, .. } => {
let anchor_span = Some((var_node.location.start, var_node.location.end));
let container = ctx.current_package.clone();
Some(SymbolDecl {
kind: SymbolKind::Constant,
name: name.clone(),
qualified_name: ctx.qualify(name),
full_span: (call_node.location.start, call_node.location.end),
anchor_span,
container,
declarator: Some(declarator.to_string()),
})
}
NodeKind::VariableWithAttributes { variable, .. } => {
constant_wrapper_decl_from_node(variable, call_node, ctx, declarator)
}
_ => None,
}
}
fn walk_statements(statements: &[Node], ctx: &mut WalkCtx, out: &mut Vec<SymbolDecl>) {
for stmt in statements {
walk(stmt, ctx, out);
}
}
fn variable_decl_from_node(
var_node: &Node,
decl_node: &Node,
ctx: &WalkCtx,
declarator: &str,
) -> Option<SymbolDecl> {
match &var_node.kind {
NodeKind::Variable { sigil, name } => {
let kind = sigil_to_symbol_kind(sigil);
let anchor_span = Some((var_node.location.start, var_node.location.end));
let container = ctx.current_package.clone();
Some(SymbolDecl {
kind,
name: name.clone(),
qualified_name: ctx.qualify(name),
full_span: (decl_node.location.start, decl_node.location.end),
anchor_span,
container,
declarator: Some(declarator.to_owned()),
})
}
NodeKind::VariableWithAttributes { variable, .. } => {
variable_decl_from_node(variable, decl_node, ctx, declarator)
}
_ => None,
}
}
fn sigil_to_symbol_kind(sigil: &str) -> SymbolKind {
match sigil {
"@" => SymbolKind::Variable(VarKind::Array),
"%" => SymbolKind::Variable(VarKind::Hash),
_ => SymbolKind::Variable(VarKind::Scalar),
}
}
#[cfg(test)]
mod tests {
use super::{constant_names_from_use_args, is_constant_name_candidate, qw_names};
#[test]
fn constant_names_extract_hash_style_pairs() {
let args = vec![
"{".to_string(),
"FOO".to_string(),
"=>".to_string(),
"1".to_string(),
",".to_string(),
"BAR".to_string(),
"=>".to_string(),
"2".to_string(),
"}".to_string(),
];
assert_eq!(constant_names_from_use_args(&args), vec!["FOO".to_string(), "BAR".to_string()]);
}
#[test]
fn constant_names_falls_back_to_first_top_level_candidate() {
let args = vec!["ANSWER".to_string(), "=>".to_string(), "42".to_string()];
assert_eq!(constant_names_from_use_args(&args), vec!["ANSWER".to_string()]);
}
#[test]
fn constant_names_supports_qw_and_deduplicates_entries() {
let args = vec!["qw(ONE TWO ONE)".to_string()];
assert_eq!(constant_names_from_use_args(&args), vec!["ONE".to_string(), "TWO".to_string()]);
}
#[test]
fn qw_names_support_multiple_delimiters() {
assert_eq!(qw_names("qw(one two)"), Some(vec!["one".to_string(), "two".to_string()]));
assert_eq!(qw_names("qw[one two]"), Some(vec!["one".to_string(), "two".to_string()]));
assert_eq!(qw_names("qw{one two}"), Some(vec!["one".to_string(), "two".to_string()]));
assert_eq!(qw_names("qw<one two>"), Some(vec!["one".to_string(), "two".to_string()]));
}
#[test]
fn constant_name_candidate_rejects_non_names() {
assert!(is_constant_name_candidate("VALID_NAME"));
assert!(!is_constant_name_candidate(""));
assert!(!is_constant_name_candidate("=>"));
assert!(!is_constant_name_candidate("$scalar"));
assert!(!is_constant_name_candidate("@array"));
assert!(!is_constant_name_candidate("%hash"));
assert!(!is_constant_name_candidate("-flag"));
}
}