use crate::ast::{Node, NodeKind};
use crate::symbol::is_universal_method;
use crate::workspace_index::{SymKind, SymbolKey};
use rustc_hash::FxHashMap;
use std::sync::Arc;
pub type ParentMap = FxHashMap<*const Node, *const Node>;
pub struct DeclarationProvider<'a> {
pub ast: Arc<Node>,
content: String,
document_uri: String,
parent_map: Option<&'a ParentMap>,
doc_version: i32,
}
#[derive(Debug, Clone)]
pub struct LocationLink {
pub origin_selection_range: (usize, usize),
pub target_uri: String,
pub target_range: (usize, usize),
pub target_selection_range: (usize, usize),
}
impl<'a> DeclarationProvider<'a> {
pub fn new(ast: Arc<Node>, content: String, document_uri: String) -> Self {
Self {
ast,
content,
document_uri,
parent_map: None,
doc_version: 0, }
}
pub fn with_parent_map(mut self, parent_map: &'a ParentMap) -> Self {
#[cfg(debug_assertions)]
{
debug_assert!(
!parent_map.is_empty(),
"DeclarationProvider: empty ParentMap (did you forget to rebuild after AST refresh?)"
);
let root_ptr = &*self.ast as *const _;
debug_assert!(
!parent_map.contains_key(&root_ptr),
"Root node must have no parent in the parent map"
);
Self::debug_assert_no_cycles(parent_map);
}
self.parent_map = Some(parent_map);
self
}
pub fn with_doc_version(mut self, version: i32) -> Self {
self.doc_version = version;
self
}
#[inline]
#[track_caller]
fn is_fresh(&self, current_version: i32) -> bool {
if self.doc_version != current_version {
tracing::warn!(
provider_version = self.doc_version,
current_version,
"DeclarationProvider used after AST refresh — returning empty result"
);
return false;
}
true
}
#[cfg(debug_assertions)]
fn debug_assert_no_cycles(parent_map: &ParentMap) {
let cap = parent_map.len() + 1;
for (&child, _) in parent_map.iter() {
let mut current = child;
let mut depth = 0;
while depth < cap {
if let Some(&parent) = parent_map.get(¤t) {
current = parent;
depth += 1;
} else {
break;
}
}
if depth >= cap {
tracing::warn!(
depth_limit = cap,
"Cycle detected in ParentMap - node is its own ancestor"
);
break;
}
}
}
pub fn build_parent_map(node: &Node, map: &mut ParentMap, parent: Option<*const Node>) {
if let Some(p) = parent {
map.insert(node as *const _, p);
}
for child in Self::get_children_static(node) {
Self::build_parent_map(child, map, Some(node as *const _));
}
}
pub fn find_declaration(
&self,
offset: usize,
current_version: i32,
) -> Option<Vec<LocationLink>> {
if !self.is_fresh(current_version) {
return None;
}
let node = self.find_node_at_offset(&self.ast, offset)?;
match &node.kind {
NodeKind::Variable { name, .. } => self.find_variable_declaration(node, name),
NodeKind::FunctionCall { name, .. } => self.find_subroutine_declaration(node, name),
NodeKind::MethodCall { method, object, .. } => {
self.find_method_declaration(node, method, object)
}
NodeKind::IndirectCall { method, object, .. } => {
self.find_method_declaration(node, method, object)
}
NodeKind::Identifier { name } => self.find_identifier_declaration(node, name),
NodeKind::Goto { target } => {
if let NodeKind::Identifier { name } = &target.kind {
self.find_label_declaration(node, name)
.or_else(|| self.find_subroutine_declaration(node, name))
} else {
None
}
}
NodeKind::String { value, .. } => self.find_modifier_target_declaration(node, value),
_ => None,
}
}
fn find_variable_declaration(&self, usage: &Node, var_name: &str) -> Option<Vec<LocationLink>> {
let mut current_ptr: *const Node = usage as *const _;
let temp_parent_map;
let parent_map = if let Some(pm) = self.parent_map {
pm
} else {
temp_parent_map = {
let mut map = FxHashMap::default();
Self::build_parent_map(&self.ast, &mut map, None);
map
};
&temp_parent_map
};
let node_lookup = self.build_node_lookup_map();
while let Some(&parent_ptr) = parent_map.get(¤t_ptr) {
let Some(parent) = node_lookup.get(&parent_ptr).copied() else {
break;
};
if matches!(parent.kind, NodeKind::Subroutine { .. } | NodeKind::Method { .. }) {
if let Some(links) =
self.find_signature_parameter_declaration(parent, usage, var_name)
{
return Some(links);
}
}
for child in self.get_children(parent) {
if child.location.start >= usage.location.start {
break;
}
if let NodeKind::VariableDeclaration { variable, .. } = &child.kind {
if let NodeKind::Variable { name, .. } = &variable.kind {
if name == var_name {
return Some(vec![LocationLink {
origin_selection_range: (usage.location.start, usage.location.end),
target_uri: self.document_uri.clone(),
target_range: (child.location.start, child.location.end),
target_selection_range: (
variable.location.start,
variable.location.end,
),
}]);
}
}
}
if let NodeKind::VariableListDeclaration { variables, .. } = &child.kind {
for var in variables {
if let NodeKind::Variable { name, .. } = &var.kind {
if name == var_name {
return Some(vec![LocationLink {
origin_selection_range: (
usage.location.start,
usage.location.end,
),
target_uri: self.document_uri.clone(),
target_range: (child.location.start, child.location.end),
target_selection_range: (var.location.start, var.location.end),
}]);
}
}
}
}
}
current_ptr = parent_ptr;
}
None
}
fn find_signature_parameter_declaration(
&self,
declaration_site: &Node,
usage: &Node,
var_name: &str,
) -> Option<Vec<LocationLink>> {
let signature = match &declaration_site.kind {
NodeKind::Subroutine { signature, .. } | NodeKind::Method { signature, .. } => {
signature.as_deref()?
}
_ => return None,
};
let NodeKind::Signature { parameters } = &signature.kind else {
return None;
};
for parameter in parameters {
let variable = match ¶meter.kind {
NodeKind::MandatoryParameter { variable }
| NodeKind::OptionalParameter { variable, .. }
| NodeKind::SlurpyParameter { variable }
| NodeKind::NamedParameter { variable } => variable.as_ref(),
_ => continue,
};
let NodeKind::Variable { name, .. } = &variable.kind else {
continue;
};
if name == var_name {
return Some(vec![self.create_location_link(
usage,
parameter,
(variable.location.start, variable.location.end),
)]);
}
}
None
}
fn find_subroutine_declaration(
&self,
node: &Node,
func_name: &str,
) -> Option<Vec<LocationLink>> {
let (target_package, target_name) = if let Some(pos) = func_name.rfind("::") {
let package = &func_name[..pos];
let name = &func_name[pos + 2..];
(Some(package), name)
} else {
(self.find_current_package(node), func_name)
};
let mut declarations = Vec::new();
self.collect_subroutine_declarations(&self.ast, target_name, &mut declarations);
if let Some(pkg_name) = target_package {
if let Some(decl) =
declarations.iter().find(|d| self.find_current_package(d) == Some(pkg_name))
{
return Some(vec![self.create_location_link(
node,
decl,
self.get_subroutine_name_range(decl),
)]);
}
}
if let Some(decl) = declarations.first() {
return Some(vec![self.create_location_link(
node,
decl,
self.get_subroutine_name_range(decl),
)]);
}
None
}
fn find_method_declaration(
&self,
node: &Node,
method_name: &str,
object: &Node,
) -> Option<Vec<LocationLink>> {
let package_name = match &object.kind {
NodeKind::Identifier { name } if name.chars().next()?.is_uppercase() => {
Some(name.as_str())
}
_ => None,
};
if let Some(pkg) = package_name {
let mut declarations = Vec::new();
self.collect_subroutine_declarations(&self.ast, method_name, &mut declarations);
if let Some(decl) =
declarations.iter().find(|d| self.find_current_package(d) == Some(pkg))
{
return Some(vec![self.create_location_link(
node,
decl,
self.get_subroutine_name_range(decl),
)]);
}
if is_universal_method(method_name)
&& let Some(decl) =
declarations.iter().find(|d| self.find_current_package(d) == Some("UNIVERSAL"))
{
return Some(vec![self.create_location_link(
node,
decl,
self.get_subroutine_name_range(decl),
)]);
}
}
self.find_subroutine_declaration(node, method_name)
}
fn find_identifier_declaration(&self, node: &Node, name: &str) -> Option<Vec<LocationLink>> {
if self.identifier_is_goto_target(node)
&& let Some(links) = self.find_label_declaration(node, name)
{
return Some(links);
}
if let Some(links) = self.find_subroutine_declaration(node, name) {
return Some(links);
}
let packages = self.find_package_declarations(&self.ast, name);
if let Some(pkg) = packages.first() {
return Some(vec![self.create_location_link(
node,
pkg,
self.get_package_name_range(pkg),
)]);
}
let constants = self.find_constant_declarations(&self.ast, name);
if let Some(const_decl) = constants.first() {
return Some(vec![self.create_location_link(
node,
const_decl,
self.get_constant_name_range_for(const_decl, name),
)]);
}
None
}
fn find_label_declaration(&self, origin: &Node, label_name: &str) -> Option<Vec<LocationLink>> {
let mut labels = Vec::new();
self.collect_label_declarations(&self.ast, label_name, &mut labels);
let labeled_stmt = labels.first().copied()?;
Some(vec![self.create_location_link(
origin,
labeled_stmt,
self.get_labeled_statement_label_range(labeled_stmt),
)])
}
fn collect_label_declarations<'b>(
&'b self,
node: &'b Node,
label_name: &str,
labels: &mut Vec<&'b Node>,
) {
if let NodeKind::LabeledStatement { label, .. } = &node.kind
&& label == label_name
{
labels.push(node);
}
for child in self.get_children(node) {
self.collect_label_declarations(child, label_name, labels);
}
}
fn get_labeled_statement_label_range(&self, node: &Node) -> (usize, usize) {
let NodeKind::LabeledStatement { label, .. } = &node.kind else {
return (node.location.start, node.location.end);
};
let start = node.location.start;
let end = node.location.end.min(self.content.len());
if start >= end {
return (node.location.start, node.location.end);
}
let text = &self.content[start..end];
let label_start = text.find(label).map_or(start, |idx| start + idx);
let label_end = label_start.saturating_add(label.len()).min(end);
(label_start, label_end)
}
fn identifier_is_goto_target(&self, node: &Node) -> bool {
let temp_parent_map;
let parent_map = if let Some(pm) = self.parent_map {
pm
} else {
temp_parent_map = {
let mut map = FxHashMap::default();
Self::build_parent_map(&self.ast, &mut map, None);
map
};
&temp_parent_map
};
let node_lookup = self.build_node_lookup_map();
let node_ptr = node as *const _;
let Some(parent_ptr) = parent_map.get(&node_ptr).copied() else {
return false;
};
let Some(parent) = node_lookup.get(&parent_ptr).copied() else {
return false;
};
match &parent.kind {
NodeKind::Goto { target } => std::ptr::eq(target.as_ref(), node),
_ => false,
}
}
fn find_modifier_target_declaration(
&self,
string_node: &Node,
method_name: &str,
) -> Option<Vec<LocationLink>> {
let bare_name = method_name.trim().trim_matches('\'').trim_matches('"').trim();
if bare_name.is_empty() {
return None;
}
let temp_parent_map;
let parent_map = if let Some(pm) = self.parent_map {
pm
} else {
temp_parent_map = {
let mut map = FxHashMap::default();
Self::build_parent_map(&self.ast, &mut map, None);
map
};
&temp_parent_map
};
let node_lookup = self.build_node_lookup_map();
let string_ptr: *const Node = string_node as *const _;
let parent_ptr = parent_map.get(&string_ptr).copied()?;
let parent = node_lookup.get(&parent_ptr).copied()?;
if let NodeKind::FunctionCall { name, args } = &parent.kind {
if matches!(name.as_str(), "before" | "after" | "around" | "override") {
if args.first().map(|a| std::ptr::eq(a, string_node)).unwrap_or(false) {
return self.find_subroutine_declaration(string_node, bare_name);
}
}
}
let grandparent_ptr = parent_map.get(&parent_ptr).copied()?;
let grandparent = node_lookup.get(&grandparent_ptr).copied()?;
if let NodeKind::FunctionCall { name, args } = &grandparent.kind {
if matches!(name.as_str(), "before" | "after" | "around" | "override") {
if args.first().map(|a| std::ptr::eq(a, string_node)).unwrap_or(false) {
return self.find_subroutine_declaration(string_node, bare_name);
}
}
}
None
}
fn find_current_package<'b>(&'b self, node: &Node) -> Option<&'b str> {
let mut current_ptr: *const Node = node as *const _;
let temp_parent_map;
let parent_map = if let Some(pm) = self.parent_map {
pm
} else {
temp_parent_map = {
let mut map = FxHashMap::default();
Self::build_parent_map(&self.ast, &mut map, None);
map
};
&temp_parent_map
};
let node_lookup = self.build_node_lookup_map();
while let Some(&parent_ptr) = parent_map.get(¤t_ptr) {
let Some(parent) = node_lookup.get(&parent_ptr).copied() else {
break;
};
for child in self.get_children(parent) {
if child.location.start >= node.location.start {
break;
}
if let NodeKind::Package { name, .. } = &child.kind {
return Some(name.as_str());
}
}
current_ptr = parent_ptr;
}
None
}
fn create_location_link(
&self,
origin: &Node,
target: &Node,
name_range: (usize, usize),
) -> LocationLink {
LocationLink {
origin_selection_range: (origin.location.start, origin.location.end),
target_uri: self.document_uri.clone(),
target_range: (target.location.start, target.location.end),
target_selection_range: name_range,
}
}
fn find_node_at_offset<'b>(&'b self, node: &'b Node, offset: usize) -> Option<&'b Node> {
if offset >= node.location.start && offset <= node.location.end {
for child in self.get_children(node) {
if let Some(found) = self.find_node_at_offset(child, offset) {
return Some(found);
}
}
return Some(node);
}
None
}
fn collect_subroutine_declarations<'b>(
&'b self,
node: &'b Node,
sub_name: &str,
subs: &mut Vec<&'b Node>,
) {
if let NodeKind::Subroutine { name, .. } = &node.kind {
if let Some(name_str) = name {
if name_str == sub_name {
subs.push(node);
}
}
}
for child in self.get_children(node) {
self.collect_subroutine_declarations(child, sub_name, subs);
}
}
fn find_package_declarations<'b>(&'b self, node: &'b Node, pkg_name: &str) -> Vec<&'b Node> {
let mut packages = Vec::new();
self.collect_package_declarations(node, pkg_name, &mut packages);
packages
}
fn collect_package_declarations<'b>(
&'b self,
node: &'b Node,
pkg_name: &str,
packages: &mut Vec<&'b Node>,
) {
if let NodeKind::Package { name, .. } = &node.kind {
if name == pkg_name {
packages.push(node);
}
}
for child in self.get_children(node) {
self.collect_package_declarations(child, pkg_name, packages);
}
}
fn find_constant_declarations<'b>(&'b self, node: &'b Node, const_name: &str) -> Vec<&'b Node> {
let mut constants = Vec::new();
self.collect_constant_declarations(node, const_name, &mut constants);
constants
}
fn strip_constant_options<'b>(&self, args: &'b [String]) -> &'b [String] {
let mut i = 0;
while i < args.len() && args[i].starts_with('-') {
i += 1;
}
if i < args.len() && args[i] == "," {
i += 1;
}
&args[i..]
}
fn collect_constant_declarations<'b>(
&'b self,
node: &'b Node,
const_name: &str,
constants: &mut Vec<&'b Node>,
) {
if let NodeKind::Use { module, args, .. } = &node.kind {
if module == "constant" {
let stripped_args = self.strip_constant_options(args);
if stripped_args.first().map(|s| s.as_str()) == Some(const_name) {
constants.push(node);
}
let args_text = stripped_args.join(" ");
if self.contains_name_in_hash(&args_text, const_name) {
constants.push(node);
}
if self.contains_name_in_qw(&args_text, const_name) {
constants.push(node);
}
}
}
for child in self.get_children(node) {
self.collect_constant_declarations(child, const_name, constants);
}
}
#[inline]
fn is_ident_ascii(b: u8) -> bool {
matches!(b, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_')
}
fn for_each_qw_window<F>(&self, s: &str, mut f: F) -> bool
where
F: FnMut(usize, usize) -> bool,
{
let b = s.as_bytes();
let mut i = 0;
while i + 1 < b.len() {
if b[i] == b'q' && b[i + 1] == b'w' {
let mut j = i + 2;
while j < b.len() && (b[j] as char).is_ascii_whitespace() {
j += 1;
}
if j >= b.len() {
break;
}
let open = b[j] as char;
if open.is_ascii_alphanumeric() || open == '_' {
i += 1;
continue;
}
let close = match open {
'(' => ')',
'[' => ']',
'{' => '}',
'<' => '>',
_ => open, };
j += 1;
let start = j;
while j < b.len() && (b[j] as char) != close {
j += 1;
}
if j <= b.len() {
if f(start, j) {
return true;
}
i = j + 1;
continue;
} else {
break;
}
}
i += 1;
}
false
}
fn for_each_brace_window<F>(&self, s: &str, mut f: F) -> bool
where
F: FnMut(usize, usize) -> bool,
{
let b = s.as_bytes();
let mut i = 0;
while i < b.len() {
if b[i] == b'{' {
let start = i + 1;
let mut nesting = 1;
let mut j = i + 1;
while j < b.len() {
match b[j] {
b'{' => nesting += 1,
b'}' => {
nesting -= 1;
if nesting == 0 {
break;
}
}
_ => {}
}
j += 1;
}
if nesting == 0 {
if f(start, j) {
return true;
}
i = j + 1;
continue;
}
}
i += 1;
}
false
}
fn contains_name_in_hash(&self, s: &str, name: &str) -> bool {
self.for_each_brace_window(s, |start, end| {
self.find_word(&s[start..end], name).is_some()
})
}
fn contains_name_in_qw(&self, s: &str, name: &str) -> bool {
self.for_each_qw_window(s, |start, end| {
s[start..end].split_whitespace().any(|tok| tok == name)
})
}
fn find_word(&self, hay: &str, needle: &str) -> Option<(usize, usize)> {
if needle.is_empty() {
return None;
}
let mut find_from = 0;
while let Some(hit) = hay[find_from..].find(needle) {
let start = find_from + hit;
let end = start + needle.len();
let left_ok = start == 0 || !Self::is_ident_ascii(hay.as_bytes()[start - 1]);
let right_ok = end == hay.len()
|| !Self::is_ident_ascii(*hay.as_bytes().get(end).unwrap_or(&b' '));
if left_ok && right_ok {
return Some((start, end));
}
find_from = end;
}
None
}
fn first_all_caps_word(&self, s: &str) -> Option<(usize, usize)> {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
while i < bytes.len() && !Self::is_ident_ascii(bytes[i]) {
i += 1;
}
let start = i;
while i < bytes.len() && Self::is_ident_ascii(bytes[i]) {
i += 1;
}
if start < i {
let w = &s[start..i];
if w.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_') {
return Some((start, i));
}
}
}
None
}
fn get_subroutine_name_range(&self, decl: &Node) -> (usize, usize) {
if let NodeKind::Subroutine { name_span: Some(loc), .. } = &decl.kind {
(loc.start, loc.end)
} else {
(decl.location.start, decl.location.end)
}
}
fn get_package_name_range(&self, decl: &Node) -> (usize, usize) {
if let NodeKind::Package { name_span, .. } = &decl.kind {
(name_span.start, name_span.end)
} else {
(decl.location.start, decl.location.end)
}
}
fn get_constant_name_range(&self, decl: &Node) -> (usize, usize) {
let text = self.get_node_text(decl);
if let NodeKind::Use { args, .. } = &decl.kind {
let best_guess = args.first().map(|s| s.as_str()).unwrap_or("");
if let Some((lo, hi)) = self.find_word(&text, best_guess) {
let abs_lo = decl.location.start + lo;
let abs_hi = decl.location.start + hi;
return (abs_lo, abs_hi);
}
}
if let Some((lo, hi)) = self.first_all_caps_word(&text) {
return (decl.location.start + lo, decl.location.start + hi);
}
(decl.location.start, decl.location.end)
}
fn get_constant_name_range_for(&self, decl: &Node, name: &str) -> (usize, usize) {
let text = self.get_node_text(decl);
if let Some((lo, hi)) = self.find_word(&text, name) {
return (decl.location.start + lo, decl.location.start + hi);
}
let mut found_range = None;
self.for_each_qw_window(&text, |start, end| {
if let Some((lo, hi)) = self.find_word(&text[start..end], name) {
found_range =
Some((decl.location.start + start + lo, decl.location.start + start + hi));
true } else {
false }
});
if let Some(range) = found_range {
return range;
}
self.for_each_brace_window(&text, |start, end| {
if let Some((lo, hi)) = self.find_word(&text[start..end], name) {
found_range =
Some((decl.location.start + start + lo, decl.location.start + start + hi));
true } else {
false }
});
if let Some(range) = found_range {
return range;
}
self.get_constant_name_range(decl)
}
fn get_children<'b>(&self, node: &'b Node) -> Vec<&'b Node> {
Self::get_children_static(node)
}
fn build_node_lookup_map(&self) -> FxHashMap<*const Node, &Node> {
let mut map = FxHashMap::default();
Self::build_node_lookup(self.ast.as_ref(), &mut map);
map
}
fn build_node_lookup<'b>(node: &'b Node, map: &mut FxHashMap<*const Node, &'b Node>) {
map.insert(node as *const Node, node);
for child in Self::get_children_static(node) {
Self::build_node_lookup(child, map);
}
}
fn get_children_static(node: &Node) -> Vec<&Node> {
match &node.kind {
NodeKind::Program { statements } => statements.iter().collect(),
NodeKind::Block { statements } => statements.iter().collect(),
NodeKind::If { condition, then_branch, else_branch, .. } => {
let mut children = vec![condition.as_ref(), then_branch.as_ref()];
if let Some(else_b) = else_branch {
children.push(else_b.as_ref());
}
children
}
NodeKind::Binary { left, right, .. } => vec![left.as_ref(), right.as_ref()],
NodeKind::Unary { operand, .. } => vec![operand.as_ref()],
NodeKind::Return { value } => {
if let Some(value) = value {
vec![value.as_ref()]
} else {
vec![]
}
}
NodeKind::VariableDeclaration { variable, initializer, .. } => {
let mut children = vec![variable.as_ref()];
if let Some(init) = initializer {
children.push(init.as_ref());
}
children
}
NodeKind::Method { signature, body, .. } => {
let mut children = vec![body.as_ref()];
if let Some(sig) = signature {
children.push(sig.as_ref());
}
children
}
NodeKind::Subroutine { signature, body, .. } => {
let mut children = vec![body.as_ref()];
if let Some(sig) = signature {
children.push(sig.as_ref());
}
children
}
NodeKind::FunctionCall { args, .. } => args.iter().collect(),
NodeKind::MethodCall { object, args, .. } => {
let mut children = vec![object.as_ref()];
children.extend(args.iter());
children
}
NodeKind::IndirectCall { object, args, .. } => {
let mut children = vec![object.as_ref()];
children.extend(args.iter());
children
}
NodeKind::While { condition, body, .. } => {
vec![condition.as_ref(), body.as_ref()]
}
NodeKind::For { init, condition, update, body, .. } => {
let mut children = Vec::new();
if let Some(i) = init {
children.push(i.as_ref());
}
if let Some(c) = condition {
children.push(c.as_ref());
}
if let Some(u) = update {
children.push(u.as_ref());
}
children.push(body.as_ref());
children
}
NodeKind::Foreach { variable, list, body, .. } => {
vec![variable.as_ref(), list.as_ref(), body.as_ref()]
}
NodeKind::ExpressionStatement { expression } => vec![expression.as_ref()],
_ => vec![],
}
}
pub fn get_node_text(&self, node: &Node) -> String {
self.content[node.location.start..node.location.end].to_string()
}
}
fn symbol_at_cursor_internal(
ast: &Node,
offset: usize,
current_pkg: &str,
source_text: &str,
) -> Option<SymbolKey> {
fn collect_node_path_at_offset<'a>(
node: &'a Node,
offset: usize,
path: &mut Vec<&'a Node>,
) -> bool {
if offset < node.location.start || offset > node.location.end {
return false;
}
path.push(node);
for child in get_node_children(node) {
if collect_node_path_at_offset(child, offset, path) {
return true;
}
}
true
}
fn find_symbol_node_at_offset(ast: &Node, offset: usize) -> Option<(Vec<&Node>, &Node)> {
let mut path = Vec::new();
if !collect_node_path_at_offset(ast, offset, &mut path) {
return None;
}
let node = path
.iter()
.rev()
.copied()
.find(|node| {
matches!(
node.kind,
NodeKind::Variable { .. }
| NodeKind::FunctionCall { .. }
| NodeKind::Subroutine { .. }
| NodeKind::MethodCall { .. }
| NodeKind::Use { .. }
)
})
.or_else(|| path.last().copied())?;
Some((path, node))
}
fn node_variable_name(node: &Node) -> Option<&str> {
if let NodeKind::Variable { name, .. } = &node.kind { Some(name.as_str()) } else { None }
}
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 token_at_offset_in_text(text: &str, rel_offset: usize) -> Option<String> {
let bytes = text.as_bytes();
if rel_offset >= bytes.len() {
return None;
}
let is_ident = |b: u8| matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_' | b':');
if !is_ident(bytes[rel_offset]) {
return None;
}
let mut start = rel_offset;
while start > 0 && is_ident(bytes[start - 1]) {
start -= 1;
}
let mut end = rel_offset + 1;
while end < bytes.len() && is_ident(bytes[end]) {
end += 1;
}
Some(text[start..end].to_string())
}
fn export_tag_members(module: &str, tag: &str) -> &'static [&'static str] {
match (module, tag) {
("POSIX", ":sys_wait_h") => {
&["WEXITSTATUS", "WIFEXITED", "WIFSIGNALED", "WIFSTOPPED", "WTERMSIG"]
}
("POSIX", ":fcntl_h") => &["F_GETFD", "F_SETFD", "F_GETFL", "F_SETFL", "FD_CLOEXEC"],
("POSIX", ":termios_h") => {
&["B9600", "B19200", "B38400", "TCSANOW", "TCSADRAIN", "TCSAFLUSH"]
}
("File::Find", ":find") => &["find", "finddepth"],
("Fcntl", ":seek") => &["SEEK_SET", "SEEK_CUR", "SEEK_END"],
("Fcntl", ":lock") => &["LOCK_SH", "LOCK_EX", "LOCK_NB", "LOCK_UN"],
("Encode", ":fallback") => &[
"FB_DEFAULT",
"FB_CROAK",
"FB_QUIET",
"FB_WARN",
"FB_PERLQQ",
"FB_HTMLCREF",
"FB_XMLCREF",
],
_ => &[],
}
}
fn tag_imports_symbol(module: &str, import_token: &str, symbol_name: &str) -> bool {
if !import_token.starts_with(':') {
return false;
}
export_tag_members(module, import_token).contains(&symbol_name)
}
const NON_IMPORT_PRAGMAS: &[&str] = &[
"constant", "parent", "base", "vars", "Exporter", "mro", "if", "lib", "feature", "utf8", ];
fn use_args_import_symbol(module: &str, args: &[String], symbol_name: &str) -> bool {
args.iter().any(|arg| {
if arg == symbol_name || tag_imports_symbol(module, arg, symbol_name) {
return true;
}
if arg.starts_with("qw") {
let content = arg
.trim_start_matches("qw")
.trim_start_matches(|c: char| "([{/<|!".contains(c))
.trim_end_matches(|c: char| ")]}/|!>".contains(c));
return content
.split_whitespace()
.any(|tok| tok == symbol_name || tag_imports_symbol(module, tok, symbol_name));
}
let bare = arg.trim().trim_matches('\'').trim_matches('"').trim();
bare == symbol_name || tag_imports_symbol(module, bare, symbol_name)
})
}
fn find_import_source(ast: &Node, symbol_name: &str) -> Option<String> {
fn require_module_name(node: &Node) -> Option<String> {
let args = match &node.kind {
NodeKind::FunctionCall { name, args } if name == "require" => args,
_ => return None,
};
let arg = args.first()?;
match &arg.kind {
NodeKind::Identifier { name } => Some(name.clone()),
NodeKind::String { value, .. } => {
let cleaned = value.trim_matches('\'').trim_matches('"').trim();
let module = cleaned.trim_end_matches(".pm").replace('/', "::");
Some(module)
}
_ => None,
}
}
fn import_call_exports(
method_node: &Node,
expected_module: &str,
symbol: &str,
aliases: &std::collections::HashMap<String, String>,
) -> bool {
let (object, method, args) = match &method_node.kind {
NodeKind::MethodCall { object, method, args } => (object, method, args),
_ => return false,
};
if method != "import" {
return false;
}
let obj_name = match &object.kind {
NodeKind::Identifier { name } => Some(name.as_str()),
NodeKind::Variable { name, .. } => aliases.get(name).map(String::as_str),
_ => return false,
};
let Some(obj_name) = obj_name else {
return false;
};
if obj_name != expected_module {
return false;
}
if args.is_empty() {
return false;
}
for arg in args {
if arg_node_matches_symbol(arg, expected_module, symbol) {
return true;
}
}
false
}
fn arg_node_matches_symbol(arg: &Node, module: &str, symbol: &str) -> bool {
match &arg.kind {
NodeKind::String { value, .. } => {
let bare = value.trim_matches('\'').trim_matches('"');
bare == symbol || tag_imports_symbol(module, bare, symbol)
}
NodeKind::Identifier { name } => {
if name == symbol {
return true;
}
if name.starts_with("qw") {
let content = name
.trim_start_matches("qw")
.trim_start_matches(|c: char| "([{/<|!".contains(c))
.trim_end_matches(|c: char| ")]}/|!>".contains(c));
return content
.split_whitespace()
.any(|tok| tok == symbol || tag_imports_symbol(module, tok, symbol));
}
false
}
NodeKind::ArrayLiteral { elements } => {
elements.iter().any(|el| arg_node_matches_symbol(el, module, symbol))
}
_ => false,
}
}
fn module_runtime_alias(expr: &Node) -> Option<(String, String)> {
let (alias_name, call_node) = match &expr.kind {
NodeKind::Assignment { lhs, rhs, op } if op == "=" => {
let NodeKind::Variable { name, .. } = &lhs.kind else {
return None;
};
(name.as_str(), rhs.as_ref())
}
NodeKind::VariableDeclaration { variable, initializer: Some(rhs), .. } => {
let NodeKind::Variable { name, .. } = &variable.kind else {
return None;
};
(name.as_str(), rhs.as_ref())
}
_ => return None,
};
let NodeKind::FunctionCall { name, args } = &call_node.kind else {
return None;
};
if !matches!(
name.as_str(),
"use_module"
| "require_module"
| "Module::Runtime::use_module"
| "Module::Runtime::require_module"
) {
return None;
}
let first = args.first()?;
let NodeKind::String { value, .. } = &first.kind else {
return None;
};
let module = value.trim_matches('\'').trim_matches('"').trim();
if module.is_empty() {
return None;
}
Some((alias_name.to_string(), module.to_string()))
}
fn inner_expr(node: &Node) -> &Node {
if let NodeKind::ExpressionStatement { expression } = &node.kind {
expression.as_ref()
} else {
node
}
}
fn scan_statements_for_require_import(stmts: &[Node], symbol: &str) -> Option<String> {
let mut required_modules: Vec<String> =
stmts.iter().filter_map(|s| require_module_name(inner_expr(s))).collect();
let mut aliases: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
for stmt in stmts {
if let Some((alias, module)) = module_runtime_alias(inner_expr(stmt)) {
aliases.insert(alias, module.clone());
if !required_modules.contains(&module) {
required_modules.push(module);
}
}
}
if required_modules.is_empty() {
return None;
}
for stmt in stmts {
let expr = inner_expr(stmt);
for module in &required_modules {
if import_call_exports(expr, module, symbol, &aliases) {
return Some(module.clone());
}
}
}
None
}
fn find(node: &Node, name: &str) -> Option<String> {
if let NodeKind::Use { module, args, .. } = &node.kind {
if NON_IMPORT_PRAGMAS.contains(&module.as_str()) {
} else {
for arg in args {
if arg == name {
return Some(module.clone());
}
if tag_imports_symbol(module, arg, name) {
return Some(module.clone());
}
if arg.starts_with("qw") {
let content = arg
.trim_start_matches("qw")
.trim_start_matches(|c: char| "([{/<|!".contains(c))
.trim_end_matches(|c: char| ")]}/|!>".contains(c));
for import_token in content.split_whitespace() {
if import_token == name
|| tag_imports_symbol(module, import_token, name)
{
return Some(module.clone());
}
}
} else {
let bare = arg.trim().trim_matches('\'').trim_matches('"').trim();
if bare == name {
return Some(module.clone());
}
if tag_imports_symbol(module, bare, name) {
return Some(module.clone());
}
}
}
}
}
let stmts = match &node.kind {
NodeKind::Program { statements } => Some(statements.as_slice()),
NodeKind::Block { statements } => Some(statements.as_slice()),
_ => None,
};
if let Some(statements) = stmts {
if let Some(module) = scan_statements_for_require_import(statements, name) {
return Some(module);
}
}
for child in get_node_children(node) {
if let Some(module) = find(child, name) {
return Some(module);
}
}
None
}
find(ast, symbol_name)
}
fn plack_builder_middleware_symbol(path: &[&Node], offset: usize) -> Option<SymbolKey> {
let has_builder = path.iter().any(|ancestor| {
matches!(ancestor.kind, NodeKind::FunctionCall { ref name, .. } if name == "builder")
});
if !has_builder {
return None;
}
let block = path.iter().rev().find_map(|ancestor| {
if let NodeKind::Block { statements } = &ancestor.kind {
Some(statements)
} else {
None
}
})?;
for statement in block {
let NodeKind::ExpressionStatement { expression } = &statement.kind else {
continue;
};
let NodeKind::FunctionCall { name, args } = &expression.kind else {
continue;
};
if name != "enable" {
continue;
}
let Some(first) = args.first() else {
continue;
};
if offset < first.location.start || offset > first.location.end {
continue;
}
let raw_name = match &first.kind {
NodeKind::String { value, .. } => normalize_symbol_name(value)?,
NodeKind::Identifier { name } => name.clone(),
_ => continue,
};
let middleware_name = if raw_name.contains("::") {
raw_name
} else {
format!("Plack::Middleware::{raw_name}")
};
return Some(SymbolKey {
pkg: middleware_name.clone().into(),
name: middleware_name.into(),
sigil: None,
kind: SymKind::Pack,
});
}
None
}
fn looks_like_package_name(name: &str) -> bool {
name.contains("::") || name.chars().next().is_some_and(|ch| ch.is_ascii_uppercase())
}
fn infer_receiver_package(
object: &Node,
current_pkg: &str,
receiver_packages: &std::collections::HashMap<String, String>,
) -> Option<String> {
if let NodeKind::Identifier { name } = &object.kind {
return Some(name.clone());
}
if let Some(name) = node_variable_name(object) {
if let Some(package_name) = receiver_packages.get(name) {
return Some(package_name.clone());
}
if matches!(name, "self" | "this" | "class") {
return Some(current_pkg.to_string());
}
if looks_like_package_name(name) {
return Some(name.to_string());
}
}
None
}
fn infer_constructor_package(
rhs: &Node,
current_pkg: &str,
receiver_packages: &std::collections::HashMap<String, String>,
) -> Option<String> {
match &rhs.kind {
NodeKind::MethodCall { method, object, .. } if method == "new" => {
infer_receiver_package(object, current_pkg, receiver_packages)
}
NodeKind::FunctionCall { name, .. } => {
name.rsplit_once("::").map(|(package_name, _)| package_name.to_string())
}
_ => None,
}
}
fn record_receiver_assignment(
node: &Node,
offset: usize,
current_pkg: &str,
receiver_packages: &mut std::collections::HashMap<String, String>,
) {
if node.location.start > offset {
return;
}
if node.location.end <= offset {
match &node.kind {
NodeKind::VariableDeclaration { variable, initializer, .. } => {
if let (Some(variable_name), Some(initializer)) =
(node_variable_name(variable), initializer.as_ref())
{
if let Some(package_name) =
infer_constructor_package(initializer, current_pkg, receiver_packages)
{
receiver_packages.insert(variable_name.to_string(), package_name);
}
}
}
NodeKind::Assignment { lhs, rhs, .. } => {
if let Some(variable_name) = node_variable_name(lhs) {
if let Some(package_name) =
infer_constructor_package(rhs, current_pkg, receiver_packages)
{
receiver_packages.insert(variable_name.to_string(), package_name);
}
}
}
_ => {}
}
}
for child in get_node_children(node) {
if child.location.start <= offset {
record_receiver_assignment(child, offset, current_pkg, receiver_packages);
}
}
}
let (path, node) = find_symbol_node_at_offset(ast, offset)?;
if let Some(symbol_key) = plack_builder_middleware_symbol(&path, offset) {
return Some(symbol_key);
}
match &node.kind {
NodeKind::Variable { sigil, name } => {
let sigil_char = sigil.chars().next();
Some(SymbolKey {
pkg: current_pkg.into(),
name: name.clone().into(),
sigil: sigil_char,
kind: SymKind::Var,
})
}
NodeKind::FunctionCall { name, .. } => {
let (pkg, bare) = if let Some(idx) = name.rfind("::") {
(name[..idx].to_string(), name[idx + 2..].to_string())
} else {
(
find_import_source(ast, name).unwrap_or_else(|| current_pkg.to_string()),
name.clone(),
)
};
Some(SymbolKey { pkg: pkg.into(), name: bare.into(), sigil: None, kind: SymKind::Sub })
}
NodeKind::Subroutine { name: Some(name), .. } => {
let (pkg, bare) = if let Some(idx) = name.rfind("::") {
(&name[..idx], &name[idx + 2..])
} else {
(current_pkg, name.as_str())
};
Some(SymbolKey { pkg: pkg.into(), name: bare.into(), sigil: None, kind: SymKind::Sub })
}
NodeKind::MethodCall { object, method, .. } => {
let mut receiver_packages = std::collections::HashMap::new();
record_receiver_assignment(ast, offset, current_pkg, &mut receiver_packages);
let pkg = infer_receiver_package(object, current_pkg, &receiver_packages)
.unwrap_or_else(|| current_pkg.to_string());
Some(SymbolKey {
pkg: pkg.into(),
name: method.clone().into(),
sigil: None,
kind: SymKind::Sub,
})
}
NodeKind::Use { module, args, .. } => {
if !NON_IMPORT_PRAGMAS.contains(&module.as_str())
&& !source_text.is_empty()
&& offset >= node.location.start
&& offset <= node.location.end
{
let rel_offset = offset.saturating_sub(node.location.start);
if let Some(stmt_text) = source_text.get(node.location.start..node.location.end)
&& let Some(token) = token_at_offset_in_text(stmt_text, rel_offset)
&& token != *module
&& token != "use"
&& use_args_import_symbol(module, args, &token)
{
return Some(SymbolKey {
pkg: module.clone().into(),
name: token.into(),
sigil: None,
kind: SymKind::Sub,
});
}
}
Some(SymbolKey {
pkg: module.clone().into(),
name: module.clone().into(),
sigil: None,
kind: SymKind::Pack,
})
}
_ => None,
}
}
pub fn symbol_at_cursor_with_source(
ast: &Node,
offset: usize,
current_pkg: &str,
source_text: &str,
) -> Option<SymbolKey> {
symbol_at_cursor_internal(ast, offset, current_pkg, source_text)
}
pub fn symbol_at_cursor(ast: &Node, offset: usize, current_pkg: &str) -> Option<SymbolKey> {
symbol_at_cursor_internal(ast, offset, current_pkg, "")
}
pub fn current_package_at(ast: &Node, offset: usize) -> &str {
fn scan<'a>(node: &'a Node, offset: usize, last: &mut Option<&'a str>) {
if let NodeKind::Package { name, .. } = &node.kind {
if node.location.start <= offset {
*last = Some(name.as_str());
}
}
for child in get_node_children(node) {
if child.location.start <= offset {
scan(child, offset, last);
}
}
}
let mut last_pkg: Option<&str> = None;
scan(ast, offset, &mut last_pkg);
last_pkg.unwrap_or("main")
}
pub fn find_node_at_offset(node: &Node, offset: usize) -> Option<&Node> {
if offset < node.location.start || offset > node.location.end {
return None;
}
let children = get_node_children(node);
for child in children {
if let Some(found) = find_node_at_offset(child, offset) {
return Some(found);
}
}
Some(node)
}
pub fn get_node_children(node: &Node) -> Vec<&Node> {
node.children()
}