use std::collections::{HashMap, HashSet};
use tree_sitter::Point;
use crate::module_index::ModuleIndex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Span {
pub start: Point,
pub end: Point,
}
pub(crate) fn extract_call_name(node: tree_sitter::Node, source: &[u8]) -> Option<String> {
match node.kind() {
"method_call_expression" => node
.child_by_field_name("method")
.and_then(|n| n.utf8_text(source).ok())
.map(|s| s.to_string()),
"function_call_expression" | "ambiguous_function_call_expression" => node
.child_by_field_name("function")
.and_then(|n| n.utf8_text(source).ok())
.map(|s| s.to_string()),
_ => None,
}
}
pub(crate) fn node_to_span(node: tree_sitter::Node) -> Span {
Span {
start: node.start_position(),
end: node.end_position(),
}
}
#[derive(Debug, Clone)]
pub struct FoldRange {
pub start_line: usize,
pub end_line: usize,
pub kind: FoldKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum FoldKind {
Region,
Comment,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ScopeId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SymbolId(pub u32);
#[derive(Debug, Clone)]
pub struct Scope {
pub id: ScopeId,
pub parent: Option<ScopeId>,
pub kind: ScopeKind,
pub span: Span,
pub package: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum ScopeKind {
File,
Package,
Class { name: String },
Sub { name: String },
Method { name: String },
Block,
ForLoop { var: String },
}
#[derive(Debug, Clone)]
pub struct Symbol {
pub id: SymbolId,
pub name: String,
pub kind: SymKind,
pub span: Span,
pub selection_span: Span,
pub scope: ScopeId,
pub package: Option<String>,
pub detail: SymbolDetail,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymKind {
Variable,
Sub,
Method,
Package,
Class,
Module,
Field,
HashKeyDef,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum SymbolDetail {
Variable {
sigil: char,
decl_kind: DeclKind,
},
Sub {
params: Vec<ParamInfo>,
is_method: bool,
return_type: Option<InferredType>,
doc: Option<String>,
},
Class {
parent: Option<String>,
roles: Vec<String>,
fields: Vec<FieldDetail>,
},
Field {
sigil: char,
attributes: Vec<String>,
},
HashKeyDef {
owner: HashKeyOwner,
is_dynamic: bool,
},
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeclKind {
My,
Our,
State,
Field,
Param,
ForVar,
}
#[derive(Debug, Clone)]
pub struct ParamInfo {
pub name: String,
pub default: Option<String>,
pub is_slurpy: bool,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct FieldDetail {
pub name: String,
pub sigil: char,
pub attributes: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct Ref {
pub kind: RefKind,
pub span: Span,
pub scope: ScopeId,
pub target_name: String,
pub access: AccessKind,
pub resolves_to: Option<SymbolId>,
}
#[derive(Debug)]
pub enum RenameKind {
Variable,
Function(String),
Package(String),
Method(String),
HashKey(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum RefKind {
Variable,
FunctionCall,
MethodCall {
invocant: String,
invocant_span: Option<Span>,
method_name_span: Span,
},
PackageRef,
HashKeyAccess {
var_text: String,
owner: Option<HashKeyOwner>,
},
ContainerAccess,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessKind {
Read,
Write,
Declaration,
}
#[derive(Debug, Clone)]
pub struct TypeConstraint {
pub variable: String,
pub scope: ScopeId,
pub constraint_span: Span,
pub inferred_type: InferredType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InferredType {
ClassName(String),
FirstParam { package: String },
HashRef,
ArrayRef,
CodeRef,
Regexp,
Numeric,
String,
}
impl InferredType {
pub fn class_name(&self) -> Option<&str> {
match self {
InferredType::ClassName(name) => Some(name.as_str()),
InferredType::FirstParam { package } => Some(package.as_str()),
_ => None,
}
}
pub fn is_object(&self) -> bool {
self.class_name().is_some()
}
}
pub fn resolve_return_type(return_types: &[InferredType]) -> Option<InferredType> {
if return_types.is_empty() {
return None;
}
let first = &return_types[0];
if return_types.iter().all(|t| t == first) {
return Some(first.clone());
}
let mut object = None;
for t in return_types {
if t.is_object() {
object = Some(t.clone());
} else if *t != InferredType::HashRef {
return None;
}
}
object
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum HashKeyOwner {
Class(String),
Variable { name: String, def_scope: ScopeId },
Sub(String),
}
#[derive(Debug, Clone)]
pub struct CallBinding {
pub variable: String,
pub func_name: String,
pub scope: ScopeId,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct MethodCallBinding {
pub variable: String,
pub invocant_var: String,
pub method_name: String,
pub scope: ScopeId,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct Import {
pub module_name: String,
pub imported_symbols: Vec<String>,
pub span: Span,
pub qw_close_paren: Option<Point>,
}
pub struct OutlineSymbol {
pub name: String,
pub detail: Option<String>,
pub kind: SymKind,
pub span: Span,
pub selection_span: Span,
pub children: Vec<OutlineSymbol>,
}
pub const TOK_VARIABLE: u32 = 0;
pub const TOK_PARAMETER: u32 = 1;
pub const TOK_FUNCTION: u32 = 2;
pub const TOK_METHOD: u32 = 3;
pub const TOK_MACRO: u32 = 4;
pub const TOK_PROPERTY: u32 = 5;
pub const TOK_NAMESPACE: u32 = 6;
#[allow(dead_code)] pub const TOK_REGEXP: u32 = 7;
#[allow(dead_code)] pub const TOK_ENUM_MEMBER: u32 = 8;
pub const TOK_KEYWORD: u32 = 9;
pub const MOD_DECLARATION: u32 = 0;
pub const MOD_READONLY: u32 = 1;
pub const MOD_MODIFICATION: u32 = 2;
pub const MOD_DEFAULT_LIBRARY: u32 = 3;
#[allow(dead_code)] pub const MOD_DEPRECATED: u32 = 4;
#[allow(dead_code)] pub const MOD_STATIC: u32 = 5;
pub const MOD_SCALAR: u32 = 6;
pub const MOD_ARRAY: u32 = 7;
pub const MOD_HASH: u32 = 8;
#[derive(Debug, Clone)]
pub struct PerlSemanticToken {
pub span: Span,
pub token_type: u32,
pub modifiers: u32,
}
fn sigil_modifier(sigil: char) -> u32 {
match sigil {
'@' => 1 << MOD_ARRAY,
'%' => 1 << MOD_HASH,
_ => 1 << MOD_SCALAR,
}
}
pub struct FileAnalysis {
pub scopes: Vec<Scope>,
pub symbols: Vec<Symbol>,
pub refs: Vec<Ref>,
pub type_constraints: Vec<TypeConstraint>,
pub fold_ranges: Vec<FoldRange>,
pub imports: Vec<Import>,
pub call_bindings: Vec<CallBinding>,
pub method_call_bindings: Vec<MethodCallBinding>,
pub package_parents: HashMap<String, Vec<String>>,
pub imported_return_types: HashMap<String, InferredType>,
pub framework_imports: HashSet<String>,
pub export: Vec<String>,
pub export_ok: Vec<String>,
base_type_constraint_count: usize,
base_symbol_count: usize,
scope_starts: Vec<(Point, ScopeId)>, symbols_by_name: HashMap<String, Vec<SymbolId>>,
symbols_by_scope: HashMap<ScopeId, Vec<SymbolId>>,
refs_by_name: HashMap<String, Vec<usize>>,
type_constraints_by_var: HashMap<String, Vec<usize>>,
}
impl FileAnalysis {
pub fn new(
scopes: Vec<Scope>,
symbols: Vec<Symbol>,
refs: Vec<Ref>,
type_constraints: Vec<TypeConstraint>,
fold_ranges: Vec<FoldRange>,
imports: Vec<Import>,
call_bindings: Vec<CallBinding>,
package_parents: HashMap<String, Vec<String>>,
method_call_bindings: Vec<MethodCallBinding>,
framework_imports: HashSet<String>,
export: Vec<String>,
export_ok: Vec<String>,
) -> Self {
let mut fa = FileAnalysis {
scopes,
symbols,
refs,
type_constraints,
fold_ranges,
imports,
call_bindings,
method_call_bindings,
package_parents,
imported_return_types: HashMap::new(),
framework_imports,
export,
export_ok,
base_type_constraint_count: 0,
base_symbol_count: 0,
scope_starts: Vec::new(),
symbols_by_name: HashMap::new(),
symbols_by_scope: HashMap::new(),
refs_by_name: HashMap::new(),
type_constraints_by_var: HashMap::new(),
};
fa.build_indices();
fa.resolve_method_call_types(None); fa.base_type_constraint_count = fa.type_constraints.len();
fa.base_symbol_count = fa.symbols.len();
fa
}
fn build_indices(&mut self) {
self.scope_starts = self.scopes.iter()
.map(|s| (s.span.start, s.id))
.collect();
self.scope_starts.sort_by_key(|(p, _)| (p.row, p.column));
for sym in &self.symbols {
self.symbols_by_name
.entry(sym.name.clone())
.or_default()
.push(sym.id);
}
for sym in &self.symbols {
self.symbols_by_scope
.entry(sym.scope)
.or_default()
.push(sym.id);
}
for (i, r) in self.refs.iter().enumerate() {
self.refs_by_name
.entry(r.target_name.clone())
.or_default()
.push(i);
}
for (i, tc) in self.type_constraints.iter().enumerate() {
self.type_constraints_by_var
.entry(tc.variable.clone())
.or_default()
.push(i);
}
}
pub fn scope_at(&self, point: Point) -> Option<ScopeId> {
let mut best: Option<(ScopeId, usize)> = None; for scope in &self.scopes {
if contains_point(&scope.span, point) {
let size = span_size(&scope.span);
if best.is_none() || size <= best.unwrap().1 {
best = Some((scope.id, size));
}
}
}
best.map(|(id, _)| id)
}
pub fn scope_chain(&self, start: ScopeId) -> Vec<ScopeId> {
let mut chain = Vec::new();
let mut current = Some(start);
while let Some(id) = current {
chain.push(id);
current = self.scopes[id.0 as usize].parent;
}
chain
}
pub fn scope(&self, id: ScopeId) -> &Scope {
&self.scopes[id.0 as usize]
}
pub fn symbol(&self, id: SymbolId) -> &Symbol {
&self.symbols[id.0 as usize]
}
pub fn visible_symbols(&self, point: Point) -> Vec<&Symbol> {
let scope = match self.scope_at(point) {
Some(s) => s,
None => return Vec::new(),
};
let chain = self.scope_chain(scope);
let mut result = Vec::new();
for scope_id in &chain {
if let Some(sym_ids) = self.symbols_by_scope.get(scope_id) {
for sid in sym_ids {
let sym = &self.symbols[sid.0 as usize];
if sym.span.start <= point || matches!(sym.kind, SymKind::Sub | SymKind::Method | SymKind::Package | SymKind::Class) {
result.push(sym);
}
}
}
}
result
}
pub fn resolve_variable(&self, name: &str, point: Point) -> Option<&Symbol> {
let scope = self.scope_at(point)?;
let chain = self.scope_chain(scope);
for scope_id in &chain {
if let Some(sym_ids) = self.symbols_by_scope.get(scope_id) {
for sid in sym_ids {
let sym = &self.symbols[sid.0 as usize];
if sym.name == name
&& matches!(sym.kind, SymKind::Variable | SymKind::Field)
&& sym.span.start <= point
{
return Some(sym);
}
}
}
}
None
}
pub fn inferred_type(&self, var_name: &str, point: Point) -> Option<&InferredType> {
let constraints = self.type_constraints_by_var.get(var_name)?;
let mut best: Option<&TypeConstraint> = None;
for &idx in constraints {
let tc = &self.type_constraints[idx];
let scope = &self.scopes[tc.scope.0 as usize];
if contains_point(&scope.span, point) && tc.constraint_span.start <= point {
if best.is_none() || tc.constraint_span.start > best.unwrap().constraint_span.start {
best = Some(tc);
}
}
}
best.map(|tc| &tc.inferred_type)
}
pub fn sub_return_type_local(&self, name: &str) -> Option<&InferredType> {
for sym in &self.symbols {
if sym.name == name && matches!(sym.kind, SymKind::Sub | SymKind::Method) {
if let SymbolDetail::Sub { ref return_type, .. } = sym.detail {
if return_type.is_some() {
return return_type.as_ref();
}
}
}
}
None
}
pub fn sub_return_type(&self, name: &str) -> Option<&InferredType> {
self.sub_return_type_local(name)
.or_else(|| self.imported_return_types.get(name))
}
#[cfg(test)]
pub fn enrich_imported_types(&mut self, imported_returns: HashMap<String, InferredType>) {
self.enrich_imported_types_with_keys(imported_returns, HashMap::new(), None);
}
pub fn enrich_imported_types_with_keys(
&mut self,
imported_returns: HashMap<String, InferredType>,
imported_hash_keys: HashMap<String, Vec<String>>,
module_index: Option<&ModuleIndex>,
) {
self.type_constraints.truncate(self.base_type_constraint_count);
self.symbols.truncate(self.base_symbol_count);
let mut new_constraints = Vec::new();
for binding in &self.call_bindings {
if self.sub_return_type_local(&binding.func_name).is_some()
|| builtin_return_type(&binding.func_name).is_some()
{
continue;
}
if let Some(rt) = imported_returns.get(&binding.func_name) {
new_constraints.push(TypeConstraint {
variable: binding.variable.clone(),
scope: binding.scope,
constraint_span: binding.span,
inferred_type: rt.clone(),
});
}
}
self.type_constraints.extend(new_constraints);
self.imported_return_types = imported_returns;
for (func_name, keys) in &imported_hash_keys {
let owner = HashKeyOwner::Sub(func_name.clone());
for key_name in keys {
let id = SymbolId(self.symbols.len() as u32);
let zero_span = Span {
start: Point { row: 0, column: 0 },
end: Point { row: 0, column: 0 },
};
self.symbols.push(Symbol {
id,
name: key_name.clone(),
kind: SymKind::HashKeyDef,
span: zero_span,
selection_span: zero_span,
scope: ScopeId(0),
package: None,
detail: SymbolDetail::HashKeyDef {
owner: owner.clone(),
is_dynamic: false,
},
});
}
}
self.resolve_method_call_types(module_index);
self.rebuild_enrichment_indices();
}
fn resolve_method_call_types(&mut self, module_index: Option<&ModuleIndex>) {
let bindings = self.method_call_bindings.clone();
for binding in &bindings {
if self.inferred_type(&binding.variable, binding.span.start).is_some() {
continue;
}
let class_name = self.resolve_invocant_class(
&binding.invocant_var,
binding.scope,
binding.span.start,
);
if let Some(cn) = class_name {
if let Some(rt) = self.find_method_return_type(&cn, &binding.method_name, module_index, None) {
self.type_constraints.push(TypeConstraint {
variable: binding.variable.clone(),
scope: binding.scope,
constraint_span: binding.span,
inferred_type: rt,
});
let idx = self.type_constraints.len() - 1;
self.type_constraints_by_var
.entry(binding.variable.clone())
.or_default()
.push(idx);
}
}
}
}
fn rebuild_enrichment_indices(&mut self) {
self.type_constraints_by_var.clear();
for (i, tc) in self.type_constraints.iter().enumerate() {
self.type_constraints_by_var
.entry(tc.variable.clone())
.or_default()
.push(i);
}
self.symbols_by_name.clear();
self.symbols_by_scope.clear();
for sym in &self.symbols {
self.symbols_by_name
.entry(sym.name.clone())
.or_default()
.push(sym.id);
self.symbols_by_scope
.entry(sym.scope)
.or_default()
.push(sym.id);
}
}
pub fn resolve_expression_type(
&self,
node: tree_sitter::Node,
source: &[u8],
module_index: Option<&ModuleIndex>,
) -> Option<InferredType> {
let point = node.start_position();
match node.kind() {
"scalar" => {
let text = node.utf8_text(source).ok()?;
self.inferred_type(text, point).cloned()
}
"package" | "bareword" => {
let text = node.utf8_text(source).ok()?;
Some(InferredType::ClassName(text.to_string()))
}
"method_call_expression" => {
let invocant_node = node.child_by_field_name("invocant")?;
let invocant_type = self.resolve_expression_type(invocant_node, source, module_index)?;
let class_name = invocant_type.class_name()?;
let method_name = node.child_by_field_name("method")?;
let method_text = method_name.utf8_text(source).ok()?;
if method_text == "new" {
return Some(InferredType::ClassName(class_name.to_string()));
}
let arg_count = self.count_call_args(node);
self.find_method_return_type(class_name, method_text, module_index, Some(arg_count))
}
"function_call_expression" | "ambiguous_function_call_expression" => {
let func_node = node.child_by_field_name("function")?;
let name = func_node.utf8_text(source).ok()?;
self.sub_return_type(name).cloned()
.or_else(|| builtin_return_type(name))
}
"func1op_call_expression" | "func0op_call_expression" => {
let name = node.child(0)?.utf8_text(source).ok()?;
builtin_return_type(name)
}
"hash_element_expression" => {
let base = node.named_child(0)?;
self.resolve_expression_type(base, source, module_index)
}
_ => None,
}
}
pub fn resolve_hash_owner_from_tree(
&self,
tree: &tree_sitter::Tree,
source: &[u8],
point: Point,
module_index: Option<&ModuleIndex>,
) -> Option<HashKeyOwner> {
let hash_elem = tree.root_node()
.descendant_for_point_range(point, point)
.and_then(|n| find_ancestor(n, "hash_element_expression"))?;
let base = hash_elem.named_child(0)?;
let ty = self.resolve_expression_type(base, source, module_index)?;
let sub_name = match base.kind() {
"method_call_expression" => base.child_by_field_name("method")
.and_then(|n| n.utf8_text(source).ok())
.map(|s| s.to_string()),
"function_call_expression" | "ambiguous_function_call_expression" =>
base.child_by_field_name("function")
.and_then(|n| n.utf8_text(source).ok())
.map(|s| s.to_string()),
_ => None,
};
if let Some(name) = sub_name {
let sub_owner = HashKeyOwner::Sub(name);
if !self.hash_key_defs_for_owner(&sub_owner).is_empty() {
return Some(sub_owner);
}
}
if let Some(cn) = ty.class_name() {
return Some(HashKeyOwner::Class(cn.to_string()));
}
None
}
fn call_arg_key_at(&self, tree: &tree_sitter::Tree, source: &[u8], point: Point) -> Option<String> {
let node = tree.root_node().descendant_for_point_range(point, point)?;
let key_node = match node.kind() {
"autoquoted_bareword" => node,
"string_literal" | "interpolated_string_literal" => node,
_ => {
let parent = node.parent()?;
if matches!(parent.kind(), "string_literal" | "interpolated_string_literal") {
parent
} else {
return None;
}
}
};
let text = key_node.utf8_text(source).ok()?;
if (text.starts_with('\'') || text.starts_with('"')) && text.len() >= 2 {
Some(text[1..text.len()-1].to_string())
} else if text.starts_with('\'') || text.starts_with('"') {
None
} else {
Some(text.to_string())
}
}
pub(crate) fn find_method_return_type(
&self,
class_name: &str,
method_name: &str,
module_index: Option<&ModuleIndex>,
arg_count: Option<usize>,
) -> Option<InferredType> {
match self.resolve_method_in_ancestors(class_name, method_name, module_index) {
Some(MethodResolution::Local { sym_id, .. }) => {
let candidates: Vec<_> = self.symbols_named(method_name).iter()
.filter(|&&sid| {
let s = self.symbol(sid);
matches!(s.kind, SymKind::Sub | SymKind::Method)
&& self.symbol_in_class(sid, class_name)
})
.copied()
.collect();
if candidates.len() <= 1 || arg_count.is_none() {
if let SymbolDetail::Sub { ref return_type, .. } = self.symbol(sym_id).detail {
return return_type.clone();
}
return None;
}
let target = arg_count.unwrap();
for sid in &candidates {
if let SymbolDetail::Sub { ref params, ref return_type, .. } = self.symbol(*sid).detail {
if params.len() == target {
return return_type.clone();
}
}
}
if let SymbolDetail::Sub { ref return_type, .. } = self.symbol(sym_id).detail {
return_type.clone()
} else {
None
}
}
Some(MethodResolution::CrossFile { ref class }) => {
module_index.and_then(|idx| {
let exports = idx.get_exports_cached(class)?;
let sub = exports.subs.get(method_name)?;
let target = match arg_count {
Some(n) if !sub.overloads.is_empty() => n,
_ => return sub.return_type.clone(),
};
if sub.params.len() == target {
return sub.return_type.clone();
}
for overload in &sub.overloads {
if overload.params.len() == target {
return overload.return_type.clone();
}
}
sub.return_type.clone()
})
}
None => None,
}
}
pub(crate) fn count_call_args(&self, node: tree_sitter::Node) -> usize {
let args = match node.child_by_field_name("arguments") {
Some(a) => a,
None => return 0,
};
match args.kind() {
"parenthesized_expression" | "list_expression" => args.named_child_count(),
_ => 1, }
}
fn find_param_field(&self, class_name: &str, key: &str) -> Option<Span> {
for sym in &self.symbols {
if matches!(sym.kind, SymKind::Field) {
if let SymbolDetail::Field { ref attributes, .. } = sym.detail {
if attributes.contains(&"param".to_string())
&& self.symbol_in_class(sym.id, class_name)
{
let bare = sym.name
.trim_start_matches('$')
.trim_start_matches('@')
.trim_start_matches('%');
if bare == key {
return Some(sym.selection_span);
}
}
}
}
}
None
}
fn method_detail(
&self,
class_name: &str,
method_name: &str,
defining_class: Option<&str>,
module_index: Option<&ModuleIndex>,
) -> String {
let base = if let Some(dc) = defining_class {
if dc != class_name {
format!("{} (from {})", class_name, dc)
} else {
class_name.to_string()
}
} else {
class_name.to_string()
};
if let Some(ref rt) = self.find_method_return_type(class_name, method_name, module_index, None) {
format!("{} → {}", base, format_inferred_type(rt))
} else {
base
}
}
pub fn complete_methods_for_class(
&self,
class_name: &str,
module_index: Option<&ModuleIndex>,
) -> Vec<CompletionCandidate> {
let mut candidates = Vec::new();
let mut seen_names: HashSet<String> = HashSet::new();
for sym in &self.symbols {
if matches!(sym.kind, SymKind::Class) && sym.name == class_name {
candidates.push(CompletionCandidate {
label: "new".to_string(),
kind: SymKind::Method,
detail: Some(self.method_detail(class_name, "new", None, module_index)),
insert_text: None,
sort_priority: PRIORITY_LOCAL,
additional_edits: vec![],
});
seen_names.insert("new".to_string());
break;
}
}
self.collect_ancestor_methods(class_name, class_name, module_index, &mut candidates, &mut seen_names, 0);
candidates
}
fn collect_ancestor_methods(
&self,
original_class: &str,
class_name: &str,
module_index: Option<&ModuleIndex>,
candidates: &mut Vec<CompletionCandidate>,
seen_names: &mut HashSet<String>,
depth: usize,
) {
if depth > 20 {
return;
}
for sym in &self.symbols {
if matches!(sym.kind, SymKind::Sub | SymKind::Method) {
if self.symbol_in_class(sym.id, class_name) && !seen_names.contains(&sym.name) {
seen_names.insert(sym.name.clone());
let defining = if class_name != original_class { Some(class_name) } else { None };
candidates.push(CompletionCandidate {
label: sym.name.clone(),
kind: sym.kind,
detail: Some(self.method_detail(original_class, &sym.name, defining, module_index)),
insert_text: None,
sort_priority: PRIORITY_LOCAL,
additional_edits: vec![],
});
}
}
}
if let Some(parents) = self.package_parents.get(class_name) {
for parent in parents {
self.collect_ancestor_methods(
original_class, parent, module_index, candidates, seen_names, depth + 1,
);
}
}
if let Some(idx) = module_index {
if let Some(exports) = idx.get_exports_cached(class_name) {
for (name, sub_info) in &exports.subs {
if !seen_names.contains(name) {
seen_names.insert(name.clone());
let kind = if sub_info.is_method { SymKind::Method } else { SymKind::Sub };
let defining = if class_name != original_class { Some(class_name) } else { None };
let detail = self.method_detail(original_class, name, defining, module_index);
candidates.push(CompletionCandidate {
label: name.clone(),
kind,
detail: Some(detail),
insert_text: None,
sort_priority: PRIORITY_LOCAL,
additional_edits: vec![],
});
}
}
}
let cross_parents = idx.parents_cached(class_name);
for parent in &cross_parents {
if !self.package_parents.contains_key(parent.as_str()) {
self.collect_ancestor_methods(
original_class, parent, module_index, candidates, seen_names, depth + 1,
);
}
}
}
}
#[allow(dead_code)]
pub fn package_at(&self, point: Point) -> Option<&str> {
let scope = self.scope_at(point)?;
let chain = self.scope_chain(scope);
for scope_id in &chain {
let s = &self.scopes[scope_id.0 as usize];
if let Some(ref pkg) = s.package {
return Some(pkg.as_str());
}
}
None
}
pub fn symbols_named(&self, name: &str) -> &[SymbolId] {
self.symbols_by_name.get(name).map(|v| v.as_slice()).unwrap_or(&[])
}
#[allow(dead_code)]
pub fn symbols_in_scope(&self, scope: ScopeId) -> &[SymbolId] {
self.symbols_by_scope.get(&scope).map(|v| v.as_slice()).unwrap_or(&[])
}
#[allow(dead_code)]
pub fn refs_named(&self, name: &str) -> Vec<&Ref> {
self.refs_by_name.get(name)
.map(|idxs| idxs.iter().map(|&i| &self.refs[i]).collect())
.unwrap_or_default()
}
#[allow(dead_code)]
pub fn refs_to(&self, target: SymbolId) -> Vec<&Ref> {
self.refs.iter()
.filter(|r| r.resolves_to == Some(target))
.collect()
}
#[allow(dead_code)]
pub fn hash_keys_for_owner(&self, owner: &HashKeyOwner) -> Vec<&Ref> {
self.refs.iter()
.filter(|r| {
if let RefKind::HashKeyAccess { owner: Some(ref o), .. } = r.kind {
o == owner
} else {
false
}
})
.collect()
}
pub fn hash_key_defs_for_owner(&self, owner: &HashKeyOwner) -> Vec<&Symbol> {
self.symbols.iter()
.filter(|s| {
if let SymbolDetail::HashKeyDef { owner: ref o, .. } = s.detail {
o == owner
} else {
false
}
})
.collect()
}
pub fn ref_at(&self, point: Point) -> Option<&Ref> {
self.refs.iter()
.filter(|r| contains_point(&r.span, point))
.min_by_key(|r| span_size(&r.span))
}
pub fn symbol_at(&self, point: Point) -> Option<&Symbol> {
self.symbols.iter().find(|s| contains_point(&s.selection_span, point))
}
pub fn find_definition(&self, point: Point, tree: Option<&tree_sitter::Tree>, source_bytes: Option<&[u8]>, module_index: Option<&ModuleIndex>) -> Option<Span> {
if let Some(r) = self.ref_at(point) {
match &r.kind {
RefKind::Variable => {
if let Some(sym_id) = r.resolves_to {
return Some(self.symbol(sym_id).selection_span);
}
}
RefKind::FunctionCall => {
for &sid in self.symbols_named(&r.target_name) {
let sym = self.symbol(sid);
if matches!(sym.kind, SymKind::Sub) {
return Some(sym.selection_span);
}
}
}
RefKind::MethodCall { ref invocant, ref invocant_span, .. } => {
let class_name = self.resolve_method_invocant(invocant, invocant_span, r.scope, point, tree, source_bytes, module_index);
if let (Some(t), Some(s), Some(ref cn)) = (tree, source_bytes, &class_name) {
if let Some(key_name) = self.call_arg_key_at(t, s, point) {
let owner = HashKeyOwner::Class(cn.to_string());
for def in self.hash_key_defs_for_owner(&owner) {
if def.name == key_name {
return Some(def.selection_span);
}
}
if let Some(span) = self.find_param_field(cn, &key_name) {
return Some(span);
}
}
}
if let Some(ref cn) = class_name {
match self.resolve_method_in_ancestors(cn, &r.target_name, module_index) {
Some(MethodResolution::Local { sym_id, .. }) => {
return Some(self.symbol(sym_id).selection_span);
}
Some(MethodResolution::CrossFile { .. }) => {
return None;
}
None => {
if self.package_parents.contains_key(cn) {
return None;
}
}
}
if let Some(span) = self.find_package_or_class(cn) {
return Some(span);
}
}
for &sid in self.symbols_named(&r.target_name) {
let sym = self.symbol(sid);
if matches!(sym.kind, SymKind::Sub | SymKind::Method) {
return Some(sym.selection_span);
}
}
}
RefKind::PackageRef => {
return self.find_package_or_class(&r.target_name);
}
RefKind::HashKeyAccess { ref owner, .. } => {
let resolved_owner = owner.clone().or_else(|| {
let tree = tree?;
let source = source_bytes?;
self.resolve_hash_owner_from_tree(tree, source, r.span.start, module_index)
});
if let Some(ref owner) = resolved_owner {
for def in self.hash_key_defs_for_owner(owner) {
if def.name == r.target_name {
return Some(def.selection_span);
}
}
}
}
RefKind::ContainerAccess => {
return self.resolve_variable(&r.target_name, point)
.map(|sym| sym.selection_span);
}
}
}
if let Some(sym) = self.symbol_at(point) {
return Some(sym.selection_span);
}
None
}
pub fn find_references(&self, point: Point, tree: Option<&tree_sitter::Tree>, source_bytes: Option<&[u8]>) -> Vec<Span> {
if let Some((target_id, include_decl)) = self.resolve_target_at(point, tree, source_bytes, None) {
let mut results = self.collect_refs_for_target(target_id, include_decl, tree, source_bytes);
results.sort_by_key(|(s, _)| (s.start.row, s.start.column));
results.dedup_by(|a, b| a.0.start == b.0.start && a.0.end == b.0.end);
results.into_iter().map(|(span, _)| span).collect()
} else {
Vec::new()
}
}
pub fn find_highlights(&self, point: Point, tree: Option<&tree_sitter::Tree>, source_bytes: Option<&[u8]>) -> Vec<(Span, AccessKind)> {
if let Some((target_id, _)) = self.resolve_target_at(point, tree, source_bytes, None) {
let mut results = self.collect_refs_for_target(target_id, true, tree, source_bytes);
results.sort_by_key(|(s, _)| (s.start.row, s.start.column));
results.dedup_by(|a, b| a.0.start == b.0.start && a.0.end == b.0.end);
results
} else {
Vec::new()
}
}
fn collect_refs_for_target(
&self,
target_id: SymbolId,
include_decl: bool,
tree: Option<&tree_sitter::Tree>,
source_bytes: Option<&[u8]>,
) -> Vec<(Span, AccessKind)> {
let sym = self.symbol(target_id);
let mut results: Vec<(Span, AccessKind)> = Vec::new();
if include_decl {
results.push((sym.selection_span, AccessKind::Declaration));
}
for r in &self.refs {
if r.resolves_to == Some(target_id) {
results.push((r.span, r.access));
}
}
if matches!(sym.kind, SymKind::Sub | SymKind::Method | SymKind::Package | SymKind::Class | SymKind::Module) {
for r in &self.refs {
if r.target_name == sym.name && r.resolves_to.is_none() {
match (&r.kind, &sym.kind) {
(RefKind::FunctionCall, SymKind::Sub) => results.push((r.span, r.access)),
(RefKind::MethodCall { .. }, SymKind::Sub | SymKind::Method) => results.push((r.span, r.access)),
(RefKind::PackageRef, SymKind::Package | SymKind::Class | SymKind::Module) => results.push((r.span, r.access)),
_ => {}
}
}
}
}
if let SymbolDetail::HashKeyDef { ref owner, .. } = sym.detail {
for r in &self.refs {
if let RefKind::HashKeyAccess { owner: ref ro, .. } = r.kind {
if r.target_name != sym.name {
continue;
}
let matches = match ro {
Some(ref ro) => ro == owner,
None => {
if let (Some(t), Some(s)) = (tree, source_bytes) {
self.resolve_hash_owner_from_tree(t, s, r.span.start, None)
.as_ref()
.map_or(false, |resolved| resolved == owner)
} else {
false
}
}
};
if matches {
results.push((r.span, r.access));
}
}
}
}
results
}
pub fn hover_info(&self, point: Point, source: &str, tree: Option<&tree_sitter::Tree>, module_index: Option<&ModuleIndex>) -> Option<String> {
if let Some(r) = self.ref_at(point) {
match &r.kind {
RefKind::Variable | RefKind::ContainerAccess => {
let method_hover = self.refs.iter()
.find(|mr| matches!(mr.kind, RefKind::MethodCall { .. })
&& contains_point(&mr.span, point)
&& mr.target_name != r.target_name);
if let Some(mr) = method_hover {
if let RefKind::MethodCall { ref invocant, ref invocant_span, .. } = mr.kind {
let class_name = self.resolve_method_invocant(
invocant, invocant_span, mr.scope, point, tree, Some(source.as_bytes()), module_index,
);
if let Some(ref cn) = class_name {
match self.resolve_method_in_ancestors(cn, &mr.target_name, module_index) {
Some(MethodResolution::Local { sym_id, class: ref defining_class, .. }) => {
let sym = self.symbol(sym_id);
let line = source_line_at(source, sym.selection_span.start.row);
let class_label = if defining_class != cn {
format!("{} (from {})", cn, defining_class)
} else {
cn.to_string()
};
let mut text = format!("```perl\n{}\n```\n\n*class {} — resolved from `{}`*", line.trim(), class_label, r.target_name);
if let Some(ref rt) = self.find_method_return_type(cn, &mr.target_name, module_index, None) {
text.push_str(&format!("\n\n*returns: {}*", format_inferred_type(rt)));
}
if let SymbolDetail::Sub { ref doc, .. } = sym.detail {
if let Some(ref d) = doc {
text.push_str(&format!("\n\n{}", d));
}
}
return Some(text);
}
Some(MethodResolution::CrossFile { ref class }) => {
if let Some(idx) = module_index {
if let Some(exports) = idx.get_exports_cached(class) {
if let Some(sub_info) = exports.sub_info(&mr.target_name) {
let sig = format_cross_file_signature(&mr.target_name, sub_info);
let mut text = format!("```perl\n{}\n```\n\n*class {} — resolved from `{}`*", sig, class, r.target_name);
if let Some(ref rt) = sub_info.return_type {
text.push_str(&format!("\n\n*returns: {}*", format_inferred_type(rt)));
}
if let Some(ref doc) = sub_info.doc {
text.push_str(&format!("\n\n{}", doc));
}
return Some(text);
}
}
}
}
None => {}
}
}
}
}
if let Some(sym_id) = r.resolves_to {
let sym = self.symbol(sym_id);
return Some(self.format_symbol_hover_at(sym, source, point));
}
if let Some(sym) = self.resolve_variable(&r.target_name, point) {
return Some(self.format_symbol_hover_at(sym, source, point));
}
}
RefKind::FunctionCall => {
for &sid in self.symbols_named(&r.target_name) {
let sym = self.symbol(sid);
if matches!(sym.kind, SymKind::Sub) {
return Some(self.format_symbol_hover(sym, source));
}
}
}
RefKind::MethodCall { ref invocant, ref invocant_span, .. } => {
let class_name = self.resolve_method_invocant(
invocant, invocant_span, r.scope, point, tree, Some(source.as_bytes()), module_index,
);
if let Some(ref cn) = class_name {
match self.resolve_method_in_ancestors(cn, &r.target_name, module_index) {
Some(MethodResolution::Local { sym_id, class: ref defining_class, .. }) => {
let sym = self.symbol(sym_id);
let line = source_line_at(source, sym.selection_span.start.row);
let class_label = if defining_class != cn {
format!("{} (from {})", cn, defining_class)
} else {
cn.to_string()
};
let mut text = format!("```perl\n{}\n```\n\n*class {}*", line.trim(), class_label);
if let Some(ref rt) = self.find_method_return_type(cn, &r.target_name, module_index, None) {
text.push_str(&format!("\n\n*returns: {}*", format_inferred_type(rt)));
}
return Some(text);
}
Some(MethodResolution::CrossFile { ref class }) => {
if let Some(idx) = module_index {
if let Some(exports) = idx.get_exports_cached(class) {
if let Some(sub_info) = exports.sub_info(&r.target_name) {
let class_label = if class != cn {
format!("{} (from {})", cn, class)
} else {
cn.to_string()
};
let sig = format_cross_file_signature(&r.target_name, sub_info);
let mut text = format!("```perl\n{}\n```\n\n*class {}*", sig, class_label);
if let Some(ref rt) = sub_info.return_type {
text.push_str(&format!("\n\n*returns: {}*", format_inferred_type(rt)));
}
if let Some(ref doc) = sub_info.doc {
text.push_str(&format!("\n\n{}", doc));
}
return Some(text);
}
}
}
}
None => {}
}
}
for &sid in self.symbols_named(&r.target_name) {
let sym = self.symbol(sid);
if matches!(sym.kind, SymKind::Sub | SymKind::Method) {
return Some(self.format_symbol_hover(sym, source));
}
}
}
RefKind::PackageRef => {
for &sid in self.symbols_named(&r.target_name) {
let sym = self.symbol(sid);
if matches!(sym.kind, SymKind::Package | SymKind::Class) {
return Some(self.format_symbol_hover(sym, source));
}
}
}
RefKind::HashKeyAccess { owner, .. } => {
if let Some(ref owner) = owner {
let defs = self.hash_key_defs_for_owner(owner);
let matching: Vec<_> = defs.iter()
.filter(|d| d.name == r.target_name)
.collect();
if !matching.is_empty() {
let lines: Vec<String> = matching.iter()
.map(|d| {
let line = source_line_at(source, d.span.start.row);
format!("- `{}`", line.trim())
})
.collect();
return Some(format!("**Hash key `{}`**\n\n{}", r.target_name, lines.join("\n")));
}
}
}
}
}
if let Some(sym) = self.symbol_at(point) {
return Some(self.format_symbol_hover(sym, source));
}
None
}
pub fn rename_at(&self, point: Point, new_name: &str, tree: Option<&tree_sitter::Tree>, source_bytes: Option<&[u8]>) -> Option<Vec<(Span, String)>> {
let refs = self.find_references(point, tree, source_bytes);
if refs.is_empty() {
return None;
}
let is_variable = self.ref_at(point)
.map(|r| matches!(r.kind, RefKind::Variable | RefKind::ContainerAccess))
.or_else(|| self.symbol_at(point).map(|s| matches!(s.kind, SymKind::Variable | SymKind::Field)))
.unwrap_or(false);
let edits: Vec<(Span, String)> = if is_variable {
let bare_name = if new_name.starts_with('$') || new_name.starts_with('@') || new_name.starts_with('%') {
&new_name[1..]
} else {
new_name
};
refs.into_iter().map(|span| {
let name_span = Span {
start: Point::new(span.start.row, span.start.column + 1),
end: span.end,
};
(name_span, bare_name.to_string())
}).collect()
} else {
refs.into_iter().map(|span| (span, new_name.to_string())).collect()
};
Some(edits)
}
pub fn rename_kind_at(&self, point: Point) -> Option<RenameKind> {
if let Some(r) = self.ref_at(point) {
return Some(match &r.kind {
RefKind::Variable | RefKind::ContainerAccess => RenameKind::Variable,
RefKind::FunctionCall => RenameKind::Function(r.target_name.clone()),
RefKind::MethodCall { .. } => RenameKind::Method(r.target_name.clone()),
RefKind::PackageRef => RenameKind::Package(r.target_name.clone()),
RefKind::HashKeyAccess { .. } => RenameKind::HashKey(r.target_name.clone()),
});
}
if let Some(sym) = self.symbol_at(point) {
return match sym.kind {
SymKind::Variable | SymKind::Field => Some(RenameKind::Variable),
SymKind::Sub => Some(RenameKind::Function(sym.name.clone())),
SymKind::Method => Some(RenameKind::Method(sym.name.clone())),
SymKind::Package | SymKind::Class => Some(RenameKind::Package(sym.name.clone())),
_ => None,
};
}
None
}
pub fn rename_sub(&self, old_name: &str, new_name: &str) -> Vec<(Span, String)> {
let mut edits = Vec::new();
for sym in &self.symbols {
if sym.name == old_name && matches!(sym.kind, SymKind::Sub | SymKind::Method) {
edits.push((sym.selection_span, new_name.to_string()));
}
}
for r in &self.refs {
if r.target_name == old_name {
match &r.kind {
RefKind::FunctionCall => {
edits.push((r.span, new_name.to_string()));
}
RefKind::MethodCall { method_name_span, .. } => {
edits.push((*method_name_span, new_name.to_string()));
}
_ => {}
}
}
}
edits
}
pub fn rename_package(&self, old_name: &str, new_name: &str) -> Vec<(Span, String)> {
let mut edits = Vec::new();
for sym in &self.symbols {
if sym.name == old_name && matches!(sym.kind, SymKind::Package | SymKind::Class | SymKind::Module) {
edits.push((sym.selection_span, new_name.to_string()));
}
}
for r in &self.refs {
if r.target_name == old_name && matches!(r.kind, RefKind::PackageRef) {
edits.push((r.span, new_name.to_string()));
}
}
edits
}
fn resolve_target_at(&self, point: Point, tree: Option<&tree_sitter::Tree>, source_bytes: Option<&[u8]>, module_index: Option<&ModuleIndex>) -> Option<(SymbolId, bool)> {
if let Some(r) = self.ref_at(point) {
match &r.kind {
RefKind::Variable | RefKind::ContainerAccess => {
if let Some(sym_id) = r.resolves_to {
return Some((sym_id, true));
}
if let Some(sym) = self.resolve_variable(&r.target_name, point) {
return Some((sym.id, true));
}
}
RefKind::FunctionCall => {
for &sid in self.symbols_named(&r.target_name) {
if matches!(self.symbol(sid).kind, SymKind::Sub) {
return Some((sid, true));
}
}
}
RefKind::MethodCall { ref invocant, ref invocant_span, .. } => {
let class_name = self.resolve_method_invocant(
invocant, invocant_span, r.scope, point, tree, source_bytes, module_index,
);
if let Some(ref cn) = class_name {
match self.resolve_method_in_ancestors(cn, &r.target_name, module_index) {
Some(MethodResolution::Local { sym_id, .. }) => {
return Some((sym_id, true));
}
Some(MethodResolution::CrossFile { .. }) => {
}
None => {}
}
}
for &sid in self.symbols_named(&r.target_name) {
if matches!(self.symbol(sid).kind, SymKind::Sub | SymKind::Method) {
return Some((sid, true));
}
}
}
RefKind::PackageRef => {
for &sid in self.symbols_named(&r.target_name) {
if matches!(self.symbol(sid).kind, SymKind::Package | SymKind::Class | SymKind::Module) {
return Some((sid, true));
}
}
}
RefKind::HashKeyAccess { ref owner, .. } => {
let resolved_owner = owner.clone().or_else(|| {
let tree = tree?;
let source = source_bytes?;
self.resolve_hash_owner_from_tree(tree, source, r.span.start, module_index)
});
if let Some(ref owner) = resolved_owner {
for def in self.hash_key_defs_for_owner(owner) {
if def.name == r.target_name {
return Some((def.id, true));
}
}
}
}
}
}
if let Some(sym) = self.symbol_at(point) {
return Some((sym.id, false));
}
None
}
pub fn resolve_method_invocant_public(
&self,
invocant: &str,
invocant_span: &Option<Span>,
scope: ScopeId,
point: Point,
tree: Option<&tree_sitter::Tree>,
source_bytes: Option<&[u8]>,
module_index: Option<&ModuleIndex>,
) -> Option<String> {
self.resolve_method_invocant(invocant, invocant_span, scope, point, tree, source_bytes, module_index)
}
fn resolve_method_invocant(
&self,
invocant: &str,
invocant_span: &Option<Span>,
scope: ScopeId,
point: Point,
tree: Option<&tree_sitter::Tree>,
source_bytes: Option<&[u8]>,
module_index: Option<&ModuleIndex>,
) -> Option<String> {
if let (Some(span), Some(tree), Some(src)) = (invocant_span, tree, source_bytes) {
if let Some(node) = tree.root_node()
.descendant_for_point_range(span.start, span.end)
{
if let Some(ty) = self.resolve_expression_type(node, src, module_index) {
if let Some(cn) = ty.class_name() {
return Some(cn.to_string());
}
}
}
}
self.resolve_invocant_class(invocant, scope, point)
}
fn resolve_invocant_class(&self, invocant: &str, scope: ScopeId, point: Point) -> Option<String> {
if invocant.starts_with('$') || invocant.starts_with('@') || invocant.starts_with('%') {
self.inferred_type(invocant, point)
.and_then(|t| t.class_name().map(|s| s.to_string()))
.or_else(|| {
let chain = self.scope_chain(scope);
for scope_id in &chain {
let s = self.scope(*scope_id);
if let ScopeKind::Class { ref name } = s.kind {
return Some(name.clone());
}
if let Some(ref pkg) = s.package {
return Some(pkg.clone());
}
}
None
})
} else {
Some(invocant.to_string())
}
}
#[cfg(test)]
pub(crate) fn find_method_in_class(&self, class_name: &str, method_name: &str) -> Option<Span> {
self.find_method_in_class_with_index(class_name, method_name, None)
}
#[cfg(test)]
fn find_method_in_class_with_index(
&self,
class_name: &str,
method_name: &str,
module_index: Option<&ModuleIndex>,
) -> Option<Span> {
match self.resolve_method_in_ancestors(class_name, method_name, module_index) {
Some(MethodResolution::Local { sym_id, .. }) => {
Some(self.symbol(sym_id).selection_span)
}
Some(MethodResolution::CrossFile { .. }) => {
None
}
None => None,
}
}
pub fn resolve_method_in_ancestors(
&self,
class_name: &str,
method_name: &str,
module_index: Option<&ModuleIndex>,
) -> Option<MethodResolution> {
self.resolve_method_in_ancestors_inner(class_name, method_name, module_index, 0)
}
fn resolve_method_in_ancestors_inner(
&self,
class_name: &str,
method_name: &str,
module_index: Option<&ModuleIndex>,
depth: usize,
) -> Option<MethodResolution> {
if depth > 20 {
return None; }
for &sid in self.symbols_named(method_name) {
let sym = self.symbol(sid);
if matches!(sym.kind, SymKind::Sub | SymKind::Method) {
if self.symbol_in_class(sid, class_name) {
return Some(MethodResolution::Local {
class: class_name.to_string(),
sym_id: sid,
});
}
}
}
if let Some(parents) = self.package_parents.get(class_name) {
for parent in parents {
if let Some(res) = self.resolve_method_in_ancestors_inner(
parent, method_name, module_index, depth + 1,
) {
return Some(res);
}
}
}
if let Some(idx) = module_index {
if let Some(exports) = idx.get_exports_cached(class_name) {
if exports.subs.contains_key(method_name) {
return Some(MethodResolution::CrossFile {
class: class_name.to_string(),
});
}
}
let cross_parents = idx.parents_cached(class_name);
for parent in &cross_parents {
if self.package_parents.contains_key(parent.as_str()) {
if let Some(res) = self.resolve_method_in_ancestors_inner(
parent, method_name, module_index, depth + 1,
) {
return Some(res);
}
} else {
if let Some(res) = self.resolve_cross_file_method(
parent, method_name, idx, depth + 1,
) {
return Some(res);
}
}
}
}
None
}
fn resolve_cross_file_method(
&self,
class_name: &str,
method_name: &str,
module_index: &ModuleIndex,
depth: usize,
) -> Option<MethodResolution> {
if depth > 20 {
return None;
}
if let Some(exports) = module_index.get_exports_cached(class_name) {
if exports.subs.contains_key(method_name) {
return Some(MethodResolution::CrossFile {
class: class_name.to_string(),
});
}
}
let parents = module_index.parents_cached(class_name);
for parent in &parents {
if let Some(res) = self.resolve_cross_file_method(
parent, method_name, module_index, depth + 1,
) {
return Some(res);
}
}
None
}
pub(crate) fn symbol_in_class(&self, sym_id: SymbolId, class_name: &str) -> bool {
let sym = self.symbol(sym_id);
if let Some(ref pkg) = sym.package {
return pkg == class_name;
}
let start_scope = self.scope_at(sym.span.start).unwrap_or(sym.scope);
let chain = self.scope_chain(start_scope);
for scope_id in &chain {
let s = self.scope(*scope_id);
if let ScopeKind::Class { ref name } = s.kind {
return name == class_name;
}
if let Some(ref pkg) = s.package {
return pkg == class_name;
}
}
false
}
fn find_package_or_class(&self, name: &str) -> Option<Span> {
for &sid in self.symbols_named(name) {
let sym = self.symbol(sid);
if matches!(sym.kind, SymKind::Package | SymKind::Class) {
return Some(sym.selection_span);
}
}
None
}
fn format_symbol_hover(&self, sym: &Symbol, source: &str) -> String {
self.format_symbol_hover_at(sym, source, sym.selection_span.end)
}
fn format_symbol_hover_at(&self, sym: &Symbol, source: &str, at: Point) -> String {
let line = source_line_at(source, sym.span.start.row);
let mut text = format!("```perl\n{}\n```", line.trim());
if matches!(sym.kind, SymKind::Variable | SymKind::Field) {
if let Some(it) = self.inferred_type(&sym.name, at) {
text.push_str(&format!("\n\n*type: {}*", format_inferred_type(it)));
}
}
if matches!(sym.kind, SymKind::Sub | SymKind::Method) {
if let SymbolDetail::Sub { return_type: Some(ref rt), .. } = sym.detail {
text.push_str(&format!("\n\n*returns: {}*", format_inferred_type(rt)));
}
}
text
}
pub fn document_symbols(&self) -> Vec<OutlineSymbol> {
let file_scope = ScopeId(0);
self.outline_children_of(file_scope)
}
fn outline_children_of(&self, parent_scope: ScopeId) -> Vec<OutlineSymbol> {
let mut result = Vec::new();
for sym in &self.symbols {
if sym.scope != parent_scope {
continue;
}
let (name, detail, children) = match sym.kind {
SymKind::Sub => {
let body_scope = self.find_body_scope(sym);
let children = body_scope
.map(|s| self.outline_children_of(s))
.unwrap_or_default();
(format!("sub {}", sym.name), None, children)
}
SymKind::Method => {
let body_scope = self.find_body_scope(sym);
let children = body_scope
.map(|s| self.outline_children_of(s))
.unwrap_or_default();
(format!("method {}", sym.name), None, children)
}
SymKind::Class => {
let body_scope = self.find_body_scope(sym);
let children = body_scope
.map(|s| self.outline_children_of(s))
.unwrap_or_default();
(sym.name.clone(), Some("class".to_string()), children)
}
SymKind::Package => {
(sym.name.clone(), Some("package".to_string()), Vec::new())
}
SymKind::Module => {
(format!("use {}", sym.name), None, Vec::new())
}
SymKind::Variable => {
let detail = match &sym.detail {
SymbolDetail::Variable { decl_kind, .. } => match decl_kind {
DeclKind::My => "my",
DeclKind::Our => "our",
DeclKind::State => "state",
DeclKind::Field => "field",
DeclKind::Param => "param",
DeclKind::ForVar => "for",
},
_ => "my",
};
(sym.name.clone(), Some(detail.to_string()), Vec::new())
}
SymKind::Field => {
(sym.name.clone(), Some("field".to_string()), Vec::new())
}
SymKind::HashKeyDef => continue, };
result.push(OutlineSymbol {
name,
detail,
kind: sym.kind,
span: sym.span,
selection_span: sym.selection_span,
children,
});
}
result
}
fn find_body_scope(&self, sym: &Symbol) -> Option<ScopeId> {
self.scopes.iter().find(|s| {
let kind_matches = match (&s.kind, &sym.kind) {
(ScopeKind::Sub { name: sn }, SymKind::Sub) => sn == &sym.name,
(ScopeKind::Method { name: mn }, SymKind::Method) => mn == &sym.name,
(ScopeKind::Class { name: cn }, SymKind::Class) => cn == &sym.name,
_ => false,
};
kind_matches && s.span == sym.span
}).map(|s| s.id)
}
pub fn semantic_tokens(&self) -> Vec<PerlSemanticToken> {
let mut tokens: Vec<PerlSemanticToken> = Vec::new();
for sym in &self.symbols {
match sym.kind {
SymKind::Variable | SymKind::Field => {
let is_self = sym.name == "$self" || sym.name == "$class";
let (sigil, is_readonly, is_param) = match &sym.detail {
SymbolDetail::Variable { sigil, decl_kind } => {
let readonly = matches!(decl_kind, DeclKind::Field);
let is_param = matches!(decl_kind, DeclKind::Param | DeclKind::ForVar);
(*sigil, readonly, is_param)
}
SymbolDetail::Field { sigil, attributes } => {
let readonly = !attributes.iter().any(|a| a == "writer" || a == "mutator" || a == "accessor");
(*sigil, readonly, true)
}
_ => continue,
};
let token_type = if is_self { TOK_KEYWORD } else if is_param { TOK_PARAMETER } else { TOK_VARIABLE };
let mut mods = if is_self { 0 } else { sigil_modifier(sigil) };
mods |= 1 << MOD_DECLARATION;
if is_readonly { mods |= 1 << MOD_READONLY; }
tokens.push(PerlSemanticToken { span: sym.selection_span, token_type, modifiers: mods });
}
SymKind::Package | SymKind::Class => {
tokens.push(PerlSemanticToken {
span: sym.selection_span,
token_type: TOK_NAMESPACE,
modifiers: 1 << MOD_DECLARATION,
});
}
SymKind::Module => {
tokens.push(PerlSemanticToken {
span: sym.selection_span,
token_type: TOK_NAMESPACE,
modifiers: 0,
});
}
SymKind::Sub => {
tokens.push(PerlSemanticToken {
span: sym.selection_span,
token_type: TOK_FUNCTION,
modifiers: 1 << MOD_DECLARATION,
});
}
SymKind::Method => {
tokens.push(PerlSemanticToken {
span: sym.selection_span,
token_type: TOK_METHOD,
modifiers: 1 << MOD_DECLARATION,
});
}
_ => {}
}
}
let imported_names: std::collections::HashSet<&str> = self.imports.iter()
.flat_map(|imp| imp.imported_symbols.iter().map(|s| s.as_str()))
.collect();
for r in &self.refs {
if matches!(r.access, AccessKind::Declaration) {
continue;
}
match &r.kind {
RefKind::Variable | RefKind::ContainerAccess => {
let sigil = r.target_name.chars().next().unwrap_or('$');
let is_self = r.target_name == "$self" || r.target_name == "$class";
let token_type = if is_self { TOK_KEYWORD } else { TOK_VARIABLE };
let mut mods = if is_self { 0 } else { sigil_modifier(sigil) };
if matches!(r.access, AccessKind::Write) { mods |= 1 << MOD_MODIFICATION; }
tokens.push(PerlSemanticToken { span: r.span, token_type, modifiers: mods });
}
RefKind::FunctionCall => {
let token_type = if self.framework_imports.contains(r.target_name.as_str()) {
TOK_MACRO
} else {
TOK_FUNCTION
};
let mut mods = 0;
if imported_names.contains(r.target_name.as_str()) {
mods |= 1 << MOD_DEFAULT_LIBRARY;
}
tokens.push(PerlSemanticToken { span: r.span, token_type, modifiers: mods });
}
RefKind::MethodCall { method_name_span, .. } => {
let mods = 0; tokens.push(PerlSemanticToken { span: *method_name_span, token_type: TOK_METHOD, modifiers: mods });
}
RefKind::PackageRef => {
tokens.push(PerlSemanticToken { span: r.span, token_type: TOK_NAMESPACE, modifiers: 0 });
}
RefKind::HashKeyAccess { .. } => {
tokens.push(PerlSemanticToken { span: r.span, token_type: TOK_PROPERTY, modifiers: 0 });
}
}
}
for sym in &self.symbols {
if matches!(sym.kind, SymKind::HashKeyDef) {
tokens.push(PerlSemanticToken {
span: sym.selection_span,
token_type: TOK_PROPERTY,
modifiers: 1 << MOD_DECLARATION,
});
}
}
tokens.sort_by_key(|t| (t.span.start.row, t.span.start.column));
tokens.dedup_by(|b, a| a.span.start.row == b.span.start.row && a.span.start.column == b.span.start.column);
tokens
}
}
pub const PRIORITY_LOCAL: u8 = 0;
pub const PRIORITY_FILE_WIDE: u8 = 10;
pub const PRIORITY_EXPLICIT_IMPORT: u8 = 12;
pub const PRIORITY_BARE_IMPORT: u8 = 15;
pub const PRIORITY_AUTO_ADD_QW: u8 = 18;
pub const PRIORITY_LESS_RELEVANT: u8 = 20;
pub const PRIORITY_UNIMPORTED: u8 = 25;
pub const PRIORITY_DYNAMIC: u8 = 50;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum MethodResolution {
Local { class: String, sym_id: SymbolId },
CrossFile { class: String },
}
pub enum ResolvedSub<'a> {
Local(&'a Symbol),
CrossFile {
params: Vec<crate::module_index::ExportedParam>,
is_method: bool,
hash_keys: Vec<String>,
},
}
#[derive(Debug, Clone)]
pub struct CompletionCandidate {
pub label: String,
pub kind: SymKind,
pub detail: Option<String>,
pub insert_text: Option<String>,
pub sort_priority: u8,
pub additional_edits: Vec<(Span, String)>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct SignatureInfo {
pub name: String,
pub params: Vec<ParamInfo>,
pub is_method: bool,
pub body_end: Point,
pub param_types: Option<Vec<Option<String>>>,
}
impl FileAnalysis {
pub fn complete_variables(&self, point: Point, sigil: char) -> Vec<CompletionCandidate> {
let visible = self.visible_symbols(point);
let mut seen = HashSet::<(String, char)>::new();
let mut candidates = Vec::new();
let mut vars: Vec<(&Symbol, usize)> = visible
.into_iter()
.filter(|s| matches!(s.kind, SymKind::Variable | SymKind::Field))
.filter_map(|s| {
if let SymbolDetail::Variable { .. } = &s.detail {
let scope = &self.scopes[s.scope.0 as usize];
let scope_size = span_size(&scope.span);
Some((s, scope_size))
} else if let SymbolDetail::Field { .. } = &s.detail {
let scope = &self.scopes[s.scope.0 as usize];
let scope_size = span_size(&scope.span);
Some((s, scope_size))
} else {
None
}
})
.collect();
vars.sort_by_key(|(_, sz)| *sz);
for (sym, scope_size) in vars {
let (bare_name, decl_sigil) = match &sym.detail {
SymbolDetail::Variable { sigil: ds, .. } => {
(sym.name[1..].to_string(), *ds)
}
SymbolDetail::Field { sigil: ds, .. } => {
(sym.name[1..].to_string(), *ds)
}
_ => continue,
};
let key = (bare_name.clone(), decl_sigil);
if seen.contains(&key) {
continue;
}
seen.insert(key);
let priority = std::cmp::min(scope_size, 255) as u8;
let detail = match &sym.detail {
SymbolDetail::Variable { decl_kind, .. } => {
Some(match decl_kind {
DeclKind::My => "my".to_string(),
DeclKind::Our => "our".to_string(),
DeclKind::State => "state".to_string(),
DeclKind::Field => "field".to_string(),
DeclKind::Param => "param".to_string(),
DeclKind::ForVar => "for".to_string(),
})
}
SymbolDetail::Field { .. } => Some("field".to_string()),
_ => None,
};
generate_cross_sigil_candidates(
&bare_name,
decl_sigil,
sigil,
detail,
priority,
&mut candidates,
);
}
candidates
}
pub fn complete_methods(&self, invocant: &str, point: Point) -> Vec<CompletionCandidate> {
let class_name = if !invocant.starts_with('$')
&& !invocant.starts_with('@')
&& !invocant.starts_with('%')
{
Some(invocant.to_string())
} else {
self.resolve_invocant_class(
invocant,
self.scope_at(point).unwrap_or(ScopeId(0)),
point,
)
};
if let Some(ref cn) = class_name {
let candidates = self.complete_methods_for_class(cn, None);
if !candidates.is_empty() {
return candidates;
}
}
self.symbols
.iter()
.filter(|s| matches!(s.kind, SymKind::Sub | SymKind::Method))
.map(|s| CompletionCandidate {
label: s.name.clone(),
kind: s.kind,
detail: Some(
if matches!(s.kind, SymKind::Method) {
"method"
} else {
"sub"
}
.to_string(),
),
insert_text: None,
sort_priority: PRIORITY_FILE_WIDE,
additional_edits: vec![],
})
.collect()
}
fn complete_hash_keys_for_owner(&self, owner: &HashKeyOwner) -> Vec<CompletionCandidate> {
let defs = self.hash_key_defs_for_owner(owner);
let mut seen = HashSet::new();
let mut candidates = Vec::new();
for def in defs {
if !seen.insert(def.name.clone()) {
continue;
}
let is_dynamic = matches!(
&def.detail,
SymbolDetail::HashKeyDef { is_dynamic: true, .. }
);
let detail = match owner {
HashKeyOwner::Class(name) => format!("{}->{{{}}}", name, def.name),
HashKeyOwner::Variable { name, .. } => format!("{}{{{}}}", name, def.name),
HashKeyOwner::Sub(name) => format!("{}()->{{{}}}", name, def.name),
};
candidates.push(CompletionCandidate {
label: def.name.clone(),
kind: SymKind::Variable,
detail: Some(detail),
insert_text: None,
sort_priority: if is_dynamic { PRIORITY_DYNAMIC } else { PRIORITY_FILE_WIDE },
additional_edits: vec![],
});
}
candidates
}
pub fn complete_hash_keys(&self, var_text: &str, point: Point) -> Vec<CompletionCandidate> {
match self.resolve_hash_key_owner(var_text, point) {
Some(owner) => self.complete_hash_keys_for_owner(&owner),
None => Vec::new(),
}
}
pub fn complete_hash_keys_for_class(&self, class_name: &str, _point: Point) -> Vec<CompletionCandidate> {
self.complete_hash_keys_for_owner(&HashKeyOwner::Class(class_name.to_string()))
}
pub fn complete_hash_keys_for_sub(&self, sub_name: &str, _point: Point) -> Vec<CompletionCandidate> {
self.complete_hash_keys_for_owner(&HashKeyOwner::Sub(sub_name.to_string()))
}
pub fn complete_general(&self, point: Point) -> Vec<CompletionCandidate> {
let mut candidates = Vec::new();
for sigil in ['$', '@', '%'] {
candidates.extend(self.complete_variables(point, sigil));
}
for sym in &self.symbols {
if matches!(sym.kind, SymKind::Sub | SymKind::Method) {
candidates.push(CompletionCandidate {
label: sym.name.clone(),
kind: sym.kind,
detail: Some(
if matches!(sym.kind, SymKind::Method) {
"method"
} else {
"sub"
}
.to_string(),
),
insert_text: None,
sort_priority: PRIORITY_FILE_WIDE,
additional_edits: vec![],
});
}
}
for sym in &self.symbols {
if matches!(sym.kind, SymKind::Package | SymKind::Class) {
candidates.push(CompletionCandidate {
label: sym.name.clone(),
kind: sym.kind,
detail: Some(
if matches!(sym.kind, SymKind::Class) {
"class"
} else {
"package"
}
.to_string(),
),
insert_text: None,
sort_priority: PRIORITY_LESS_RELEVANT,
additional_edits: vec![],
});
}
}
candidates
}
pub fn complete_keyval_args(
&self,
call_name: &str,
is_method: bool,
invocant: Option<&str>,
point: Point,
used_keys: &HashSet<String>,
module_index: Option<&ModuleIndex>,
) -> Vec<CompletionCandidate> {
if call_name == "new" {
if let Some(inv) = invocant {
let class_name = if !inv.starts_with('$') {
Some(inv.to_string())
} else {
self.resolve_invocant_class(
inv,
self.scope_at(point).unwrap_or(ScopeId(0)),
point,
)
};
if let Some(ref cn) = class_name {
let param_candidates = self.class_param_completions(cn, used_keys);
if !param_candidates.is_empty() {
return param_candidates;
}
}
}
}
let resolved = match self.find_sub_for_call(call_name, is_method, invocant, point, module_index) {
Some(r) => r,
None => return Vec::new(),
};
match resolved {
ResolvedSub::Local(sub_sym) => {
let params = match &sub_sym.detail {
SymbolDetail::Sub { params, .. } => params,
_ => return Vec::new(),
};
let slurpy = params
.iter()
.find(|p| p.is_slurpy && p.name.starts_with('%'));
let slurpy_name = match slurpy {
Some(p) => {
if p.name.starts_with('%') || p.name.starts_with('$') || p.name.starts_with('@') {
&p.name[1..]
} else {
&p.name
}
}
None => return Vec::new(),
};
let body_scope = self.find_body_scope(sub_sym);
let keys = match body_scope {
Some(scope_id) => self.hash_keys_in_scope(slurpy_name, scope_id),
None => Vec::new(),
};
keys.into_iter()
.filter(|k| !used_keys.contains(k))
.map(|k| CompletionCandidate {
label: format!("{} =>", k),
kind: SymKind::Variable,
detail: Some(format!("{}(%{})", call_name, slurpy_name)),
insert_text: Some(format!("{} => ", k)),
sort_priority: PRIORITY_LOCAL,
additional_edits: vec![],
})
.collect()
}
ResolvedSub::CrossFile { hash_keys, params, .. } => {
let has_slurpy = params.iter().any(|p| p.is_slurpy && p.name.starts_with('%'));
if !has_slurpy || hash_keys.is_empty() {
return Vec::new();
}
hash_keys.into_iter()
.filter(|k| !used_keys.contains(k))
.map(|k| CompletionCandidate {
label: format!("{} =>", k),
kind: SymKind::Variable,
detail: Some(format!("{}()", call_name)),
insert_text: Some(format!("{} => ", k)),
sort_priority: PRIORITY_LOCAL,
additional_edits: vec![],
})
.collect()
}
}
}
pub fn signature_for_call(
&self,
name: &str,
is_method: bool,
invocant: Option<&str>,
point: Point,
module_index: Option<&ModuleIndex>,
) -> Option<SignatureInfo> {
let resolved = self.find_sub_for_call(name, is_method, invocant, point, module_index)?;
match resolved {
ResolvedSub::Local(sub_sym) => {
let (params, sym_is_method) = match &sub_sym.detail {
SymbolDetail::Sub { params, is_method, .. } => (params.clone(), *is_method),
_ => return None,
};
let mut params = params;
let is_method = is_method
|| sym_is_method
|| params
.first()
.map_or(false, |p| p.name == "$self" || p.name == "$class");
if is_method && !params.is_empty() {
let first = ¶ms[0].name;
if first == "$self" || first == "$class" {
params.remove(0);
}
}
Some(SignatureInfo {
name: name.to_string(),
params,
is_method,
body_end: sub_sym.span.end,
param_types: None, })
}
ResolvedSub::CrossFile { params: exported_params, is_method: cf_is_method, .. } => {
let all_types: Vec<Option<String>> = exported_params.iter()
.map(|p| p.inferred_type.clone())
.collect();
let mut params: Vec<ParamInfo> = exported_params
.iter()
.map(|p| ParamInfo {
name: p.name.clone(),
default: None,
is_slurpy: p.is_slurpy,
})
.collect();
let is_method = is_method || cf_is_method
|| params.first().map_or(false, |p| p.name == "$self" || p.name == "$class");
let mut param_types = all_types;
if is_method && !params.is_empty() {
let first = ¶ms[0].name;
if first == "$self" || first == "$class" {
params.remove(0);
if !param_types.is_empty() {
param_types.remove(0);
}
}
}
Some(SignatureInfo {
name: name.to_string(),
params,
is_method,
body_end: Point::new(0, 0),
param_types: Some(param_types),
})
}
}
}
fn find_sub_for_call<'s>(
&'s self,
name: &str,
is_method: bool,
invocant: Option<&str>,
point: Point,
module_index: Option<&ModuleIndex>,
) -> Option<ResolvedSub<'s>> {
let class_name = if is_method {
invocant.and_then(|inv| {
if !inv.starts_with('$') && !inv.starts_with('@') && !inv.starts_with('%') {
Some(inv.to_string())
} else {
self.resolve_invocant_class(
inv,
self.scope_at(point).unwrap_or(ScopeId(0)),
point,
)
}
})
} else {
None
};
if let Some(ref cn) = class_name {
match self.resolve_method_in_ancestors(cn, name, module_index) {
Some(MethodResolution::Local { sym_id, .. }) => {
return Some(ResolvedSub::Local(self.symbol(sym_id)));
}
Some(MethodResolution::CrossFile { ref class }) => {
if let Some(idx) = module_index {
if let Some(exports) = idx.get_exports_cached(class) {
if let Some(sub_info) = exports.sub_info(name) {
return Some(ResolvedSub::CrossFile {
params: sub_info.params.clone(),
is_method: sub_info.is_method,
hash_keys: sub_info.hash_keys.clone(),
});
}
}
}
}
None => {}
}
}
for &sid in self.symbols_named(name) {
let sym = self.symbol(sid);
if matches!(sym.kind, SymKind::Sub | SymKind::Method) {
return Some(ResolvedSub::Local(sym));
}
}
if !is_method {
if let Some(idx) = module_index {
for import in &self.imports {
if import.imported_symbols.iter().any(|s| s == name) {
if let Some(exports) = idx.get_exports_cached(&import.module_name) {
if let Some(sub_info) = exports.sub_info(name) {
return Some(ResolvedSub::CrossFile {
params: sub_info.params.clone(),
is_method: sub_info.is_method,
hash_keys: sub_info.hash_keys.clone(),
});
}
}
}
}
for import in &self.imports {
if let Some(exports) = idx.get_exports_cached(&import.module_name) {
if exports.export.iter().any(|s| s == name) {
if let Some(sub_info) = exports.sub_info(name) {
return Some(ResolvedSub::CrossFile {
params: sub_info.params.clone(),
is_method: sub_info.is_method,
hash_keys: sub_info.hash_keys.clone(),
});
}
}
}
}
}
}
None
}
fn resolve_hash_key_owner(&self, var_text: &str, point: Point) -> Option<HashKeyOwner> {
let bare_name = if var_text.starts_with('$') || var_text.starts_with('@') || var_text.starts_with('%') {
&var_text[1..]
} else {
var_text
};
if let Some(it) = self.inferred_type(var_text, point) {
if let Some(cn) = it.class_name() {
return Some(HashKeyOwner::Class(cn.to_string()));
}
}
for cb in &self.call_bindings {
if cb.variable == var_text
&& cb.span.start <= point
&& contains_point(&self.scopes[cb.scope.0 as usize].span, point)
{
return Some(HashKeyOwner::Sub(cb.func_name.clone()));
}
}
for mcb in &self.method_call_bindings {
if mcb.variable == var_text
&& mcb.span.start <= point
&& contains_point(&self.scopes[mcb.scope.0 as usize].span, point)
{
return Some(HashKeyOwner::Sub(mcb.method_name.clone()));
}
}
let try_names: Vec<String> = if var_text.starts_with('$') {
vec![format!("%{}", bare_name), var_text.to_string()]
} else {
vec![var_text.to_string()]
};
for name in &try_names {
if let Some(sym) = self.resolve_variable(name, point) {
return Some(HashKeyOwner::Variable {
name: name.clone(),
def_scope: sym.scope,
});
}
}
for sym in &self.symbols {
if let SymbolDetail::HashKeyDef { ref owner, .. } = sym.detail {
match owner {
HashKeyOwner::Variable { name, .. } => {
let owner_bare = if name.starts_with('$') || name.starts_with('@') || name.starts_with('%') {
&name[1..]
} else {
name
};
if owner_bare == bare_name {
return Some(owner.clone());
}
}
HashKeyOwner::Class(_) | HashKeyOwner::Sub(_) => {}
}
}
}
None
}
fn class_param_completions(
&self,
class_name: &str,
used_keys: &HashSet<String>,
) -> Vec<CompletionCandidate> {
let mut candidates = Vec::new();
for sym in &self.symbols {
if matches!(sym.kind, SymKind::Field) {
if let SymbolDetail::Field { ref attributes, .. } = sym.detail {
if attributes.contains(&"param".to_string()) {
if self.symbol_in_class(sym.id, class_name) {
let key = sym
.name
.trim_start_matches('$')
.trim_start_matches('@')
.trim_start_matches('%')
.to_string();
if !used_keys.contains(&key) {
candidates.push(CompletionCandidate {
label: format!("{} =>", key),
kind: SymKind::Variable,
detail: Some(format!("{}->new(:param)", class_name)),
insert_text: Some(format!("{} => ", key)),
sort_priority: PRIORITY_LOCAL,
additional_edits: vec![],
});
}
}
}
}
}
}
candidates
}
fn hash_keys_in_scope(&self, var_bare_name: &str, scope_id: ScopeId) -> Vec<String> {
let scope_span = &self.scopes[scope_id.0 as usize].span;
let mut keys = Vec::new();
let mut seen = HashSet::new();
for r in &self.refs {
if let RefKind::HashKeyAccess { ref var_text, .. } = r.kind {
let ref_bare = if var_text.starts_with('$')
|| var_text.starts_with('@')
|| var_text.starts_with('%')
{
&var_text[1..]
} else {
var_text.as_str()
};
if ref_bare == var_bare_name && contains_point(scope_span, r.span.start) {
if !seen.contains(&r.target_name) {
seen.insert(r.target_name.clone());
keys.push(r.target_name.clone());
}
}
}
}
keys
}
}
fn generate_cross_sigil_candidates(
bare_name: &str,
decl_sigil: char,
requested_sigil: char,
detail: Option<String>,
priority: u8,
out: &mut Vec<CompletionCandidate>,
) {
match requested_sigil {
'$' => {
if decl_sigil == '$' {
out.push(CompletionCandidate {
label: format!("${}", bare_name),
kind: SymKind::Variable,
detail: detail.clone(),
insert_text: Some(bare_name.to_string()),
sort_priority: priority,
additional_edits: vec![],
});
}
if decl_sigil == '@' {
out.push(CompletionCandidate {
label: format!("${}[]", bare_name),
kind: SymKind::Variable,
detail: detail.clone().or(Some(format!("@{}", bare_name))),
insert_text: Some(format!("{}[", bare_name)),
sort_priority: priority,
additional_edits: vec![],
});
out.push(CompletionCandidate {
label: format!("$#{}", bare_name),
kind: SymKind::Variable,
detail: detail
.clone()
.or(Some(format!("last index of @{}", bare_name))),
insert_text: Some(format!("#{}", bare_name)),
sort_priority: priority.saturating_add(1),
additional_edits: vec![],
});
}
if decl_sigil == '%' {
out.push(CompletionCandidate {
label: format!("${}{{}}", bare_name),
kind: SymKind::Variable,
detail: detail.clone().or(Some(format!("%{}", bare_name))),
insert_text: Some(format!("{}{{", bare_name)),
sort_priority: priority,
additional_edits: vec![],
});
}
}
'@' => {
if decl_sigil == '@' {
out.push(CompletionCandidate {
label: format!("@{}", bare_name),
kind: SymKind::Variable,
detail: detail.clone(),
insert_text: Some(bare_name.to_string()),
sort_priority: priority,
additional_edits: vec![],
});
out.push(CompletionCandidate {
label: format!("@{}[]", bare_name),
kind: SymKind::Variable,
detail: Some("array slice".to_string()),
insert_text: Some(format!("{}[", bare_name)),
sort_priority: priority.saturating_add(1),
additional_edits: vec![],
});
}
if decl_sigil == '%' {
out.push(CompletionCandidate {
label: format!("@{}{{}}", bare_name),
kind: SymKind::Variable,
detail: detail.clone().or(Some("hash slice".to_string())),
insert_text: Some(format!("{}{{", bare_name)),
sort_priority: priority,
additional_edits: vec![],
});
}
}
'%' => {
if decl_sigil == '%' {
out.push(CompletionCandidate {
label: format!("%{}", bare_name),
kind: SymKind::Variable,
detail: detail.clone(),
insert_text: Some(bare_name.to_string()),
sort_priority: priority,
additional_edits: vec![],
});
out.push(CompletionCandidate {
label: format!("%{}{{}}", bare_name),
kind: SymKind::Variable,
detail: Some("hash kv slice".to_string()),
insert_text: Some(format!("{}{{", bare_name)),
sort_priority: priority.saturating_add(1),
additional_edits: vec![],
});
}
if decl_sigil == '@' {
out.push(CompletionCandidate {
label: format!("%{}[]", bare_name),
kind: SymKind::Variable,
detail: Some("array kv slice".to_string()),
insert_text: Some(format!("{}[", bare_name)),
sort_priority: priority,
additional_edits: vec![],
});
}
}
_ => {}
}
}
pub(crate) fn contains_point(span: &Span, point: Point) -> bool {
(span.start.row < point.row || (span.start.row == point.row && span.start.column <= point.column))
&& (point.row < span.end.row || (point.row == span.end.row && point.column <= span.end.column))
}
fn find_ancestor<'a>(node: tree_sitter::Node<'a>, kind: &str) -> Option<tree_sitter::Node<'a>> {
let mut current = node;
for _ in 0..20 {
if current.kind() == kind {
return Some(current);
}
current = current.parent()?;
}
None
}
fn span_size(span: &Span) -> usize {
let rows = span.end.row.saturating_sub(span.start.row);
let cols = if rows == 0 {
span.end.column.saturating_sub(span.start.column)
} else {
0
};
rows * 10000 + cols
}
fn source_line_at(source: &str, row: usize) -> &str {
source.lines().nth(row).unwrap_or("")
}
pub(crate) fn builtin_return_type(name: &str) -> Option<InferredType> {
match name {
"time" | "length" | "index" | "rindex" | "abs" | "int" | "sqrt"
| "hex" | "oct" | "ord" | "rand" | "pos" | "tell"
| "fileno" => Some(InferredType::Numeric),
"join" | "uc" | "lc" | "ucfirst" | "lcfirst" | "substr" | "sprintf"
| "ref" | "chr" | "crypt" | "quotemeta" | "pack" | "readline"
| "readlink" => Some(InferredType::String),
_ => None,
}
}
pub(crate) fn builtin_first_arg_type(name: &str) -> Option<InferredType> {
match name {
"abs" | "int" | "sqrt" | "chr"
| "sin" | "cos" | "atan2" | "log" | "exp" => Some(InferredType::Numeric),
"uc" | "lc" | "ucfirst" | "lcfirst" | "length" | "chomp" | "chop"
| "substr" | "index" | "rindex" | "quotemeta"
| "hex" | "oct" | "ord" => Some(InferredType::String),
_ => None,
}
}
pub fn inferred_type_to_tag(ty: &InferredType) -> String {
match ty {
InferredType::ClassName(name) => format!("Object:{}", name),
InferredType::FirstParam { package } => format!("Object:{}", package),
InferredType::HashRef => "HashRef".to_string(),
InferredType::ArrayRef => "ArrayRef".to_string(),
InferredType::CodeRef => "CodeRef".to_string(),
InferredType::Regexp => "Regexp".to_string(),
InferredType::Numeric => "Numeric".to_string(),
InferredType::String => "String".to_string(),
}
}
pub fn inferred_type_from_tag(tag: &str) -> Option<InferredType> {
if let Some(class_name) = tag.strip_prefix("Object:") {
return Some(InferredType::ClassName(class_name.to_string()));
}
match tag {
"HashRef" => Some(InferredType::HashRef),
"ArrayRef" => Some(InferredType::ArrayRef),
"CodeRef" => Some(InferredType::CodeRef),
"Regexp" => Some(InferredType::Regexp),
"Numeric" => Some(InferredType::Numeric),
"String" => Some(InferredType::String),
_ => None,
}
}
fn format_cross_file_signature(method_name: &str, sub_info: &crate::module_index::ExportedSub) -> String {
if sub_info.params.is_empty() {
format!("sub {}()", method_name)
} else {
let params: Vec<&str> = sub_info.params.iter().map(|p| p.name.as_str()).collect();
format!("sub {}({})", method_name, params.join(", "))
}
}
pub(crate) fn format_inferred_type(ty: &InferredType) -> String {
match ty {
InferredType::ClassName(name) => name.clone(),
InferredType::FirstParam { package } => package.clone(),
InferredType::HashRef => "HashRef".to_string(),
InferredType::ArrayRef => "ArrayRef".to_string(),
InferredType::CodeRef => "CodeRef".to_string(),
InferredType::Regexp => "Regexp".to_string(),
InferredType::Numeric => "Numeric".to_string(),
InferredType::String => "String".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tree_sitter::Point;
fn fa_with_constraints(constraints: Vec<TypeConstraint>) -> FileAnalysis {
FileAnalysis::new(
vec![Scope {
id: ScopeId(0),
parent: None,
kind: ScopeKind::File,
span: Span { start: Point::new(0, 0), end: Point::new(10, 0) },
package: None,
}],
vec![],
vec![],
constraints,
vec![],
vec![],
vec![],
HashMap::new(),
vec![],
HashSet::new(),
vec![],
vec![],
)
}
fn constraint(var: &str, row: usize, inferred_type: InferredType) -> TypeConstraint {
TypeConstraint {
variable: var.to_string(),
scope: ScopeId(0),
constraint_span: Span { start: Point::new(row, 0), end: Point::new(row, 20) },
inferred_type,
}
}
#[test]
fn test_resolve_hashref_type() {
let fa = fa_with_constraints(vec![constraint("$href", 0, InferredType::HashRef)]);
assert_eq!(fa.inferred_type("$href", Point::new(1, 0)), Some(&InferredType::HashRef));
}
#[test]
fn test_resolve_arrayref_type() {
let fa = fa_with_constraints(vec![constraint("$aref", 0, InferredType::ArrayRef)]);
assert_eq!(fa.inferred_type("$aref", Point::new(1, 0)), Some(&InferredType::ArrayRef));
}
#[test]
fn test_resolve_coderef_type() {
let fa = fa_with_constraints(vec![constraint("$cref", 0, InferredType::CodeRef)]);
assert_eq!(fa.inferred_type("$cref", Point::new(1, 0)), Some(&InferredType::CodeRef));
}
#[test]
fn test_resolve_regexp_type() {
let fa = fa_with_constraints(vec![constraint("$re", 0, InferredType::Regexp)]);
assert_eq!(fa.inferred_type("$re", Point::new(1, 0)), Some(&InferredType::Regexp));
}
#[test]
fn test_resolve_numeric_type() {
let fa = fa_with_constraints(vec![constraint("$n", 0, InferredType::Numeric)]);
assert_eq!(fa.inferred_type("$n", Point::new(1, 0)), Some(&InferredType::Numeric));
}
#[test]
fn test_resolve_string_type() {
let fa = fa_with_constraints(vec![constraint("$s", 0, InferredType::String)]);
assert_eq!(fa.inferred_type("$s", Point::new(1, 0)), Some(&InferredType::String));
}
#[test]
fn test_resolve_reassignment_changes_type() {
let fa = fa_with_constraints(vec![
constraint("$x", 0, InferredType::HashRef),
constraint("$x", 3, InferredType::ArrayRef),
]);
assert_eq!(fa.inferred_type("$x", Point::new(1, 0)), Some(&InferredType::HashRef));
assert_eq!(fa.inferred_type("$x", Point::new(4, 0)), Some(&InferredType::ArrayRef));
}
#[test]
fn test_resolve_sub_return_type() {
let fa = FileAnalysis::new(
vec![Scope {
id: ScopeId(0),
parent: None,
kind: ScopeKind::File,
span: Span { start: Point::new(0, 0), end: Point::new(10, 0) },
package: None,
}],
vec![Symbol {
id: SymbolId(0),
name: "get_config".to_string(),
kind: SymKind::Sub,
span: Span { start: Point::new(0, 0), end: Point::new(2, 1) },
selection_span: Span { start: Point::new(0, 4), end: Point::new(0, 14) },
scope: ScopeId(0),
package: None,
detail: SymbolDetail::Sub {
params: vec![],
is_method: false,
return_type: Some(InferredType::HashRef),
doc: None,
},
}],
vec![],
vec![],
vec![],
vec![],
vec![],
HashMap::new(),
vec![],
HashSet::new(),
vec![],
vec![],
);
assert_eq!(fa.sub_return_type("get_config"), Some(&InferredType::HashRef));
assert_eq!(fa.sub_return_type("nonexistent"), None);
}
#[test]
fn test_resolve_return_type_all_agree() {
assert_eq!(
resolve_return_type(&[InferredType::HashRef, InferredType::HashRef]),
Some(InferredType::HashRef),
);
}
#[test]
fn test_resolve_return_type_disagreement() {
assert_eq!(
resolve_return_type(&[InferredType::HashRef, InferredType::ArrayRef]),
None,
);
}
#[test]
fn test_resolve_return_type_empty() {
assert_eq!(resolve_return_type(&[]), None);
}
#[test]
fn test_resolve_return_type_object_subsumes_hashref() {
assert_eq!(
resolve_return_type(&[
InferredType::ClassName("Foo".into()),
InferredType::HashRef,
]),
Some(InferredType::ClassName("Foo".into())),
);
assert_eq!(
resolve_return_type(&[
InferredType::HashRef,
InferredType::ClassName("Foo".into()),
]),
Some(InferredType::ClassName("Foo".into())),
);
}
#[test]
fn test_resolve_return_type_object_does_not_subsume_arrayref() {
assert_eq!(
resolve_return_type(&[
InferredType::ClassName("Foo".into()),
InferredType::ArrayRef,
]),
None,
);
}
#[test]
fn test_resolve_return_type_single() {
assert_eq!(
resolve_return_type(&[InferredType::CodeRef]),
Some(InferredType::CodeRef),
);
}
#[test]
fn test_class_name_helper() {
assert_eq!(InferredType::ClassName("Foo".into()).class_name(), Some("Foo"));
assert_eq!(InferredType::FirstParam { package: "Bar".into() }.class_name(), Some("Bar"));
assert_eq!(InferredType::HashRef.class_name(), None);
assert_eq!(InferredType::ArrayRef.class_name(), None);
assert_eq!(InferredType::CodeRef.class_name(), None);
assert_eq!(InferredType::Regexp.class_name(), None);
assert_eq!(InferredType::Numeric.class_name(), None);
assert_eq!(InferredType::String.class_name(), None);
}
#[test]
fn test_inferred_type_tag_roundtrip() {
let cases = vec![
InferredType::ClassName("Foo::Bar".into()),
InferredType::FirstParam { package: "Baz".into() },
InferredType::HashRef,
InferredType::ArrayRef,
InferredType::CodeRef,
InferredType::Regexp,
InferredType::Numeric,
InferredType::String,
];
for ty in &cases {
let tag = inferred_type_to_tag(ty);
let restored = inferred_type_from_tag(&tag);
assert!(restored.is_some(), "Failed to deserialize tag: {}", tag);
match ty {
InferredType::FirstParam { package } => {
assert_eq!(restored.unwrap(), InferredType::ClassName(package.clone()));
}
_ => {
assert_eq!(&restored.unwrap(), ty, "Roundtrip failed for tag: {}", tag);
}
}
}
}
#[test]
fn test_inferred_type_from_tag_unknown() {
assert_eq!(inferred_type_from_tag("UnknownTag"), None);
assert_eq!(inferred_type_from_tag(""), None);
}
#[test]
fn test_sub_return_type_imported_fallback() {
let mut fa = fa_with_constraints(vec![]);
let mut imported = HashMap::new();
imported.insert("get_config".to_string(), InferredType::HashRef);
fa.enrich_imported_types(imported);
assert_eq!(fa.sub_return_type("get_config"), Some(&InferredType::HashRef));
assert_eq!(fa.sub_return_type("nonexistent"), None);
}
#[test]
fn test_enrich_imported_types_pushes_constraints() {
let mut fa = FileAnalysis::new(
vec![Scope {
id: ScopeId(0),
parent: None,
kind: ScopeKind::File,
span: Span { start: Point::new(0, 0), end: Point::new(10, 0) },
package: None,
}],
vec![],
vec![],
vec![],
vec![],
vec![],
vec![CallBinding {
variable: "$cfg".to_string(),
func_name: "get_config".to_string(),
scope: ScopeId(0),
span: Span { start: Point::new(2, 0), end: Point::new(2, 30) },
}],
HashMap::new(),
vec![],
HashSet::new(),
vec![],
vec![],
);
let mut imported = HashMap::new();
imported.insert("get_config".to_string(), InferredType::HashRef);
fa.enrich_imported_types(imported);
assert_eq!(
fa.inferred_type("$cfg", Point::new(3, 0)),
Some(&InferredType::HashRef),
"Enrichment should propagate imported return type to call binding variable"
);
}
}