use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use tree_sitter::Point;
#[derive(Debug)]
pub struct CachedModule {
pub path: std::path::PathBuf,
pub analysis: std::sync::Arc<FileAnalysis>,
}
impl CachedModule {
pub fn new(path: std::path::PathBuf, analysis: std::sync::Arc<FileAnalysis>) -> Self {
CachedModule { path, analysis }
}
pub fn sub_info(&self, name: &str) -> Option<SubInfo<'_>> {
let mut syms = self.analysis.symbols.iter().filter(|s| {
s.name == name && matches!(s.kind, SymKind::Sub | SymKind::Method)
});
let primary = syms.next()?;
let overloads: Vec<&Symbol> = syms.collect();
let hash_keys: Vec<String> = self
.analysis
.hash_key_defs_for_owner(&HashKeyOwner::Sub {
package: primary.package.clone(),
name: name.to_string(),
})
.iter()
.map(|s| s.name.clone())
.collect();
Some(SubInfo {
analysis: &self.analysis,
primary,
overloads,
hash_keys,
})
}
pub fn package_var_def_line(&self, name: &str, package: &str) -> Option<u32> {
self.analysis
.symbols
.iter()
.find(|s| {
matches!(s.kind, SymKind::Variable | SymKind::Field)
&& s.name == name
&& s.package.as_deref() == Some(package)
})
.map(|s| s.span.start.row as u32)
}
pub fn has_sub(&self, name: &str) -> bool {
self.analysis.symbols.iter().any(|s| {
s.name == name && matches!(s.kind, SymKind::Sub | SymKind::Method)
})
}
pub fn has_sub_in_package(&self, name: &str, package: &str) -> bool {
self.analysis.symbols.iter().any(|s| {
s.name == name
&& matches!(s.kind, SymKind::Sub | SymKind::Method)
&& s.package.as_deref() == Some(package)
})
}
}
pub struct SubInfo<'a> {
analysis: &'a FileAnalysis,
primary: &'a Symbol,
#[allow(dead_code)] overloads: Vec<&'a Symbol>,
hash_keys: Vec<String>,
}
impl<'a> SubInfo<'a> {
pub fn def_line(&self) -> u32 {
self.primary.span.start.row as u32
}
pub fn params(&self) -> &'a [ParamInfo] {
match &self.primary.detail {
SymbolDetail::Sub { params, .. } => params,
_ => &[],
}
}
pub fn is_method(&self) -> bool {
if self.primary.kind == SymKind::Method {
return true;
}
matches!(
self.primary.detail,
SymbolDetail::Sub { is_method: true, .. }
)
}
pub fn return_type(&self, module_index: Option<&dyn CrossFileLookup>) -> Option<InferredType> {
match &self.primary.detail {
SymbolDetail::Sub { .. } => {
self.analysis.symbol_return_type_via_bag_ctx(self.primary.id, None, module_index)
}
_ => None,
}
}
pub fn doc(&self) -> Option<&'a str> {
match &self.primary.detail {
SymbolDetail::Sub { doc, .. } => doc.as_deref(),
_ => None,
}
}
pub fn hash_keys(&self) -> &[String] {
&self.hash_keys
}
#[allow(dead_code)] pub fn param_counts(&self) -> Vec<usize> {
std::iter::once(self.primary)
.chain(self.overloads.iter().copied())
.map(|s| match &s.detail {
SymbolDetail::Sub { params, .. } => params.len(),
_ => 0,
})
.collect()
}
#[allow(dead_code)] pub fn return_type_for_arity(&self, arity: usize, module_index: Option<&dyn CrossFileLookup>) -> Option<InferredType> {
for sym in std::iter::once(self.primary).chain(self.overloads.iter().copied()) {
if let SymbolDetail::Sub { params, .. } = &sym.detail {
if params.len() == arity {
return self.analysis.symbol_return_type_via_bag_ctx(sym.id, Some(arity), module_index);
}
}
}
None
}
#[allow(dead_code)] pub fn primary_id(&self) -> SymbolId {
self.primary.id
}
#[allow(dead_code)] pub fn id_for_arity(&self, arity: usize) -> Option<SymbolId> {
for sym in std::iter::once(self.primary).chain(self.overloads.iter().copied()) {
if let SymbolDetail::Sub { params, .. } = &sym.detail {
if params.len() == arity {
return Some(sym.id);
}
}
}
None
}
pub fn param_inferred_type(&self, param_name: &str) -> Option<InferredType> {
self.analysis
.inferred_type_via_bag(param_name, self.primary.span.end)
}
}
pub trait CrossFileLookup {
fn get_cached(&self, module_name: &str) -> Option<std::sync::Arc<CachedModule>>;
fn parents_cached(&self, module_name: &str) -> Vec<String>;
fn modules_with_symbol(&self, name: &str) -> Vec<String>;
fn find_exporters(&self, func_name: &str) -> Vec<String>;
fn defining_module_cached(&self, entry: &str, name: &str) -> Option<std::sync::Arc<CachedModule>>;
fn module_declaring_method_in_package(&self, name: &str, class: &str) -> Option<String>;
fn for_each_cached(&self, f: &mut dyn FnMut(&str, &std::sync::Arc<CachedModule>));
fn for_each_reexport_module(
&self,
start: Vec<String>,
visit: &mut dyn FnMut(&std::sync::Arc<CachedModule>) -> std::ops::ControlFlow<()>,
);
fn for_each_entity_bridged_to(
&self,
class_name: &str,
f: &mut dyn FnMut(&str, &std::sync::Arc<CachedModule>, &Symbol),
);
fn direct_children_of(&self, class: &str) -> Vec<(String, String)>;
fn for_each_loader_shape(&self, f: &mut dyn FnMut(&str, &InferredType));
}
#[derive(Serialize, Deserialize)]
#[serde(remote = "Point")]
pub(crate) struct PointDef {
pub row: usize,
pub column: usize,
}
pub(crate) mod point_opt_serde {
use super::PointDef;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tree_sitter::Point;
pub fn serialize<S: Serializer>(val: &Option<Point>, s: S) -> Result<S::Ok, S::Error> {
#[derive(Serialize)]
struct W<'a>(#[serde(with = "PointDef")] &'a Point);
val.as_ref().map(W).serialize(s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Point>, D::Error> {
#[derive(Deserialize)]
struct W(#[serde(with = "PointDef")] Point);
Option::<W>::deserialize(d).map(|o| o.map(|W(p)| p))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Span {
#[serde(with = "PointDef")]
pub start: Point,
#[serde(with = "PointDef")]
pub end: Point,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FoldRange {
pub start_line: usize,
pub end_line: usize,
pub kind: FoldKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[allow(dead_code)]
pub enum FoldKind {
Region,
Comment,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ScopeId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SymbolId(pub u32);
#[derive(Debug, Clone, Serialize, Deserialize)]
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, Serialize, Deserialize)]
#[allow(dead_code)]
pub enum ScopeKind {
File,
Class { name: String },
Sub { name: String },
Method { name: String },
Block,
ForLoop { var: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageRange {
pub package: String,
pub span: Span,
pub kind: PackageKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PackageKind {
Statement,
Block,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Namespace {
Language,
Framework { id: String },
}
impl Default for Namespace {
fn default() -> Self { Self::Language }
}
impl Namespace {
pub fn framework(id: impl Into<String>) -> Self {
Self::Framework { id: id.into() }
}
pub fn is_framework(&self) -> bool {
matches!(self, Self::Framework { .. })
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
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,
#[serde(default)]
pub namespace: Namespace,
#[serde(default)]
pub outline_label: Option<String>,
}
impl Symbol {
pub fn bare_name(&self) -> &str {
match &self.detail {
SymbolDetail::Variable { sigil, .. } | SymbolDetail::Field { sigil, .. } => {
let off = sigil.len_utf8();
self.name.get(off..).unwrap_or(&self.name)
}
_ => &self.name,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SymKind {
Variable,
Sub,
Method,
Package,
Class,
Module,
Field,
HashKeyDef,
Handler,
Namespace,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub enum SymbolDetail {
Variable {
sigil: char,
decl_kind: DeclKind,
},
Sub {
params: Vec<ParamInfo>,
is_method: bool,
doc: Option<String>,
#[serde(default)]
display: Option<HandlerDisplay>,
#[serde(default)]
hide_in_outline: bool,
#[serde(default)]
opaque_return: bool,
#[serde(default)]
is_constant: bool,
#[serde(default)]
lexical: bool,
},
Class {
parent: Option<String>,
roles: Vec<String>,
fields: Vec<FieldDetail>,
},
Field {
sigil: char,
attributes: Vec<String>,
},
HashKeyDef {
owner: HashKeyOwner,
is_dynamic: bool,
},
Handler {
owner: HandlerOwner,
dispatchers: Vec<String>,
params: Vec<ParamInfo>,
#[serde(default)]
display: HandlerDisplay,
#[serde(default)]
hide_in_outline: bool,
},
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeclKind {
My,
Our,
State,
Field,
Param,
ForVar,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParamInfo {
pub name: String,
pub default: Option<String>,
pub is_slurpy: bool,
#[serde(default)]
pub is_invocant: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct FieldDetail {
pub name: String,
pub sigil: char,
pub attributes: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ref {
pub kind: RefKind,
pub span: Span,
pub scope: ScopeId,
pub target_name: String,
pub access: AccessKind,
pub resolves_to: Option<SymbolId>,
#[serde(default)]
pub resolved_method_target: Option<MethodTarget>,
}
pub fn split_qualified(name: &str) -> (Option<&str>, &str) {
match name.rsplit_once("::") {
Some((pkg, base)) => (Some(pkg), base),
None => (None, name),
}
}
impl Ref {
pub fn unqualified_target_name(&self) -> &str {
split_qualified(&self.target_name).1
}
pub fn qualified_var_target(&self) -> Option<(&str, String)> {
let mut chars = self.target_name.chars();
let sigil = chars.next()?;
if !matches!(sigil, '$' | '@' | '%') {
return None;
}
let (pkg, base) = split_qualified(chars.as_str());
pkg.map(|p| (p, format!("{sigil}{base}")))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MethodTarget {
Local { sym_id: SymbolId, invocant_class: String },
CrossFile { invocant_class: String },
}
impl MethodTarget {
pub fn invocant_class(&self) -> &str {
match self {
MethodTarget::Local { invocant_class, .. }
| MethodTarget::CrossFile { invocant_class } => invocant_class,
}
}
}
#[derive(Debug)]
pub enum RenameKind {
Variable,
Function { name: String, package: Option<String> },
Package(String),
Method { name: String, class: String },
HashKey(String),
Handler { owner: HandlerOwner, name: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[allow(dead_code)]
pub enum RefKind {
Variable,
FunctionCall {
#[serde(default)]
resolved_package: Option<String>,
},
MethodCall {
invocant: crate::conventions::InvocantName,
invocant_span: Option<Span>,
method_name_span: Span,
},
PackageRef,
HashKeyAccess {
var_text: String,
owner: Option<HashKeyOwner>,
},
ContainerAccess,
DispatchCall {
dispatcher: String,
owner: Option<HandlerOwner>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AccessKind {
Read,
Write,
Declaration,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TypeConstraint {
pub variable: String,
pub scope: ScopeId,
pub constraint_span: Span,
pub inferred_type: InferredType,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum InferredType {
ClassName(String),
FirstParam { package: String },
HashRef,
ArrayRef,
CodeRef { return_edge: Option<crate::witnesses::WitnessAttachment> },
Regexp,
Numeric,
String,
Parametric(ParametricType),
Sequence(Vec<InferredType>),
TypeConstraintOf(Box<InferredType>),
BrandedRoute {
base: String,
controller: Option<String>,
stash: Vec<(String, String)>,
},
HashWithKeys {
keys: Vec<(String, Option<Box<InferredType>>)>,
open: bool,
},
Optional(Box<InferredType>),
Undef,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ParametricType {
ResultSet { base: String, row: String },
}
impl ParametricType {
pub fn class_name(&self) -> Option<&str> {
match self {
ParametricType::ResultSet { base, .. } => Some(base.as_str()),
}
}
pub fn hash_key_class(&self) -> Option<&str> {
match self {
ParametricType::ResultSet { row, .. } => Some(row.as_str()),
}
}
pub fn method_arg_owner(&self, method: &str) -> Option<HashKeyOwner> {
match self {
ParametricType::ResultSet { row, .. } => match method {
"search" | "search_rs" | "find" | "find_or_new" | "find_or_create"
| "update_or_create" | "create" | "update" | "populate" | "new_result" => {
Some(HashKeyOwner::Class(row.clone()))
}
_ => None,
},
}
}
pub fn return_method_declarations(
&self,
) -> Vec<(&'static str, crate::witnesses::ReturnExpr)> {
match self {
ParametricType::ResultSet { .. } => {
let row_of_receiver = crate::witnesses::ReturnExpr::Operator(
crate::witnesses::ParametricOp::RowOf(Box::new(
crate::witnesses::ReturnExpr::Receiver,
)),
);
["find", "first", "single", "next", "create",
"find_or_new", "find_or_create", "update_or_create",
"new_result"]
.iter()
.map(|m| (*m, row_of_receiver.clone()))
.collect()
}
}
}
}
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()),
InferredType::Parametric(p) => p.class_name(),
InferredType::BrandedRoute { base, .. } => Some(base.as_str()),
_ => None,
}
}
pub fn optional_inner(&self) -> Option<&InferredType> {
match self {
InferredType::Optional(inner) => Some(inner),
_ => None,
}
}
pub fn is_hash_shaped(&self) -> bool {
matches!(
self,
InferredType::HashRef | InferredType::HashWithKeys { .. }
)
}
pub fn is_array_shaped(&self) -> bool {
matches!(self, InferredType::ArrayRef | InferredType::Sequence(_))
}
pub fn key_value_type(&self, key: &str) -> Option<Option<&InferredType>> {
match self {
InferredType::HashWithKeys { keys, .. } => keys
.iter()
.find(|(k, _)| k == key)
.map(|(_, t)| t.as_deref()),
_ => None,
}
}
#[allow(dead_code)]
pub fn route_default(&self, key: &str) -> Option<&str> {
let InferredType::BrandedRoute { controller, stash, .. } = self else {
return None;
};
if key == "controller" {
return controller.as_deref();
}
stash.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str())
}
pub fn constrained_inner(&self) -> Option<&InferredType> {
match self {
InferredType::TypeConstraintOf(inner) => Some(inner),
_ => None,
}
}
pub fn element_at(&self, i: i32) -> Option<&InferredType> {
let InferredType::Sequence(elems) = self else { return None };
let n = elems.len() as i32;
let idx = if i < 0 { n + i } else { i };
if idx < 0 || idx >= n { return None; }
elems.get(idx as usize)
}
pub fn is_object(&self) -> bool {
self.class_name().is_some()
}
pub fn hash_key_class(&self) -> Option<&str> {
match self {
InferredType::Parametric(p) => p.hash_key_class(),
_ => self.class_name(),
}
}
pub fn as_parametric(&self) -> Option<&ParametricType> {
match self {
InferredType::Parametric(p) => Some(p),
_ => None,
}
}
pub fn callable_return_edge(&self) -> Option<&crate::witnesses::WitnessAttachment> {
match self {
InferredType::CodeRef { return_edge } => return_edge.as_ref(),
_ => None,
}
}
pub fn subsumes_narrowing(&self, narrowing: &InferredType) -> bool {
match (self, narrowing) {
(
InferredType::CodeRef { return_edge: have },
InferredType::CodeRef { return_edge: want },
) => want.is_none() || have.is_some(),
(InferredType::ClassName(a), InferredType::ClassName(b)) => a == b,
(InferredType::FirstParam { package: a }, InferredType::FirstParam { package: b }) => {
a == b
}
(InferredType::Parametric(a), InferredType::Parametric(b)) => a == b,
(
InferredType::BrandedRoute { controller: hc, stash: hs, .. },
InferredType::BrandedRoute { controller: wc, stash: ws, .. },
) => (wc.is_none() || hc.is_some()) && ws.len() <= hs.len(),
(InferredType::HashWithKeys { .. }, InferredType::HashRef) => true,
(a @ InferredType::HashWithKeys { .. }, b @ InferredType::HashWithKeys { .. }) => {
a == b
}
(InferredType::Sequence(_), InferredType::ArrayRef) => true,
(a @ InferredType::Sequence(_), b @ InferredType::Sequence(_)) => a == b,
(InferredType::Optional(a), InferredType::Optional(b)) => a.subsumes_narrowing(b),
(a, InferredType::Optional(b)) => a.subsumes_narrowing(b),
(a, b) => std::mem::discriminant(a) == std::mem::discriminant(b),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TypeProvenance {
Inferred,
PluginOverride { plugin_id: String, reason: String },
ReducerFold { reducer: String, evidence: Vec<String> },
Delegation { kind: String, via: String },
FrameworkSynthesis { framework: String, reason: String },
}
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());
}
if return_types.iter().all(|t| t.is_hash_shaped()) {
return Some(InferredType::HashRef);
}
if return_types.iter().all(|t| t.is_array_shaped()) {
return Some(InferredType::ArrayRef);
}
let mut object = None;
for t in return_types {
if t.is_object() {
object = Some(t.clone());
} else if !t.is_hash_shaped() {
return None;
}
}
object
}
pub fn join_return_arms(value_types: &[InferredType], has_undef_arm: bool) -> Option<InferredType> {
let base = resolve_return_type(value_types);
match base {
Some(t) if has_undef_arm && !matches!(t, InferredType::Optional(_)) => {
Some(InferredType::Optional(Box::new(t)))
}
other => other,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum HandlerDisplay {
Event,
Method,
Function,
Field,
Property,
Constant,
Helper,
Route,
Action,
Task,
}
impl Default for HandlerDisplay {
fn default() -> Self { Self::Event }
}
impl HandlerDisplay {
pub fn outline_word(&self) -> Option<&'static str> {
match self {
HandlerDisplay::Event => Some("event"),
HandlerDisplay::Helper => Some("helper"),
HandlerDisplay::Route => Some("route"),
HandlerDisplay::Action => Some("action"),
HandlerDisplay::Task => Some("task"),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum HandlerOwner {
Class(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginNamespace {
pub id: String,
pub plugin_id: String,
pub kind: String,
pub entities: Vec<SymbolId>,
pub bridges: Vec<Bridge>,
pub decl_span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Bridge {
Class(String),
}
pub const APP_SURFACE_CLASS: &str = "Mojolicious::_AppSurface";
pub fn scope_chain_of(scopes: &[Scope], start: ScopeId) -> Vec<ScopeId> {
let mut chain = Vec::new();
let mut current = Some(start);
while let Some(id) = current {
chain.push(id);
current = scopes[id.0 as usize].parent;
}
chain
}
pub fn parents_of(
class: &str,
package_parents: &HashMap<String, Vec<String>>,
module_index: Option<&dyn CrossFileLookup>,
consumers: &[String],
) -> Vec<String> {
let mut parents: Vec<String> = package_parents.get(class).cloned().unwrap_or_default();
if let Some(idx) = module_index {
for p in idx.parents_cached(class) {
if !parents.contains(&p) {
parents.push(p);
}
}
}
if class != APP_SURFACE_CLASS
&& consumers.iter().any(|c| c == class)
&& !parents.iter().any(|p| p == APP_SURFACE_CLASS)
{
parents.push(APP_SURFACE_CLASS.to_string());
}
parents
}
pub fn class_isa(
class: &str,
target: &str,
package_parents: &HashMap<String, Vec<String>>,
module_index: Option<&dyn CrossFileLookup>,
) -> bool {
if class == target {
return true;
}
let mut seen: HashSet<String> = HashSet::new();
let mut stack: Vec<String> = vec![class.to_string()];
let mut budget = 0;
while let Some(cur) = stack.pop() {
if budget > 200 {
break;
}
budget += 1;
if !seen.insert(cur.clone()) {
continue;
}
if cur == target {
return true;
}
if let Some(parents) = package_parents.get(&cur) {
for p in parents {
stack.push(p.clone());
}
}
if let Some(idx) = module_index {
for p in idx.parents_cached(&cur) {
stack.push(p);
}
}
}
false
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GateResult<U> {
Applies(U),
DoesNotApply,
ReceiverUntyped,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceiverGated<T> {
gate: String,
inner: T,
}
impl<T> ReceiverGated<T> {
pub fn new(gate: impl Into<String>, inner: T) -> Self {
Self { gate: gate.into(), inner }
}
pub fn gate(&self) -> &str {
&self.gate
}
pub fn resolve_for(
&self,
receiver_class: Option<&str>,
package_parents: &HashMap<String, Vec<String>>,
module_index: Option<&dyn CrossFileLookup>,
) -> GateResult<&T> {
match receiver_class {
None => GateResult::ReceiverUntyped,
Some(recv) if recv.is_empty() => GateResult::ReceiverUntyped,
Some(recv) => {
if class_isa(recv, &self.gate, package_parents, module_index) {
GateResult::Applies(&self.inner)
} else {
GateResult::DoesNotApply
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum HashKeyOwner {
Class(String),
Variable { name: String, def_scope: ScopeId },
Sub { package: Option<String>, name: String },
}
impl HashKeyOwner {
pub fn found_by(&self, lookup: &HashKeyOwner) -> bool {
if self == lookup { return true; }
match (self, lookup) {
(HashKeyOwner::Sub { package: Some(c1), .. }, HashKeyOwner::Class(c2)) => c1 == c2,
_ => false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallBinding {
pub variable: String,
pub func_name: String,
pub scope: ScopeId,
pub span: Span,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyWrite {
pub var_text: String,
pub key: WriteKey,
pub scope: ScopeId,
pub span: Span,
pub rhs_span: Option<Span>,
pub conditional: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WriteKey {
Hash(String),
Index(i32),
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MethodCallBinding {
pub variable: String,
pub invocant_var: String,
pub method_name: String,
pub scope: ScopeId,
pub span: Span,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DispatchCandidate {
pub name: String,
pub span: Span,
pub dispatcher: String,
pub owner_class: String,
#[serde(default)]
pub receiver_class: Option<String>,
pub call_span: Span,
}
pub type ProvisionalDispatch = ReceiverGated<DispatchCandidate>;
impl ProvisionalDispatch {
fn receiver_hint(&self) -> Option<&String> {
self.inner.receiver_class.as_ref()
}
fn call_span(&self) -> Span {
self.inner.call_span
}
fn dispatcher(&self) -> &str {
&self.inner.dispatcher
}
}
#[derive(Debug, Clone)]
pub struct AppliedDispatch {
pub name: String,
pub span: Span,
pub owner: HandlerOwner,
}
#[derive(Debug, Clone)]
pub struct UntypedDispatch {
pub call_span: Span,
pub dispatcher: String,
pub gate: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ImportedSymbol {
pub local_name: String,
#[serde(default)]
pub remote_name: Option<String>,
}
impl ImportedSymbol {
pub fn same(name: impl Into<String>) -> Self {
Self { local_name: name.into(), remote_name: None }
}
#[allow(dead_code)]
pub fn renamed(local: impl Into<String>, remote: impl Into<String>) -> Self {
let remote = remote.into();
let local = local.into();
if remote == local {
Self { local_name: local, remote_name: None }
} else {
Self { local_name: local, remote_name: Some(remote) }
}
}
pub fn remote(&self) -> &str {
self.remote_name.as_deref().unwrap_or(&self.local_name)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Import {
pub module_name: String,
pub imported_symbols: Vec<ImportedSymbol>,
pub span: Span,
#[serde(with = "point_opt_serde")]
pub qw_close_paren: Option<Point>,
#[serde(default)]
pub empty_import: bool,
}
pub struct ExportSurface<'a> {
analysis: &'a FileAnalysis,
default_set: Option<Vec<String>>,
optional_set: Option<Vec<String>>,
tags: Option<HashMap<String, Vec<String>>>,
all_names: Option<HashSet<String>>,
}
impl<'a> ExportSurface<'a> {
pub fn default_set(&self) -> &[String] {
self.default_set.as_deref().unwrap_or(&self.analysis.export)
}
pub fn optional_set(&self) -> &[String] {
self.optional_set.as_deref().unwrap_or(&self.analysis.export_ok)
}
pub fn tag_members(&self, tag: &str) -> Option<Vec<&str>> {
if tag.eq_ignore_ascii_case("DEFAULT") {
return Some(self.default_set().iter().map(|s| s.as_str()).collect());
}
if let Some(tags) = &self.tags {
return tags.get(tag).map(|v| v.iter().map(|s| s.as_str()).collect());
}
self.analysis
.export_tags
.get(tag)
.map(|v| v.iter().map(|s| s.as_str()).collect())
}
pub fn exports(&self, name: &str) -> bool {
if let Some(all) = &self.all_names {
return all.contains(name);
}
self.analysis.exports_name(name)
}
pub fn all_names(&self) -> HashSet<String> {
if let Some(all) = &self.all_names {
return all.clone();
}
self.analysis
.export
.iter()
.chain(self.analysis.export_ok.iter())
.cloned()
.collect()
}
}
enum ImportSelector<'a> {
Tag(&'a str),
Rename { local: &'a str, remote: &'a str },
Name(&'a str),
}
pub fn imported_names(
import: &Import,
surface: &ExportSurface<'_>,
) -> std::collections::HashSet<(String, String)> {
let mut bound = std::collections::HashSet::new();
if import.empty_import {
return bound;
}
if import.imported_symbols.is_empty() {
for name in surface.default_set().iter().chain(surface.optional_set()) {
bound.insert((name.clone(), name.clone()));
}
return bound;
}
for sym in &import.imported_symbols {
let selector = if sym.remote_name.is_some() {
ImportSelector::Rename { local: &sym.local_name, remote: sym.remote() }
} else if let Some(tag) = sym
.local_name
.strip_prefix(':')
.or_else(|| sym.local_name.strip_prefix('-'))
{
ImportSelector::Tag(tag)
} else {
ImportSelector::Name(&sym.local_name)
};
match selector {
ImportSelector::Tag(tag) => {
if let Some(members) = surface.tag_members(tag) {
for m in members {
bound.insert((m.to_string(), m.to_string()));
}
}
}
ImportSelector::Rename { local, remote } => {
bound.insert((local.to_string(), remote.to_string()));
}
ImportSelector::Name(name) => {
bound.insert((name.to_string(), name.to_string()));
}
}
}
bound
}
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 handler_display: Option<HandlerDisplay>,
}
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;
pub const TOK_ENUM_MEMBER: u32 = 7;
pub const TOK_KEYWORD: u32 = 8;
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 sub_display_override(detail: &SymbolDetail) -> Option<HandlerDisplay> {
if let SymbolDetail::Sub { display, .. } = detail {
*display
} else {
None
}
}
fn sigil_modifier(sigil: char) -> u32 {
match sigil {
'@' => 1 << MOD_ARRAY,
'%' => 1 << MOD_HASH,
_ => 1 << MOD_SCALAR,
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FileAnalysis {
pub scopes: Vec<Scope>,
pub symbols: Vec<Symbol>,
pub refs: Vec<Ref>,
pub fold_ranges: Vec<FoldRange>,
pub imports: Vec<Import>,
pub call_bindings: Vec<CallBinding>,
pub method_call_bindings: Vec<MethodCallBinding>,
#[serde(default)]
pub package_ranges: Vec<PackageRange>,
pub package_parents: HashMap<String, Vec<String>>,
#[serde(default)]
pub app_surface_consumers: Vec<String>,
pub package_uses: HashMap<String, Vec<String>>,
pub framework_imports: HashSet<String>,
pub export: Vec<String>,
pub export_ok: Vec<String>,
#[serde(default)]
pub export_tags: HashMap<String, Vec<String>>,
#[serde(default)]
pub reexport_modules: Vec<String>,
#[serde(default)]
pub plugin_namespaces: Vec<PluginNamespace>,
#[serde(default)]
pub type_provenance: HashMap<SymbolId, TypeProvenance>,
#[serde(default)]
pub package_framework: HashMap<String, crate::witnesses::FrameworkFact>,
#[serde(default)]
pub witnesses: crate::witnesses::WitnessBag,
#[serde(default)]
base_symbol_count: usize,
#[serde(default)]
base_witness_count: usize,
#[serde(default)]
base_ref_count: usize,
#[serde(default)]
pub provisional_dispatches: Vec<ProvisionalDispatch>,
#[serde(default)]
pub gated_param_types: Vec<ReceiverGated<TypeConstraint>>,
#[serde(default)]
pub attr_projections: Vec<AttrProjection>,
#[serde(default)]
pub reassigned_scalars: HashSet<String>,
#[serde(default)]
pub key_writes: Vec<KeyWrite>,
#[serde(default)]
pub role_requires: HashMap<String, Vec<String>>,
#[serde(default)]
pub contract_symbols: HashSet<SymbolId>,
#[serde(default)]
pub dynamic_parent_packages: HashSet<String>,
#[serde(default)]
pub role_packages: HashSet<String>,
#[serde(default)]
pub plugin_loads: Vec<PluginLoadFact>,
#[serde(default)]
pub loader_config_params: Vec<LoaderConfigParam>,
#[serde(skip, default)]
scope_starts: Vec<(Point, ScopeId)>, #[serde(skip, default)]
symbols_by_name: HashMap<String, Vec<SymbolId>>,
#[serde(skip, default)]
symbols_by_scope: HashMap<ScopeId, Vec<SymbolId>>,
#[serde(skip, default)]
refs_by_name: HashMap<String, Vec<usize>>,
#[serde(skip, default)]
refs_by_target: HashMap<SymbolId, Vec<usize>>,
#[serde(skip, default)]
call_ref_by_start: HashMap<Point, usize>,
#[serde(skip, default)]
export_lookup: HashSet<String>,
}
#[derive(Default)]
pub struct FileAnalysisParts {
pub scopes: Vec<Scope>,
pub symbols: Vec<Symbol>,
pub refs: Vec<Ref>,
pub fold_ranges: Vec<FoldRange>,
pub imports: Vec<Import>,
pub call_bindings: Vec<CallBinding>,
pub package_parents: HashMap<String, Vec<String>>,
pub method_call_bindings: Vec<MethodCallBinding>,
pub framework_imports: HashSet<String>,
pub export: Vec<String>,
pub export_ok: Vec<String>,
pub export_tags: HashMap<String, Vec<String>>,
pub reexport_modules: Vec<String>,
pub plugin_namespaces: Vec<PluginNamespace>,
pub package_uses: HashMap<String, Vec<String>>,
pub type_provenance: HashMap<SymbolId, TypeProvenance>,
pub package_ranges: Vec<PackageRange>,
pub app_surface_consumers: Vec<String>,
pub witnesses: crate::witnesses::WitnessBag,
pub package_framework: HashMap<String, crate::witnesses::FrameworkFact>,
pub provisional_dispatches: Vec<ProvisionalDispatch>,
pub gated_param_types: Vec<ReceiverGated<TypeConstraint>>,
pub attr_projections: Vec<AttrProjection>,
pub reassigned_scalars: HashSet<String>,
pub key_writes: Vec<KeyWrite>,
pub role_requires: HashMap<String, Vec<String>>,
pub contract_symbols: HashSet<SymbolId>,
pub dynamic_parent_packages: HashSet<String>,
pub role_packages: HashSet<String>,
pub plugin_loads: Vec<PluginLoadFact>,
pub loader_config_params: Vec<LoaderConfigParam>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginLoadFact {
pub name: String,
pub config_span: Option<Span>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoaderConfigParam {
pub variable: String,
pub scope: ScopeId,
pub in_role: String,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AttrProjection {
pub class: String,
pub attr: String,
pub kind: AttrProjectionKind,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum AttrProjectionKind {
CtorKey,
InternalKey,
Accessor {
method: String,
affix: Option<(String, String)>,
},
}
impl AttrProjection {
pub fn accessor(class: String, attr: String, method: String) -> Self {
let affix = method.find(attr.as_str()).map(|i| {
(
method[..i].to_string(),
method[i + attr.len()..].to_string(),
)
});
AttrProjection {
class,
attr,
kind: AttrProjectionKind::Accessor { method, affix },
}
}
}
pub struct FieldProjections {
pub class: String,
pub bare: String,
pub has_param: bool,
pub has_reader: bool,
pub has_internal: bool,
pub variable_spans: Vec<Span>,
pub mapped: Vec<MappedMember>,
}
#[derive(Debug, Clone)]
pub struct MappedMember {
pub method: String,
pub affix: Option<(String, String)>,
}
struct FieldGroup {
field_sym: Option<SymbolId>,
decl_span: Option<Span>,
class: String,
bare: String,
has_param: bool,
has_reader: bool,
}
impl FileAnalysis {
pub fn new(parts: FileAnalysisParts) -> Self {
let FileAnalysisParts {
scopes,
symbols,
refs,
fold_ranges,
imports,
call_bindings,
package_parents,
method_call_bindings,
framework_imports,
export,
export_ok,
export_tags,
reexport_modules,
plugin_namespaces,
package_uses,
type_provenance,
package_ranges,
app_surface_consumers,
mut witnesses,
package_framework,
provisional_dispatches,
gated_param_types,
attr_projections,
reassigned_scalars,
key_writes,
role_requires,
contract_symbols,
dynamic_parent_packages,
role_packages,
plugin_loads,
loader_config_params,
} = parts;
witnesses.rebuild_index();
let mut fa = FileAnalysis {
scopes,
symbols,
refs,
fold_ranges,
imports,
call_bindings,
method_call_bindings,
package_ranges,
package_parents,
app_surface_consumers,
package_uses,
framework_imports,
export,
export_ok,
export_tags,
reexport_modules,
plugin_namespaces,
type_provenance,
witnesses,
package_framework,
base_symbol_count: 0,
base_witness_count: 0,
base_ref_count: 0,
provisional_dispatches,
gated_param_types,
attr_projections,
reassigned_scalars,
key_writes,
role_requires,
contract_symbols,
dynamic_parent_packages,
role_packages,
plugin_loads,
loader_config_params,
scope_starts: Vec::new(),
symbols_by_name: HashMap::new(),
symbols_by_scope: HashMap::new(),
refs_by_name: HashMap::new(),
refs_by_target: HashMap::new(),
call_ref_by_start: HashMap::new(),
export_lookup: HashSet::new(),
};
fa.build_indices();
fa
}
pub(crate) fn finalize_post_walk(&mut self) {
self.resolve_method_call_types(None);
self.fix_chain_receiver_hash_key_owners(None);
self.stamp_method_call_targets(None);
self.base_symbol_count = self.symbols.len();
self.base_witness_count = self.witnesses.len();
self.base_ref_count = self.refs.len();
}
pub(crate) fn stamp_method_call_targets(&mut self, module_index: Option<&dyn CrossFileLookup>) {
let mut stamped: Vec<(usize, Option<MethodTarget>)> = Vec::new();
for (i, r) in self.refs.iter().enumerate() {
if !matches!(r.kind, RefKind::MethodCall { .. }) {
continue;
}
let target = self
.method_call_invocant_class(r, module_index)
.map(|cn| {
match self.resolve_method_in_ancestors(&cn, r.unqualified_target_name(), module_index) {
Some(MethodResolution::Local { sym_id, .. }) => MethodTarget::Local {
sym_id,
invocant_class: cn,
},
_ => MethodTarget::CrossFile { invocant_class: cn },
}
});
stamped.push((i, target));
}
for (i, target) in stamped {
self.refs[i].resolved_method_target = target;
}
}
fn fix_chain_receiver_hash_key_owners(&mut self, module_index: Option<&dyn CrossFileLookup>) {
let mut owner_fixes: Vec<(usize, HashKeyOwner)> = Vec::new();
for (i, r) in self.refs.iter().enumerate() {
if !matches!(r.kind, RefKind::HashKeyAccess { owner: None, .. }) {
continue;
}
let mut enclosing: Option<&Ref> = None;
let mut enclosing_area: u64 = u64::MAX;
for other in &self.refs {
if !matches!(other.kind, RefKind::MethodCall { .. }) {
continue;
}
if !contains_point(&other.span, r.span.start) {
continue;
}
let area = (other.span.end.row.saturating_sub(other.span.start.row)) as u64
* 10_000
+ other.span.end.column as u64;
if area < enclosing_area {
enclosing = Some(other);
enclosing_area = area;
}
}
let Some(call) = enclosing else { continue };
let Some(ty) = self.method_call_invocant_type(call, module_index) else {
continue;
};
let Some(p) = ty.as_parametric() else { continue };
let Some(o) = p.method_arg_owner(&call.target_name) else { continue };
owner_fixes.push((i, o));
}
for (i, o) in owner_fixes {
if let RefKind::HashKeyAccess { ref mut owner, .. } = self.refs[i].kind {
*owner = Some(o);
}
}
}
pub fn after_deserialize(&mut self) {
self.scope_starts.clear();
self.symbols_by_name.clear();
self.symbols_by_scope.clear();
self.refs_by_name.clear();
self.refs_by_target.clear();
self.call_ref_by_start.clear();
self.export_lookup.clear();
self.build_indices();
}
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);
}
let hashkey_defs: HashMap<(&str, &HashKeyOwner), SymbolId> = self.symbols.iter()
.filter_map(|sym| {
if let SymbolDetail::HashKeyDef { owner, .. } = &sym.detail {
Some(((sym.name.as_str(), owner), sym.id))
} else {
None
}
})
.collect();
let mut hashkey_resolutions: Vec<(usize, SymbolId)> = Vec::new();
for (i, r) in self.refs.iter().enumerate() {
if r.resolves_to.is_some() {
continue;
}
if let RefKind::HashKeyAccess { owner: Some(owner), .. } = &r.kind {
if let Some(&sid) = hashkey_defs.get(&(r.target_name.as_str(), owner)) {
hashkey_resolutions.push((i, sid));
}
}
}
for (idx, sid) in hashkey_resolutions {
self.refs[idx].resolves_to = Some(sid);
}
let handler_defs: HashMap<(&str, &HandlerOwner), SymbolId> = self.symbols.iter()
.filter_map(|sym| {
if let SymbolDetail::Handler { owner, .. } = &sym.detail {
Some(((sym.name.as_str(), owner), sym.id))
} else {
None
}
})
.collect();
let mut handler_resolutions: Vec<(usize, SymbolId)> = Vec::new();
for (i, r) in self.refs.iter().enumerate() {
if r.resolves_to.is_some() { continue; }
if let RefKind::DispatchCall { owner: Some(owner), .. } = &r.kind {
if let Some(&sid) = handler_defs.get(&(r.target_name.as_str(), owner)) {
handler_resolutions.push((i, sid));
}
}
}
for (idx, sid) in handler_resolutions {
self.refs[idx].resolves_to = Some(sid);
}
for (i, r) in self.refs.iter().enumerate() {
self.refs_by_name
.entry(r.target_name.clone())
.or_default()
.push(i);
if let Some(sym_id) = r.resolves_to {
self.refs_by_target.entry(sym_id).or_default().push(i);
}
if matches!(r.kind, RefKind::MethodCall { .. } | RefKind::FunctionCall { .. }) {
let cur = self.call_ref_by_start.get(&r.span.start).copied();
let take = match cur {
None => true,
Some(prev) => {
let prev_span = self.refs[prev].span;
let new_smaller = (r.span.end.row, r.span.end.column)
< (prev_span.end.row, prev_span.end.column);
new_smaller
}
};
if take {
self.call_ref_by_start.insert(r.span.start, i);
}
}
}
self.export_lookup = self.export.iter()
.chain(self.export_ok.iter())
.cloned()
.collect();
}
pub fn refs_to_symbol(&self, sym_id: SymbolId) -> &[usize] {
self.refs_by_target.get(&sym_id).map(|v| v.as_slice()).unwrap_or(&[])
}
pub fn exports_name(&self, name: &str) -> bool {
self.export_lookup.contains(name)
}
pub fn export_surface(&self) -> ExportSurface<'_> {
ExportSurface {
analysis: self,
default_set: None,
optional_set: None,
tags: None,
all_names: None,
}
}
pub fn export_surface_with_index(
&self,
module_index: &dyn CrossFileLookup,
) -> ExportSurface<'_> {
if self.reexport_modules.is_empty() {
return self.export_surface();
}
let mut default_set: Vec<String> = self.export.clone();
let mut optional_set: Vec<String> = self.export_ok.clone();
let mut tags: HashMap<String, Vec<String>> = self.export_tags.clone();
module_index.for_each_reexport_module(
self.reexport_modules.to_vec(),
&mut |cached| {
let a = &cached.analysis;
for n in &a.export {
if !default_set.contains(n) {
default_set.push(n.clone());
}
}
for n in &a.export_ok {
if !optional_set.contains(n) {
optional_set.push(n.clone());
}
}
for (tag, members) in &a.export_tags {
let bucket = tags.entry(tag.clone()).or_default();
for m in members {
if !bucket.contains(m) {
bucket.push(m.clone());
}
}
}
std::ops::ControlFlow::Continue(())
},
);
let mut all_names: HashSet<String> = HashSet::new();
all_names.extend(default_set.iter().cloned());
all_names.extend(optional_set.iter().cloned());
for members in tags.values() {
all_names.extend(members.iter().cloned());
}
ExportSurface {
analysis: self,
default_set: Some(default_set),
optional_set: Some(optional_set),
tags: Some(tags),
all_names: Some(all_names),
}
}
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> {
scope_chain_of(&self.scopes, start)
}
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> {
use crate::witnesses::{WitnessAttachment, WitnessPayload};
let mut best: Option<(&InferredType, Point)> = None;
for w in self.witnesses.all() {
let WitnessAttachment::Variable { name, scope } = &w.attachment else { continue };
if name != var_name { continue; }
let scope_obj = &self.scopes[scope.0 as usize];
if !contains_point(&scope_obj.span, point) { continue; }
if w.span.start > point { continue; }
let WitnessPayload::InferredType(t) = &w.payload else { continue };
if best.is_none() || w.span.start > best.unwrap().1 {
best = Some((t, w.span.start));
}
}
best.map(|(t, _)| t)
}
pub fn inferred_type_via_bag(&self, var_name: &str, point: Point) -> Option<InferredType> {
self.inferred_type_via_bag_ctx(var_name, point, None)
}
pub(crate) fn bag_context<'a>(
&'a self,
module_index: Option<&'a dyn CrossFileLookup>,
) -> crate::witnesses::BagContext<'a> {
crate::witnesses::BagContext {
scopes: &self.scopes,
package_framework: &self.package_framework,
module_index,
package_parents: &self.package_parents,
app_surface_consumers: &self.app_surface_consumers,
}
}
pub fn inferred_type_via_bag_ctx(
&self,
var_name: &str,
point: Point,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<InferredType> {
let scope = self.scope_at(point)?;
if let Some(t) = crate::witnesses::query_variable_type(
&self.witnesses,
&self.bag_context(module_index),
var_name,
scope,
point,
) {
return Some(t);
}
self.gated_param_type_for(var_name, scope, point, module_index)
}
fn gated_param_type_for(
&self,
var: &str,
scope: ScopeId,
point: Point,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<InferredType> {
if self.gated_param_types.is_empty() {
return None;
}
let chain = self.scope_chain(scope);
let pkg = self.package_at(point);
for gated in &self.gated_param_types {
if let GateResult::Applies(tc) =
gated.resolve_for(pkg, &self.package_parents, module_index)
{
if tc.variable == var && chain.contains(&tc.scope) {
return Some(tc.inferred_type.clone());
}
}
}
None
}
#[allow(dead_code)] pub fn method_call_return_type_via_bag(
&self,
ref_idx: usize,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<InferredType> {
use crate::witnesses::{
FrameworkFact, ReducedValue, ReducerQuery, ReducerRegistry,
WitnessAttachment,
};
let att = WitnessAttachment::Expression(crate::witnesses::RefIdx(ref_idx as u32));
let reg = ReducerRegistry::with_defaults();
let ctx = self.bag_context(module_index);
let own_span = self.refs[ref_idx].span;
let receiver = if let RefKind::MethodCall { invocant_span: Some(span), .. } =
&self.refs[ref_idx].kind
{
let strictly_inside = (span.start.row, span.start.column)
>= (own_span.start.row, own_span.start.column)
&& (span.end.row, span.end.column) <= (own_span.end.row, own_span.end.column)
&& *span != own_span;
if strictly_inside {
self.expr_type_at_span(*span, module_index)
} else {
None
}
} else {
None
};
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None,
receiver,
context: Some(&ctx),
};
match reg.query(&self.witnesses, &q) {
ReducedValue::Type(t) => {
if let InferredType::FirstParam { package } = t {
Some(InferredType::ClassName(package))
} else {
Some(t)
}
}
_ => None,
}
}
fn bag_query_expr_span(
&self,
span: Span,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<InferredType> {
use crate::witnesses::{
FrameworkFact, ReducedValue, ReducerQuery, ReducerRegistry,
WitnessAttachment,
};
let att = WitnessAttachment::Expr(span);
let reg = ReducerRegistry::with_defaults();
let ctx = self.bag_context(module_index);
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: None,
receiver: None,
context: Some(&ctx),
};
match reg.query(&self.witnesses, &q) {
ReducedValue::Type(t) => Some(t),
_ => None,
}
}
pub fn expr_type_at_span(
&self,
span: Span,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<InferredType> {
thread_local! {
static EXPR_SPAN_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
const EXPR_SPAN_DEPTH_CAP: u32 = 64;
let depth = EXPR_SPAN_DEPTH.with(|d| {
let n = d.get();
d.set(n + 1);
n
});
struct DepthGuard;
impl Drop for DepthGuard {
fn drop(&mut self) {
EXPR_SPAN_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
}
}
let _guard = DepthGuard;
if depth >= EXPR_SPAN_DEPTH_CAP {
return None;
}
if let Some((recv_idx, kind)) = self.refs.iter().enumerate().find_map(|(i, r)| {
if r.span == span && matches!(r.kind, RefKind::MethodCall { .. } | RefKind::FunctionCall { .. }) {
Some((i, &r.kind))
} else {
None
}
}) {
match kind {
RefKind::MethodCall { .. } => {
if let Some(t) =
self.method_call_return_type_via_bag(recv_idx, module_index)
{
return Some(t);
}
}
RefKind::FunctionCall { .. } => {
if let Some(t) = self.sub_return_type_at_arity(
&self.refs[recv_idx].target_name,
Some(0),
) {
return Some(t);
}
}
_ => {}
}
}
self.bag_query_expr_span(span, module_index)
}
pub fn sub_return_type_at_arity(
&self,
sub_name: &str,
arity: Option<u32>,
) -> Option<InferredType> {
let ctx = self.bag_context(None);
crate::witnesses::query_sub_return_type(
&self.witnesses,
&self.symbols,
sub_name,
arity,
None,
Some(&ctx),
)
}
#[allow(dead_code)] pub fn mutated_keys_on_class(&self, class: &str) -> Vec<String> {
use crate::witnesses::{WitnessAttachment, WitnessPayload};
let mut out: Vec<String> = Vec::new();
for w in self.witnesses.all() {
if let WitnessAttachment::HashKey { owner, name } = &w.attachment {
let matches_class = match owner {
HashKeyOwner::Class(c) if c == class => true,
HashKeyOwner::Sub { package: Some(p), .. } if p == class => true,
_ => false,
};
if !matches_class {
continue;
}
if matches!(
&w.payload,
WitnessPayload::Fact { family, .. } if family == "mutation"
) && !out.contains(name)
{
out.push(name.clone());
}
}
}
out
}
pub fn closed_shape_is_whole_story(&self, var_text: &str) -> bool {
!self.reassigned_scalars.contains(var_text)
}
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 Some(t) = self.symbol_return_type_via_bag(sym.id, None) {
return Some(t);
}
}
}
None
}
#[allow(dead_code)] pub fn sub_return_type(&self, name: &str) -> Option<InferredType> {
self.sub_return_type_at_arity(name, None)
}
#[allow(dead_code)] pub fn return_type_provenance(&self, sym_id: SymbolId) -> TypeProvenance {
self.type_provenance
.get(&sym_id)
.cloned()
.unwrap_or(TypeProvenance::Inferred)
}
fn apply_loader_config_params(&mut self, module_index: Option<&dyn CrossFileLookup>) {
if self.loader_config_params.is_empty() {
return;
}
let Some(idx) = module_index else { return };
let my_packages: Vec<String> = self
.symbols
.iter()
.filter(|s| matches!(s.kind, SymKind::Package | SymKind::Class))
.map(|s| s.name.clone())
.collect();
let matches_me = |load_name: &str| -> bool {
my_packages.iter().any(|p| {
p == load_name || p.rsplit("::").next() == Some(load_name)
})
};
let markers = self.loader_config_params.clone();
for m in &markers {
let pkg = self.scopes.get(m.scope.0 as usize).and_then(|sc| sc.package.clone());
let Some(pkg) = pkg else { continue };
if !self.class_isa(&pkg, &m.in_role, module_index) {
continue;
}
let mut shapes: Vec<InferredType> = Vec::new();
idx.for_each_loader_shape(&mut |load_name, t| {
if matches_me(load_name) {
shapes.push(t.clone());
}
});
if shapes.is_empty() {
continue;
}
let agreed = if shapes.windows(2).all(|w| w[0] == w[1]) {
shapes.pop().unwrap()
} else {
let mut keys: Vec<(String, Option<Box<InferredType>>)> = Vec::new();
let mut all_keyed = true;
for sh in &shapes {
match sh {
InferredType::HashWithKeys { keys: ks, .. } => {
for (k, v) in ks {
match keys.iter_mut().find(|(ek, _)| ek == k) {
None => keys.push((k.clone(), v.clone())),
Some((_, ev)) => {
if *ev != *v {
*ev = None;
}
}
}
}
}
_ => {
all_keyed = false;
break;
}
}
}
if !all_keyed {
continue;
}
InferredType::HashWithKeys { keys, open: true }
};
let span = self
.scopes
.get(m.scope.0 as usize)
.map(|sc| Span { start: sc.span.start, end: sc.span.start })
.unwrap_or(Span {
start: Point { row: 0, column: 0 },
end: Point { row: 0, column: 0 },
});
self.push_type_constraint(TypeConstraint {
variable: m.variable.clone(),
scope: m.scope,
constraint_span: span,
inferred_type: agreed,
});
}
}
pub fn enrich_imported_types_with_keys(
&mut self,
module_index: Option<&dyn CrossFileLookup>,
) {
self.symbols.truncate(self.base_symbol_count);
self.witnesses.truncate(self.base_witness_count);
self.refs.truncate(self.base_ref_count);
self.apply_loader_config_params(module_index);
let mut imported_hash_keys: HashMap<String, Vec<String>> = HashMap::new();
let mut imported_returns: HashMap<String, InferredType> = HashMap::new();
if let Some(idx) = module_index {
for import in &self.imports {
let Some(cached) = idx.get_cached(&import.module_name) else { continue };
for sym in &cached.analysis.symbols {
if !matches!(sym.kind, SymKind::Sub | SymKind::Method) {
continue;
}
if !cached.analysis.exports_name(&sym.name) {
continue;
}
if matches!(sym.detail, SymbolDetail::Sub { .. }) {
if let Some(ty) = cached.analysis.symbol_return_type_via_bag(sym.id, None) {
imported_returns.insert(sym.name.clone(), ty);
}
}
if let Some(sub_info) = cached.sub_info(&sym.name) {
let hk = sub_info.hash_keys();
if !hk.is_empty() {
imported_hash_keys.insert(sym.name.clone(), hk.to_vec());
}
}
}
}
}
let mut to_push: Vec<TypeConstraint> = 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) {
to_push.push(TypeConstraint {
variable: binding.variable.clone(),
scope: binding.scope,
constraint_span: binding.span,
inferred_type: rt.clone(),
});
}
}
for tc in to_push {
self.push_type_constraint(tc);
}
for (func_name, keys) in &imported_hash_keys {
let owner = HashKeyOwner::Sub { package: None, name: 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,
},
namespace: Namespace::Language,
outline_label: None,
});
}
}
let imported_keyed_subs: std::collections::HashSet<String> = imported_hash_keys
.keys()
.cloned()
.collect();
let binding_by_var: std::collections::HashMap<String, String> = self.call_bindings.iter()
.filter_map(|b| {
let bare = split_qualified(&b.func_name).1.to_string();
if imported_keyed_subs.contains(&bare) {
Some((b.variable.clone(), bare))
} else {
None
}
})
.collect();
if !binding_by_var.is_empty() {
for r in &mut self.refs {
if let RefKind::HashKeyAccess { ref var_text, ref mut owner } = r.kind {
if owner.is_some() && !matches!(owner, Some(HashKeyOwner::Variable { .. })) {
continue;
}
if let Some(func_name) = binding_by_var.get(var_text.as_str()) {
if let Some(keys) = imported_hash_keys.get(func_name) {
if keys.iter().any(|k| k == &r.target_name) {
*owner = Some(HashKeyOwner::Sub {
package: None,
name: func_name.clone(),
});
r.resolves_to = None;
}
}
}
}
}
}
self.fix_chain_receiver_hash_key_owners(module_index);
if let Some(idx) = module_index {
use crate::witnesses::{Witness, WitnessAttachment, WitnessPayload, WitnessSource};
let zero = Span {
start: Point { row: 0, column: 0 },
end: Point { row: 0, column: 0 },
};
let parents_snapshot: Vec<(String, Vec<String>)> = self
.package_parents
.iter()
.map(|(c, ps)| (c.clone(), ps.clone()))
.collect();
for (child, parents) in &parents_snapshot {
let mut emitted_for_child: std::collections::HashSet<String> =
std::collections::HashSet::new();
for parent in parents {
if parent == child {
continue;
}
let Some(cached) = idx.get_cached(parent) else { continue };
for sym in &cached.analysis.symbols {
if sym.package.as_deref() != Some(parent.as_str()) {
continue;
}
if !matches!(sym.kind, SymKind::Sub | SymKind::Method) {
continue;
}
if !emitted_for_child.insert(sym.name.clone()) {
continue;
}
self.witnesses.push(Witness {
attachment: WitnessAttachment::MethodOnClass {
class: child.clone(),
name: sym.name.clone(),
},
source: WitnessSource::Enrichment("inheritance_cross".to_string()),
payload: WitnessPayload::Edge(WitnessAttachment::MethodOnClass {
class: parent.clone(),
name: sym.name.clone(),
}),
span: zero,
});
}
}
}
}
{
let key_writes = std::mem::take(&mut self.key_writes);
let ctx = crate::witnesses::BagContext {
scopes: &self.scopes,
package_framework: &self.package_framework,
module_index,
package_parents: &self.package_parents,
app_surface_consumers: &self.app_surface_consumers,
};
crate::witnesses::emit_mutation_extension_witnesses(
&mut self.witnesses,
&ctx,
&key_writes,
false,
);
self.key_writes = key_writes;
}
self.resolve_method_call_types(module_index);
self.stamp_method_call_targets(module_index);
self.rebuild_enrichment_indices();
}
fn resolve_method_call_types(&mut self, module_index: Option<&dyn CrossFileLookup>) {
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.push_type_constraint(TypeConstraint {
variable: binding.variable.clone(),
scope: binding.scope,
constraint_span: binding.span,
inferred_type: rt,
});
}
}
}
}
pub(crate) fn push_type_constraint(&mut self, tc: TypeConstraint) {
use crate::witnesses::{
TypeObservation, Witness, WitnessAttachment, WitnessPayload, WitnessSource,
};
let TypeConstraint { variable, scope, constraint_span: span, inferred_type: ty } = tc;
self.witnesses.push(Witness {
attachment: WitnessAttachment::Variable { name: variable.clone(), scope },
source: WitnessSource::Builder("type_constraint".into()),
payload: WitnessPayload::InferredType(ty.clone()),
span: Span { start: span.start, end: span.start },
});
match ty {
InferredType::ClassName(n) => {
self.witnesses.push(Witness {
attachment: WitnessAttachment::Variable { name: variable, scope },
source: WitnessSource::Builder("type_constraint".into()),
payload: WitnessPayload::Observation(TypeObservation::ClassAssertion(n)),
span,
});
}
InferredType::FirstParam { package } => {
self.witnesses.push(Witness {
attachment: WitnessAttachment::Variable { name: variable, scope },
source: WitnessSource::Builder("type_constraint".into()),
payload: WitnessPayload::Observation(TypeObservation::FirstParamInMethod {
package,
}),
span,
});
}
_ => {}
}
}
fn resolve_dispatch_candidate<'a>(
&'a self,
gated: &'a ProvisionalDispatch,
module_index: Option<&dyn CrossFileLookup>,
) -> GateResult<&'a DispatchCandidate> {
let recv = self.dispatch_receiver_class(gated, module_index);
gated.resolve_for(recv.as_deref(), &self.package_parents, module_index)
}
fn dispatch_receiver_class(
&self,
gated: &ProvisionalDispatch,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<String> {
gated.receiver_hint().cloned().or_else(|| {
let call_span = gated.call_span();
let dispatcher = gated.dispatcher();
self.refs
.iter()
.find(|r| {
r.span == call_span
&& r.target_name == dispatcher
&& matches!(r.kind, RefKind::MethodCall { .. })
})
.and_then(|r| self.method_call_invocant_class(r, module_index))
})
}
pub fn applicable_dispatches(
&self,
module_index: Option<&dyn CrossFileLookup>,
) -> Vec<AppliedDispatch> {
let materialized: HashSet<(Point, Point, String)> = self
.refs
.iter()
.filter_map(|r| match &r.kind {
RefKind::DispatchCall { dispatcher, .. } => {
Some((r.span.start, r.span.end, dispatcher.clone()))
}
_ => None,
})
.collect();
let mut out = Vec::new();
for gated in &self.provisional_dispatches {
if let GateResult::Applies(c) = self.resolve_dispatch_candidate(gated, module_index) {
if materialized.contains(&(c.span.start, c.span.end, c.dispatcher.clone())) {
continue;
}
out.push(AppliedDispatch {
name: c.name.clone(),
span: c.span,
owner: HandlerOwner::Class(c.owner_class.clone()),
});
}
}
out
}
pub fn dispatch_at(
&self,
point: Point,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<AppliedDispatch> {
for gated in &self.provisional_dispatches {
if !contains_point(&gated.call_span(), point) {
continue;
}
if let GateResult::Applies(c) = self.resolve_dispatch_candidate(gated, module_index) {
return Some(AppliedDispatch {
name: c.name.clone(),
span: c.span,
owner: HandlerOwner::Class(c.owner_class.clone()),
});
}
}
None
}
pub fn untyped_dispatches(
&self,
module_index: Option<&dyn CrossFileLookup>,
) -> Vec<UntypedDispatch> {
let mut out = Vec::new();
for gated in &self.provisional_dispatches {
if let GateResult::ReceiverUntyped =
self.resolve_dispatch_candidate(gated, module_index)
{
out.push(UntypedDispatch {
call_span: gated.call_span(),
dispatcher: gated.dispatcher().to_string(),
gate: gated.gate().to_string(),
});
}
}
out
}
fn rebuild_enrichment_indices(&mut self) {
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);
}
let hashkey_defs: HashMap<(String, HashKeyOwner), SymbolId> = self.symbols.iter()
.filter_map(|sym| {
if let SymbolDetail::HashKeyDef { owner, .. } = &sym.detail {
Some(((sym.name.clone(), owner.clone()), sym.id))
} else {
None
}
})
.collect();
let mut hashkey_resolutions: Vec<(usize, SymbolId)> = Vec::new();
for (i, r) in self.refs.iter().enumerate() {
if r.resolves_to.is_some() {
continue;
}
if let RefKind::HashKeyAccess { owner: Some(owner), .. } = &r.kind {
if let Some(&sid) = hashkey_defs.get(&(r.target_name.clone(), owner.clone())) {
hashkey_resolutions.push((i, sid));
}
}
}
for (idx, sid) in hashkey_resolutions {
self.refs[idx].resolves_to = Some(sid);
}
self.refs_by_name.clear();
self.refs_by_target.clear();
for (i, r) in self.refs.iter().enumerate() {
self.refs_by_name
.entry(r.target_name.clone())
.or_default()
.push(i);
if let Some(sym_id) = r.resolves_to {
self.refs_by_target.entry(sym_id).or_default().push(i);
}
}
}
pub(crate) fn symbol_return_type_via_bag(
&self,
sym_id: SymbolId,
arg_count: Option<usize>,
) -> Option<InferredType> {
self.symbol_return_type_via_bag_ctx(sym_id, arg_count, None)
}
pub(crate) fn symbol_return_type_via_bag_ctx(
&self,
sym_id: SymbolId,
arg_count: Option<usize>,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<InferredType> {
use crate::witnesses::{
FrameworkFact, ReducedValue, ReducerQuery, ReducerRegistry,
WitnessAttachment,
};
let att = WitnessAttachment::Symbol(sym_id);
let reg = ReducerRegistry::with_defaults();
let ctx = self.bag_context(module_index);
let resolved_arity = arg_count.map(|n| n as u32).or_else(|| {
self.symbols
.get(sym_id.0 as usize)
.and_then(|s| match &s.detail {
SymbolDetail::Sub { params, .. } => Some(params.len() as u32),
_ => None,
})
});
let receiver = self
.symbols
.get(sym_id.0 as usize)
.and_then(|s| s.package.clone())
.map(InferredType::ClassName);
let q = ReducerQuery {
attachment: &att,
point: None,
framework: FrameworkFact::Plain,
arity_hint: resolved_arity,
receiver,
context: Some(&ctx),
};
match reg.query(&self.witnesses, &q) {
ReducedValue::Type(t) => Some(t),
_ => None,
}
}
pub(crate) fn find_method_return_type(
&self,
class_name: &str,
method_name: &str,
module_index: Option<&dyn CrossFileLookup>,
arg_count: Option<usize>,
) -> Option<InferredType> {
use crate::witnesses::{
FrameworkFact, ReducedValue, ReducerQuery, ReducerRegistry,
WitnessAttachment,
};
let framework = self
.package_framework
.get(class_name)
.copied()
.unwrap_or(FrameworkFact::Plain);
let att = WitnessAttachment::MethodOnClass {
class: class_name.to_string(),
name: method_name.to_string(),
};
let ctx = self.bag_context(module_index);
let q = ReducerQuery {
attachment: &att,
point: None,
framework,
arity_hint: arg_count.map(|n| n as u32),
receiver: Some(InferredType::ClassName(class_name.to_string())),
context: Some(&ctx),
};
let reg = ReducerRegistry::with_defaults();
if let ReducedValue::Type(t) = reg.query(&self.witnesses, &q) {
return Some(t);
}
None
}
fn method_detail(
&self,
class_name: &str,
method_name: &str,
defining_class: Option<&str>,
module_index: Option<&dyn CrossFileLookup>,
) -> 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) {
let opaque = self.method_opaque_return_cross_file(class_name, method_name, module_index)
|| defining_class.is_some_and(|dc| {
self.method_opaque_return_cross_file(dc, method_name, module_index)
});
if opaque {
return String::new();
}
format!("{} → {}", base, format_inferred_type(&rt))
} else {
base
}
}
fn method_opaque_return(&self, class_name: &str, method_name: &str) -> bool {
let check = |sym: &Symbol| -> bool {
if sym.name != method_name { return false; }
if !matches!(sym.kind, SymKind::Sub | SymKind::Method) { return false; }
if sym.package.as_deref() != Some(class_name) { return false; }
matches!(&sym.detail, SymbolDetail::Sub { opaque_return: true, .. })
};
for sym in &self.symbols {
if check(sym) { return true; }
}
false
}
fn method_opaque_return_cross_file(
&self,
class_name: &str,
method_name: &str,
module_index: Option<&dyn CrossFileLookup>,
) -> bool {
if self.method_opaque_return(class_name, method_name) {
return true;
}
let Some(idx) = module_index else { return false };
let mut found = false;
idx.for_each_entity_bridged_to(class_name, &mut |_mod, _cached, sym| {
if found { return; }
if sym.name != method_name { return; }
if !matches!(sym.kind, SymKind::Sub | SymKind::Method) { return; }
if matches!(&sym.detail, SymbolDetail::Sub { opaque_return: true, .. }) {
found = true;
}
});
found
}
pub fn complete_methods_for_class(
&self,
class_name: &str,
module_index: Option<&dyn CrossFileLookup>,
) -> 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![],
display_override: None,
});
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<&dyn CrossFileLookup>,
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 };
let display_override = sub_display_override(&sym.detail);
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![],
display_override,
});
}
}
}
for ns in &self.plugin_namespaces {
let bridges_class = ns.bridges.iter().any(|b|
matches!(b, Bridge::Class(c) if c == class_name));
if !bridges_class { continue; }
for sym_id in &ns.entities {
let Some(sym) = self.symbols.get(sym_id.0 as usize) else { continue };
if !matches!(sym.kind, SymKind::Sub | SymKind::Method) { continue; }
if seen_names.contains(&sym.name) { continue; }
seen_names.insert(sym.name.clone());
let defining = if class_name != original_class { Some(class_name) } else { None };
let display_override = sub_display_override(&sym.detail);
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![],
display_override,
});
}
}
if let Some(idx) = module_index {
let mut bridged: Vec<(String, SymKind, Option<SymbolDetail>, Option<InferredType>)> = Vec::new();
idx.for_each_entity_bridged_to(class_name, &mut |_mod, _cached, sym| {
if !matches!(sym.kind, SymKind::Sub | SymKind::Method) { return; }
bridged.push((
sym.name.clone(),
sym.kind,
Some(sym.detail.clone()),
None,
));
});
for (name, kind, detail, _rt) in bridged {
if seen_names.contains(&name) { continue; }
seen_names.insert(name.clone());
let is_method = kind == SymKind::Method
|| matches!(detail, Some(SymbolDetail::Sub { is_method: true, .. }));
let kind = if is_method { SymKind::Method } else { SymKind::Sub };
let defining = if class_name != original_class { Some(class_name) } else { None };
let method_detail_str = self.method_detail(original_class, &name, defining, module_index);
let display_override = detail.as_ref()
.map(|d| sub_display_override(d))
.unwrap_or(None);
candidates.push(CompletionCandidate {
label: name,
kind,
detail: Some(method_detail_str),
insert_text: None,
sort_priority: PRIORITY_LOCAL,
additional_edits: vec![],
display_override,
});
}
if let Some(cached) = idx.get_cached(class_name) {
for sym in &cached.analysis.symbols {
if !matches!(sym.kind, SymKind::Sub | SymKind::Method) { continue; }
if sym.package.as_deref() != Some(class_name) { continue; }
if seen_names.contains(&sym.name) { continue; }
seen_names.insert(sym.name.clone());
let is_method = sym.kind == SymKind::Method
|| matches!(sym.detail, SymbolDetail::Sub { is_method: true, .. });
let kind = if 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, &sym.name, defining, module_index);
let display_override = sub_display_override(&sym.detail);
candidates.push(CompletionCandidate {
label: sym.name.clone(),
kind,
detail: Some(detail),
insert_text: None,
sort_priority: PRIORITY_LOCAL,
additional_edits: vec![],
display_override,
});
}
}
}
for parent in parents_of(
class_name,
&self.package_parents,
module_index,
&self.app_surface_consumers,
) {
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> {
if !self.package_ranges.is_empty() {
let mut best: Option<&PackageRange> = None;
for r in &self.package_ranges {
if !contains_point(&r.span, point) {
continue;
}
let win = match best {
None => true,
Some(prev) => {
let cur_start = (r.span.start.row, r.span.start.column);
let prev_start = (prev.span.start.row, prev.span.start.column);
cur_start > prev_start
|| (cur_start == prev_start && span_size(&r.span) < span_size(&prev.span))
}
};
if win {
best = Some(r);
}
}
return best.map(|r| r.package.as_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 handlers_for_owner<'a>(
&'a self,
owner_class: &'a str,
dispatchers: &'a [String],
) -> impl Iterator<Item = &'a Symbol> + 'a {
self.symbols.iter().filter(move |sym| {
let SymbolDetail::Handler { owner, dispatchers: dd, .. } = &sym.detail else {
return false;
};
let HandlerOwner::Class(c) = owner;
if c != owner_class { return false; }
if !dispatchers.is_empty()
&& !dd.iter().any(|d| dispatchers.iter().any(|n| n == d))
{
return false;
}
true
})
}
pub fn trigger_view_at(&self, point: Point) -> (Vec<String>, Vec<String>) {
let pkg = match self.package_at(point) {
Some(p) => p.to_string(),
None => return (Vec::new(), Vec::new()),
};
let uses = self.package_uses.get(&pkg).cloned().unwrap_or_default();
let mut parents = Vec::new();
let mut seen = std::collections::HashSet::new();
let mut stack = vec![pkg.clone()];
while let Some(cur) = stack.pop() {
if let Some(ps) = self.package_parents.get(&cur) {
for p in ps {
if seen.insert(p.clone()) {
parents.push(p.clone());
stack.push(p.clone());
}
}
}
}
(uses, parents)
}
pub fn invocant_text_to_class(&self, invocant: Option<&str>, point: Point) -> Option<String> {
use crate::conventions::InvocantText;
let text = invocant?;
if crate::conventions::is_conventional_invocant_name(text) {
return self.package_at(point).map(|s| s.to_string());
}
match InvocantText::parse(text) {
InvocantText::CurrentPackage | InvocantText::PositionalReceiver => {
self.package_at(point).map(|s| s.to_string())
}
InvocantText::Scalar(_) => self
.inferred_type_via_bag(text, point)
.and_then(|t| t.class_name().map(str::to_string)),
InvocantText::NonScalar(_) => None,
InvocantText::Bareword(b) => Some(b.to_string()),
}
}
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.found_by(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 hash_key_owner_at(&self, point: Point) -> Option<HashKeyOwner> {
if let Some(RefKind::HashKeyAccess { owner: Some(o), .. }) =
self.ref_at(point).map(|r| &r.kind)
{
return Some(o.clone());
}
match self.symbol_at(point).map(|s| &s.detail) {
Some(SymbolDetail::HashKeyDef { owner, .. }) => Some(owner.clone()),
_ => None,
}
}
pub fn find_definition(&self, point: Point, _module_index: Option<&dyn CrossFileLookup>) -> 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 { resolved_package } => {
for &sid in self.symbols_named(r.unqualified_target_name()) {
let sym = self.symbol(sid);
if sym.kind != SymKind::Sub { continue; }
if sym.package == *resolved_package {
return Some(sym.selection_span);
}
}
}
RefKind::MethodCall { .. } => {
match &r.resolved_method_target {
Some(MethodTarget::Local { sym_id, .. }) => {
return Some(self.symbol(*sym_id).selection_span);
}
Some(MethodTarget::CrossFile { .. }) | None => {
return None;
}
}
}
RefKind::PackageRef => {
return self.find_package_or_class(&r.target_name);
}
RefKind::HashKeyAccess { ref owner, .. } => {
if let Some(ref owner) = 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);
}
RefKind::DispatchCall { owner: Some(owner), .. } => {
for sym in &self.symbols {
if sym.name != r.target_name { continue; }
if let SymbolDetail::Handler { owner: o, .. } = &sym.detail {
if o == owner {
return Some(sym.selection_span);
}
}
}
return None;
}
RefKind::DispatchCall { owner: None, .. } => {}
}
}
if let Some(sym) = self.symbol_at(point) {
return Some(sym.selection_span);
}
None
}
pub fn find_references(
&self,
point: Point,
module_index: Option<&dyn CrossFileLookup>,
) -> Vec<Span> {
if let Some(g) = self.field_group_at(point) {
return self.field_group_spans(&g);
}
if let Some((target_id, include_decl)) = self.resolve_target_at(point, module_index) {
let mut results = self.collect_refs_for_target(target_id, include_decl, module_index);
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,
module_index: Option<&dyn CrossFileLookup>,
) -> Vec<(Span, AccessKind)> {
if let Some((target_id, _)) = self.resolve_target_at(point, module_index) {
let mut results = self.collect_refs_for_target(target_id, true, module_index);
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);
return results;
}
if let Some(r) = self.ref_at(point) {
let mut results: Vec<(Span, AccessKind)> = Vec::new();
match &r.kind {
RefKind::MethodCall { method_name_span, .. } => {
let Some(wanted_class) = self.method_call_invocant_class(r, module_index) else {
return Vec::new();
};
results.push((*method_name_span, r.access));
for other in &self.refs {
if std::ptr::eq(other, r) { continue; }
if other.unqualified_target_name() != r.unqualified_target_name() { continue; }
if !matches!(other.kind, RefKind::MethodCall { .. }) { continue; }
let Some(ocn) = self.method_call_invocant_class(other, module_index) else { continue };
if ocn != wanted_class { continue; }
if let RefKind::MethodCall { method_name_span: ms, .. } = &other.kind {
results.push((*ms, other.access));
}
}
}
RefKind::FunctionCall { resolved_package: Some(pkg) } => {
let wanted_pkg = pkg.clone();
results.push((r.span, r.access));
for other in &self.refs {
if std::ptr::eq(other, r) { continue; }
if other.target_name != r.target_name { continue; }
if let RefKind::FunctionCall { resolved_package: Some(op) } = &other.kind {
if op == &wanted_pkg {
results.push((other.span, other.access));
}
}
}
}
_ => {}
}
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);
return results;
}
Vec::new()
}
fn collect_refs_for_target(
&self,
target_id: SymbolId,
include_decl: bool,
module_index: Option<&dyn CrossFileLookup>,
) -> 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 &idx in self.refs_to_symbol(target_id) {
let r = &self.refs[idx];
results.push((r.span, r.access));
}
if matches!(sym.kind, SymKind::Sub | SymKind::Method | SymKind::Package | SymKind::Class | SymKind::Module) {
let sym_package = sym.package.clone();
for r in &self.refs {
if r.resolves_to.is_some() { continue; }
match (&r.kind, &sym.kind) {
(RefKind::FunctionCall { resolved_package }, SymKind::Sub) => {
if r.unqualified_target_name() == sym.name
&& *resolved_package == sym_package {
results.push((r.span, r.access));
}
}
(RefKind::MethodCall { method_name_span, .. },
SymKind::Sub | SymKind::Method) if r.unqualified_target_name() == sym.name => {
match (self.method_call_invocant_class(r, module_index), &sym_package) {
(Some(cn), Some(pkg)) if cn == *pkg => {
results.push((*method_name_span, r.access));
}
_ => {}
}
}
(RefKind::PackageRef, SymKind::Package | SymKind::Class | SymKind::Module)
if r.target_name == sym.name =>
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) => owner.found_by(ro),
None => false,
};
if matches {
results.push((r.span, r.access));
}
}
}
}
results
}
pub fn hover_info(&self, point: Point, source: &str, module_index: Option<&dyn CrossFileLookup>) -> 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 matches!(mr.kind, RefKind::MethodCall { .. }) {
let class_name = self.method_call_invocant_class(mr, module_index);
let mname = mr.unqualified_target_name();
if let Some(ref cn) = class_name {
match self.resolve_method_in_ancestors(cn, mname, 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, mname, 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, ref def_module }) => {
if let Some(idx) = module_index {
let module = def_module.as_deref().unwrap_or(class.as_str());
if let Some(cached) = idx.get_cached(module) {
if let Some(sub_info) = cached.sub_info(mname) {
let sig = format_cross_file_signature(mname, &sub_info);
let mut text = format!("```perl\n{}\n```\n\n*class {} — resolved from `{}`*", sig, class, r.target_name);
if let Some(rt) = sub_info.return_type(Some(idx)) {
text.push_str(&format!("\n\n*returns: {}*", format_inferred_type(&rt)));
}
if let Some(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, module_index));
}
if let Some(sym) = self.resolve_variable(&r.target_name, point) {
return Some(self.format_symbol_hover_at(sym, source, point, module_index));
}
}
RefKind::FunctionCall { resolved_package } => {
for &sid in self.symbols_named(r.unqualified_target_name()) {
let sym = self.symbol(sid);
if sym.kind != SymKind::Sub { continue; }
if sym.package == *resolved_package {
return Some(self.format_symbol_hover(sym, source, module_index));
}
}
if let Some(idx) = module_index {
for import in &self.imports {
let matched = import.imported_symbols.iter()
.find(|s| s.local_name == r.target_name);
let Some(is) = matched else { continue };
let Some(cached) = idx.get_cached(&import.module_name) else { continue };
let Some(sub_info) = cached.sub_info(is.remote()) else { continue };
let sig_params = sub_info.params().iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>()
.join(", ");
let mut sig = format!("sub {}({})", r.target_name, sig_params);
if let Some(rt) = sub_info.return_type(Some(idx)) {
sig.push_str(&format!(" → {}", format_inferred_type(&rt)));
}
let mut text = format!("```perl\n{}\n```", sig);
if let Some(doc) = sub_info.doc() {
text.push_str(&format!("\n\n{}", doc));
}
if is.remote() != r.target_name {
text.push_str(&format!(
"\n\n*imported from `{}` (as `{}`)*",
import.module_name, is.remote()
));
} else {
text.push_str(&format!(
"\n\n*imported from `{}`*",
import.module_name
));
}
return Some(text);
}
}
}
RefKind::MethodCall { .. } => {
let class_name = r
.resolved_method_target
.as_ref()
.map(|t| t.invocant_class().to_string());
let method = r.unqualified_target_name();
if let Some(ref cn) = class_name {
match self.resolve_method_in_ancestors(cn, method, 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, method, module_index, None) {
text.push_str(&format!("\n\n*returns: {}*", format_inferred_type(&rt)));
}
return Some(text);
}
Some(MethodResolution::CrossFile { ref class, ref def_module }) => {
if let Some(idx) = module_index {
let module = def_module.as_deref().unwrap_or(class.as_str());
if let Some(cached) = idx.get_cached(module) {
if let Some(sub_info) = cached.sub_info(method) {
let class_label = if class != cn {
format!("{} (from {})", cn, class)
} else {
cn.to_string()
};
let sig = format_cross_file_signature(method, &sub_info);
let mut text = format!("```perl\n{}\n```\n\n*class {}*", sig, class_label);
if let Some(rt) = sub_info.return_type(Some(idx)) {
text.push_str(&format!("\n\n*returns: {}*", format_inferred_type(&rt)));
}
if let Some(doc) = sub_info.doc() {
text.push_str(&format!("\n\n{}", doc));
}
return Some(text);
}
}
}
}
None => {}
}
}
for &sid in self.symbols_named(method) {
let sym = self.symbol(sid);
if matches!(sym.kind, SymKind::Sub | SymKind::Method) {
return Some(self.format_symbol_hover(sym, source, module_index));
}
}
}
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, module_index));
}
}
}
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")));
}
}
}
RefKind::DispatchCall { dispatcher, owner } => {
if let Some(ref owner) = owner {
return Some(self.format_handler_hover(
&r.target_name,
owner,
Some(dispatcher),
module_index,
));
}
}
}
}
if let Some(sym) = self.symbol_at(point) {
if let SymbolDetail::Handler { owner, .. } = &sym.detail {
return Some(self.format_handler_hover(&sym.name, owner, None, module_index));
}
return Some(self.format_symbol_hover(sym, source, module_index));
}
None
}
pub fn rename_at(&self, point: Point, new_name: &str) -> Option<Vec<(Span, String)>> {
if let Some(group) = self.field_group_at(point) {
return Some(self.rename_field_group(&group, new_name));
}
let refs = self.find_references(point, None);
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)
}
fn field_group_at(&self, point: Point) -> Option<FieldGroup> {
if let Some(s) = self.symbol_at(point) {
if matches!(s.kind, SymKind::Method | SymKind::HashKeyDef) {
if let Some(pkg) = s.package.as_deref() {
if let Some(g) = self.attr_pair_group(&s.name, pkg) {
if contains_point(&g.decl_span.unwrap(), point) {
return Some(g);
}
}
}
}
}
let field_sym = self
.symbol_at(point)
.filter(|s| matches!(s.kind, SymKind::Field))
.or_else(|| {
self.ref_at(point).and_then(|r| {
if !matches!(r.kind, RefKind::Variable) {
return None;
}
r.resolves_to
.map(|id| self.symbol(id))
.filter(|s| matches!(s.kind, SymKind::Field))
})
});
if let Some(sym) = field_sym {
return self.field_group_of(sym);
}
if let Some(r) = self.ref_at(point) {
if matches!(r.kind, RefKind::MethodCall { .. }) {
let bare = r.unqualified_target_name();
let cls = r
.resolved_method_target
.as_ref()
.map(|t| t.invocant_class().to_string())
.or_else(|| self.method_call_invocant_class(r, None));
if let Some(class) = cls {
let field_name = format!("${}", bare);
if let Some(sym) = self.symbols.iter().find(|s| {
matches!(s.kind, SymKind::Field)
&& s.name == field_name
&& s.package.as_deref() == Some(class.as_str())
}) {
if let Some(g) = self.field_group_of(sym) {
if g.has_reader {
return Some(g);
}
}
}
if let Some(g) = self.attr_pair_group(bare, &class) {
if g.has_reader {
return Some(g);
}
}
}
return None;
}
}
let (key, owner) = match self.ref_at(point).map(|r| (r, &r.kind)) {
Some((r, RefKind::HashKeyAccess { owner: Some(o), .. })) => {
(r.target_name.clone(), o.clone())
}
_ => match self.symbol_at(point) {
Some(s) if matches!(s.kind, SymKind::HashKeyDef) => match &s.detail {
SymbolDetail::HashKeyDef { owner, .. } => (s.name.clone(), owner.clone()),
_ => return None,
},
_ => return None,
},
};
let HashKeyOwner::Sub { package: Some(class), name } = owner else {
return None;
};
if !crate::conventions::is_constructor_name(&name) {
return None;
}
let field_name = format!("${}", key);
if let Some(sym) = self.symbols.iter().find(|s| {
matches!(s.kind, SymKind::Field)
&& s.name == field_name
&& s.package.as_deref() == Some(class.as_str())
}) {
return self.field_group_of(sym);
}
self.attr_pair_group(&key, &class)
}
fn attr_pair_group(&self, bare: &str, class: &str) -> Option<FieldGroup> {
let key_def = self.symbols.iter().find(|s| {
matches!(s.kind, SymKind::HashKeyDef)
&& s.name == bare
&& matches!(
&s.detail,
SymbolDetail::HashKeyDef {
owner: HashKeyOwner::Sub { package: Some(p), name },
..
} if p == class && crate::conventions::is_constructor_name(name)
)
})?;
let accessor = self.symbols.iter().find(|s| {
matches!(s.kind, SymKind::Method)
&& s.name == bare
&& s.package.as_deref() == Some(class)
&& s.selection_span == key_def.selection_span
});
Some(FieldGroup {
field_sym: None,
decl_span: Some(key_def.selection_span),
class: class.to_string(),
bare: bare.to_string(),
has_param: true,
has_reader: accessor.is_some(),
})
}
fn field_group_of(&self, sym: &Symbol) -> Option<FieldGroup> {
let SymbolDetail::Field { ref attributes, .. } = sym.detail else {
return None;
};
if !sym.name.starts_with('$') {
return None;
}
Some(FieldGroup {
field_sym: Some(sym.id),
decl_span: None,
class: sym.package.clone()?,
bare: sym.name[1..].to_string(),
has_param: attributes.iter().any(|a| a == "param"),
has_reader: attributes.iter().any(|a| a == "reader"),
})
}
fn rename_field_group(&self, g: &FieldGroup, new_name: &str) -> Vec<(Span, String)> {
let bare_new = new_name.trim_start_matches(['$', '@', '%']);
let bare: Vec<(Span, String)> = self
.field_group_spans_bare(g)
.into_iter()
.map(|s| (s, bare_new.to_string()))
.collect();
let mut edits = bare.clone();
for (method, affix) in self
.attr_projections
.iter()
.filter(|a| a.class == g.class && a.attr == g.bare)
.filter_map(|a| match &a.kind {
AttrProjectionKind::Accessor { method, affix } => {
Some((method.clone(), affix.clone()))
}
_ => None,
})
{
let Some((pre, suf)) = affix else { continue };
let new_method = format!("{}{}{}", pre, bare_new, suf);
for span in self.mapped_member_spans(g, &method) {
if bare.iter().any(|(b, _)| *b == span) {
continue;
}
edits.push((span, new_method.clone()));
}
}
edits.sort_by_key(|(s, _)| (s.start.row, s.start.column));
edits.dedup_by(|a, b| a.0 == b.0);
edits
}
fn mapped_member_spans(&self, g: &FieldGroup, method: &str) -> Vec<Span> {
let mut spans = Vec::new();
for r in &self.refs {
if let RefKind::MethodCall { method_name_span, .. } = &r.kind {
if r.unqualified_target_name() != method {
continue;
}
let cls = r
.resolved_method_target
.as_ref()
.map(|t| t.invocant_class().to_string())
.or_else(|| self.method_call_invocant_class(r, None));
if cls.as_deref() == Some(g.class.as_str()) {
spans.push(*method_name_span);
}
}
}
for s in &self.symbols {
if matches!(s.kind, SymKind::Sub | SymKind::Method)
&& s.name == method
&& s.package.as_deref() == Some(g.class.as_str())
{
spans.push(s.selection_span);
}
}
spans
}
fn field_group_spans(&self, g: &FieldGroup) -> Vec<Span> {
let mut spans = self.field_group_spans_bare(g);
for a in self
.attr_projections
.iter()
.filter(|a| a.class == g.class && a.attr == g.bare)
{
if let AttrProjectionKind::Accessor { method, .. } = &a.kind {
spans.extend(self.mapped_member_spans(g, method));
}
}
spans.sort_by_key(|s| (s.start.row, s.start.column));
spans.dedup();
spans
}
fn field_group_spans_bare(&self, g: &FieldGroup) -> Vec<Span> {
let mut spans: Vec<Span> = Vec::new();
if let Some(field_sym) = g.field_sym {
for (span, _access) in self.collect_refs_for_target(field_sym, true, None) {
spans.push(Span {
start: Point::new(span.start.row, span.start.column + 1),
end: span.end,
});
}
}
if let Some(decl) = g.decl_span {
spans.push(decl);
}
if g.has_param {
let owner = HashKeyOwner::Sub {
package: Some(g.class.clone()),
name: "new".to_string(),
};
for r in &self.refs {
if let RefKind::HashKeyAccess { owner: Some(o), .. } = &r.kind {
if r.target_name == g.bare && o.found_by(&owner) {
spans.push(r.span);
}
}
}
}
if g.has_reader {
for r in &self.refs {
if let RefKind::MethodCall { method_name_span, .. } = &r.kind {
if r.unqualified_target_name() != g.bare {
continue;
}
let cls = r
.resolved_method_target
.as_ref()
.map(|t| t.invocant_class().to_string())
.or_else(|| self.method_call_invocant_class(r, None));
if cls.as_deref() == Some(g.class.as_str()) {
spans.push(*method_name_span);
}
}
}
}
if self
.attr_projections
.iter()
.any(|a| {
a.class == g.class
&& a.attr == g.bare
&& matches!(a.kind, AttrProjectionKind::InternalKey)
})
{
for r in &self.refs {
if let RefKind::HashKeyAccess { owner: Some(HashKeyOwner::Class(c)), .. } =
&r.kind
{
if c == &g.class && r.target_name == g.bare {
spans.push(r.span);
}
}
}
}
spans.sort_by_key(|s| (s.start.row, s.start.column));
spans.dedup();
spans
}
pub fn deferred_hash_key_owner(
&self,
key_ref: &Ref,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<HashKeyOwner> {
let enclosing = self
.refs
.iter()
.filter(|c| {
matches!(c.kind, RefKind::MethodCall { .. })
&& contains_point(&c.span, key_ref.span.start)
&& contains_point(&c.span, key_ref.span.end)
})
.min_by_key(|c| span_size(&c.span))?;
let class = enclosing
.resolved_method_target
.as_ref()
.map(|t| t.invocant_class().to_string())
.or_else(|| self.method_call_invocant_class(enclosing, module_index))?;
Some(HashKeyOwner::Sub {
package: Some(class),
name: enclosing.unqualified_target_name().to_string(),
})
}
pub fn field_projections_at(&self, point: Point) -> Option<FieldProjections> {
let g = self.field_group_at(point)?;
Some(self.projections_of(g))
}
pub fn field_projections_named(&self, bare: &str, class: &str) -> Option<FieldProjections> {
let field_name = format!("${}", bare);
let g = self
.symbols
.iter()
.find(|s| {
matches!(s.kind, SymKind::Field)
&& s.name == field_name
&& s.package.as_deref() == Some(class)
})
.and_then(|sym| self.field_group_of(sym))
.or_else(|| self.attr_pair_group(bare, class))?;
Some(self.projections_of(g))
}
fn projections_of(&self, g: FieldGroup) -> FieldProjections {
let mut variable_spans: Vec<Span> = g
.field_sym
.map(|fs| {
self.collect_refs_for_target(fs, true, None)
.into_iter()
.map(|(span, _)| Span {
start: Point::new(span.start.row, span.start.column + 1),
end: span.end,
})
.collect()
})
.unwrap_or_default();
if let Some(decl) = g.decl_span {
variable_spans.push(decl);
}
let mapped = self
.attr_projections
.iter()
.filter(|a| a.class == g.class && a.attr == g.bare)
.filter_map(|a| match &a.kind {
AttrProjectionKind::Accessor { method, affix } => Some(MappedMember {
method: method.clone(),
affix: affix.clone(),
}),
_ => None,
})
.collect();
let has_internal = self.attr_projections.iter().any(|a| {
a.class == g.class
&& a.attr == g.bare
&& matches!(a.kind, AttrProjectionKind::InternalKey)
});
FieldProjections {
class: g.class,
bare: g.bare,
has_param: g.has_param,
has_reader: g.has_reader,
has_internal,
variable_spans,
mapped,
}
}
pub fn rename_kind_at(&self, point: Point, module_index: Option<&dyn CrossFileLookup>) -> Option<RenameKind> {
if let Some(r) = self.ref_at(point) {
match &r.kind {
RefKind::Variable | RefKind::ContainerAccess => return Some(RenameKind::Variable),
RefKind::FunctionCall { resolved_package } => {
return Some(RenameKind::Function {
name: r.unqualified_target_name().to_string(),
package: resolved_package.clone(),
});
}
RefKind::MethodCall { .. } => {
if let Some(class) = self.method_call_invocant_class(r, module_index) {
return Some(RenameKind::Method {
name: r.unqualified_target_name().to_string(),
class,
});
}
}
RefKind::PackageRef => return Some(RenameKind::Package(r.target_name.clone())),
RefKind::HashKeyAccess { .. } => return Some(RenameKind::HashKey(r.target_name.clone())),
RefKind::DispatchCall { owner: Some(owner), .. } => {
return Some(RenameKind::Handler {
owner: owner.clone(),
name: r.target_name.clone(),
});
}
RefKind::DispatchCall { owner: None, .. } => return None,
}
}
if let Some(sym) = self.symbol_at(point) {
return match sym.kind {
SymKind::Variable | SymKind::Field => Some(RenameKind::Variable),
SymKind::Sub => Some(RenameKind::Function {
name: sym.name.clone(),
package: sym.package.clone(),
}),
SymKind::Method => {
let class = sym.package.clone()?;
Some(RenameKind::Method {
name: sym.name.clone(),
class,
})
}
SymKind::Package | SymKind::Class => Some(RenameKind::Package(sym.name.clone())),
SymKind::Handler => {
if let SymbolDetail::Handler { owner, .. } = &sym.detail {
Some(RenameKind::Handler { owner: owner.clone(), name: sym.name.clone() })
} else { None }
}
_ => None,
};
}
None
}
#[allow(dead_code)]
fn rename_callable_in_scope(
&self,
old_name: &str,
scope: &Option<String>,
new_name: &str,
module_index: Option<&dyn CrossFileLookup>,
) -> Vec<(Span, String)> {
let mut edits = Vec::new();
for sym in &self.symbols {
if sym.name != old_name { continue; }
if !matches!(sym.kind, SymKind::Sub | SymKind::Method) { continue; }
if sym.package != *scope { continue; }
edits.push((sym.selection_span, new_name.to_string()));
}
for r in &self.refs {
if r.target_name != old_name { continue; }
match &r.kind {
RefKind::FunctionCall { resolved_package } => {
if resolved_package == scope {
edits.push((r.span, new_name.to_string()));
}
}
RefKind::MethodCall { method_name_span, .. } => {
if let (Some(cls), Some(wanted)) =
(self.method_call_invocant_class(r, module_index), scope.as_ref())
{
if &cls == wanted {
edits.push((*method_name_span, new_name.to_string()));
}
}
}
_ => {}
}
}
edits
}
#[allow(dead_code)]
pub fn rename_sub_in_package(
&self,
old_name: &str,
package: &Option<String>,
new_name: &str,
module_index: Option<&dyn CrossFileLookup>,
) -> Vec<(Span, String)> {
self.rename_callable_in_scope(old_name, package, new_name, module_index)
}
#[allow(dead_code)]
pub fn rename_method_in_class(
&self,
old_name: &str,
class: &str,
new_name: &str,
module_index: Option<&dyn CrossFileLookup>,
) -> Vec<(Span, String)> {
self.rename_callable_in_scope(old_name, &Some(class.to_string()), new_name, module_index)
}
#[allow(dead_code)]
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, module_index: Option<&dyn CrossFileLookup>) -> 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 { resolved_package } => {
for &sid in self.symbols_named(r.unqualified_target_name()) {
let sym = self.symbol(sid);
if sym.kind != SymKind::Sub { continue; }
if sym.package == *resolved_package {
return Some((sid, true));
}
}
}
RefKind::MethodCall { .. } => {
let class_name = self.method_call_invocant_class(r, module_index);
let method = r.unqualified_target_name();
if let Some(ref cn) = class_name {
match self.resolve_method_in_ancestors(cn, method, module_index) {
Some(MethodResolution::Local { sym_id, .. }) => {
return Some((sym_id, true));
}
Some(MethodResolution::CrossFile { .. }) => {
for &sid in self.symbols_named(method) {
let sym = self.symbol(sid);
if !matches!(sym.kind, SymKind::Sub | SymKind::Method) { continue; }
if sym.package.as_deref() == Some(cn.as_str()) {
return Some((sid, true));
}
}
}
None => {}
}
for &sid in self.symbols_named(method) {
let sym = self.symbol(sid);
if !matches!(sym.kind, SymKind::Sub | SymKind::Method) { continue; }
if sym.package.as_deref() == Some(cn.as_str()) {
return Some((sid, true));
}
}
return None;
}
for &sid in self.symbols_named(method) {
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, .. } => {
if let Some(ref owner) = owner {
for def in self.hash_key_defs_for_owner(owner) {
if def.name == r.target_name {
return Some((def.id, true));
}
}
}
}
RefKind::DispatchCall { owner: Some(owner), .. } => {
for sym in &self.symbols {
if sym.name != r.target_name { continue; }
if let SymbolDetail::Handler { owner: o, .. } = &sym.detail {
if o == owner {
return Some((sym.id, true));
}
}
}
}
RefKind::DispatchCall { owner: None, .. } => {}
}
}
if let Some(sym) = self.symbol_at(point) {
return Some((sym.id, false));
}
None
}
pub fn method_call_invocant_class(
&self,
r: &Ref,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<String> {
let RefKind::MethodCall { invocant, invocant_span, .. } = &r.kind else {
return None;
};
use crate::conventions::MethodToken;
match MethodToken::parse(&r.target_name) {
MethodToken::Super(name) => {
let encl = self.enclosing_class_for_scope(r.scope)?;
return self
.resolve_super_method(&encl, name, module_index)
.map(|res| res.class().to_string());
}
token => {
if let Some(pkg) = token.literal_package() {
return Some(pkg.to_string());
}
}
}
if invocant.is_empty() {
return None;
}
use crate::conventions::InvocantText;
if matches!(
invocant.classify(),
InvocantText::PositionalReceiver | InvocantText::CurrentPackage
) {
return self.enclosing_class_for_scope(r.scope);
}
if let Some(span) = invocant_span {
if invocant.contains("->") {
if let Some(cn) = self
.inferred_type_via_bag_ctx(invocant, span.start, module_index)
.and_then(|t| t.class_name().map(|s| s.to_string()))
{
return Some(cn);
}
}
}
if let Some(span) = invocant_span {
if let Some(cn) = self
.expr_type_at_span(*span, module_index)
.and_then(|t| t.class_name().map(|s| s.to_string()))
{
return Some(cn);
}
}
if let Some(span) = invocant_span {
if let Some(&recv_idx) = self.call_ref_by_start.get(&span.start) {
let recv_span = self.refs[recv_idx].span;
let contained = recv_span.start == span.start
&& (recv_span.end.row, recv_span.end.column)
<= (span.end.row, span.end.column);
let is_self = std::ptr::eq(&self.refs[recv_idx], r);
if contained && !is_self {
if let RefKind::MethodCall { .. } = &self.refs[recv_idx].kind {
let recv = &self.refs[recv_idx];
if let Some(recv_class) =
self.method_call_invocant_class(recv, module_index)
{
let recv_method = recv.unqualified_target_name();
if crate::conventions::is_constructor_name(recv_method) {
return Some(recv_class);
}
if let Some(cn) = self
.find_method_return_type(
&recv_class,
recv_method,
module_index,
None,
)
.and_then(|t| t.class_name().map(|s| s.to_string()))
{
return Some(cn);
}
}
return None;
}
}
}
}
let point = invocant_span.map(|s| s.start).unwrap_or(r.span.start);
let first = invocant.as_bytes()[0];
if first == b'$' || first == b'@' || first == b'%' {
if let Some(cn) = self
.inferred_type_via_bag_ctx(invocant, point, module_index)
.and_then(|t| t.class_name().map(|s| s.to_string()))
{
return Some(cn);
}
if crate::conventions::is_conventional_invocant_name(invocant) {
return self.enclosing_class_for_scope(r.scope);
}
return None;
}
let bare = split_qualified(invocant).1;
if let Some(InferredType::ClassName(c)) = self.sub_return_type_at_arity(bare, Some(0)) {
return Some(c);
}
if invocant
.as_bytes()
.first()
.is_some_and(u8::is_ascii_lowercase)
{
if let Some(cls) =
self.resolve_controller_token(invocant, &r.target_name, module_index)
{
return Some(cls);
}
}
Some(invocant.to_string())
}
fn resolve_controller_token(
&self,
token: &str,
action: &str,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<String> {
let camelized = camelize_controller(token);
if camelized.is_empty() {
return None;
}
let idx = module_index?;
let mut candidates: Vec<String> = idx
.modules_with_symbol(action)
.into_iter()
.filter(|m| module_tail_matches(m, &camelized))
.collect();
if candidates.is_empty() {
idx.for_each_cached(&mut |name, _| {
if module_tail_matches(name, &camelized) {
candidates.push(name.to_string());
}
});
}
candidates.sort();
candidates.dedup();
let mut owners: Vec<String> = candidates
.into_iter()
.filter(|cls| {
self.resolve_method_in_ancestors(cls, action, Some(idx))
.is_some()
})
.collect();
owners.sort_by(|a, b| {
is_controller_shaped(b)
.cmp(&is_controller_shaped(a))
.then_with(|| a.cmp(b))
});
owners.into_iter().next()
}
pub fn method_call_invocant_type(
&self,
r: &Ref,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<InferredType> {
let RefKind::MethodCall { invocant, invocant_span, .. } = &r.kind else {
return None;
};
if invocant.is_empty() {
return None;
}
let point = invocant_span.map(|s| s.start).unwrap_or(r.span.start);
if matches!(
invocant.classify(),
crate::conventions::InvocantText::PositionalReceiver
| crate::conventions::InvocantText::CurrentPackage
) {
return self.enclosing_class_for_scope(r.scope).map(InferredType::ClassName);
}
if let Some(span) = invocant_span {
if let Some(&recv_idx) = self.call_ref_by_start.get(&span.start) {
let recv_span = self.refs[recv_idx].span;
let contained = recv_span.start == span.start
&& (recv_span.end.row, recv_span.end.column)
<= (span.end.row, span.end.column);
let is_self = std::ptr::eq(&self.refs[recv_idx], r);
if contained && !is_self {
match &self.refs[recv_idx].kind {
RefKind::MethodCall { .. } => {
return self.method_call_return_type_via_bag(recv_idx, module_index);
}
RefKind::FunctionCall { .. } => {
return self.sub_return_type_at_arity(
&self.refs[recv_idx].target_name,
Some(0),
);
}
_ => {}
}
}
}
}
let first = invocant.as_bytes()[0];
if first == b'$' || first == b'@' || first == b'%' {
if let Some(t) = self.inferred_type_via_bag_ctx(invocant, point, module_index) {
return Some(t);
}
if crate::conventions::is_conventional_invocant_name(invocant) {
return self.enclosing_class_for_scope(r.scope).map(InferredType::ClassName);
}
return None;
}
let bare = split_qualified(invocant).1;
if let Some(InferredType::ClassName(c)) = self.sub_return_type_at_arity(bare, Some(0)) {
return Some(InferredType::ClassName(c));
}
Some(InferredType::ClassName(invocant.to_string()))
}
pub(crate) fn enclosing_class_for_scope(&self, scope: ScopeId) -> Option<String> {
for sid in self.scope_chain(scope).iter() {
let s = self.scope(*sid);
if let ScopeKind::Class { ref name } = s.kind {
return Some(name.clone());
}
if let Some(ref pkg) = s.package {
return Some(pkg.clone());
}
}
None
}
#[cfg(test)]
pub(crate) fn resolve_invocant_class_test(
&self,
invocant: &str,
scope: ScopeId,
point: Point,
) -> Option<String> {
self.resolve_invocant_class(invocant, scope, point)
}
fn resolve_invocant_class(&self, invocant: &str, scope: ScopeId, point: Point) -> Option<String> {
use crate::conventions::InvocantText;
let enclosing = || {
for scope_id in &self.scope_chain(scope) {
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
};
match InvocantText::parse(invocant) {
InvocantText::CurrentPackage | InvocantText::PositionalReceiver => enclosing(),
InvocantText::NonScalar(_) => None,
InvocantText::Scalar(_) => {
self.inferred_type_via_bag(invocant, point)
.and_then(|t| t.class_name().map(|s| s.to_string()))
.or_else(|| {
if crate::conventions::is_conventional_invocant_name(invocant) {
enclosing()
} else {
None
}
})
}
InvocantText::Bareword(_) => {
let bare = split_qualified(invocant).1;
if let Some(InferredType::ClassName(c)) =
self.sub_return_type_at_arity(bare, Some(0))
{
return Some(c);
}
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<&dyn CrossFileLookup>,
) -> 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,
}
}
fn for_each_ancestor_class(
&self,
class_name: &str,
module_index: Option<&dyn CrossFileLookup>,
mut visit: impl FnMut(&str) -> std::ops::ControlFlow<()>,
) {
if visit(class_name).is_break() {
return;
}
let graph = crate::graph::GraphView::new(self, module_index);
graph.walk(
crate::graph::Node::Class(class_name.to_string()),
crate::graph::EdgeKindMask::INHERITS,
&mut |n| match n {
crate::graph::Node::Class(c) => visit(c),
_ => std::ops::ControlFlow::Continue(()),
},
);
}
#[cfg(test)]
pub fn for_each_ancestor_class_test(
&self,
class_name: &str,
module_index: Option<&dyn CrossFileLookup>,
visit: impl FnMut(&str) -> std::ops::ControlFlow<()>,
) {
self.for_each_ancestor_class(class_name, module_index, visit)
}
pub fn class_isa(
&self,
child: &str,
ancestor: &str,
module_index: Option<&dyn CrossFileLookup>,
) -> bool {
if child == ancestor {
return true;
}
let graph = crate::graph::GraphView::new(self, module_index);
let mut found = false;
graph.walk(
crate::graph::Node::Class(child.to_string()),
crate::graph::EdgeKindMask::INHERITS,
&mut |n| {
if matches!(n, crate::graph::Node::Class(c) if c == ancestor) {
found = true;
std::ops::ControlFlow::Break(())
} else {
std::ops::ControlFlow::Continue(())
}
},
);
found
}
pub fn method_rename_chain(
&self,
class_name: &str,
method_name: &str,
module_index: Option<&dyn CrossFileLookup>,
) -> Vec<String> {
let defining = match self.resolve_method_in_ancestors(class_name, method_name, module_index) {
Some(MethodResolution::Local { class, .. })
| Some(MethodResolution::CrossFile { class, .. }) => class,
None => return vec![class_name.to_string()],
};
let mut chain = Vec::new();
self.for_each_ancestor_class(class_name, module_index, |cls| {
chain.push(cls.to_string());
if cls == defining {
std::ops::ControlFlow::Break(())
} else {
std::ops::ControlFlow::Continue(())
}
});
if chain.is_empty() { chain.push(class_name.to_string()); }
chain
}
fn method_resolution_on_class(
&self,
cls: &str,
method_name: &str,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<MethodResolution> {
for &sid in self.symbols_named(method_name) {
let sym = self.symbol(sid);
if matches!(sym.kind, SymKind::Sub | SymKind::Method)
&& self.symbol_in_class(sid, cls)
{
return Some(MethodResolution::Local { class: cls.to_string(), sym_id: sid });
}
}
for ns in &self.plugin_namespaces {
if !ns.bridges.iter().any(|b| matches!(b, Bridge::Class(c) if c == cls)) { continue; }
for sym_id in &ns.entities {
let Some(sym) = self.symbols.get(sym_id.0 as usize) else { continue };
if !matches!(sym.kind, SymKind::Sub | SymKind::Method) { continue; }
if sym.name == method_name {
return Some(MethodResolution::Local { class: cls.to_string(), sym_id: *sym_id });
}
}
}
if let Some(idx) = module_index {
if let Some(cached) = idx.get_cached(cls) {
if cached.has_sub(method_name) {
return Some(MethodResolution::CrossFile { class: cls.to_string(), def_module: None });
}
}
if let Some(home) = idx.module_declaring_method_in_package(method_name, cls) {
return Some(MethodResolution::CrossFile { class: cls.to_string(), def_module: Some(home) });
}
let mut bridged_module: Option<String> = None;
idx.for_each_entity_bridged_to(cls, &mut |mod_name, _cached, sym| {
if bridged_module.is_some() { return; }
if !matches!(sym.kind, SymKind::Sub | SymKind::Method) { return; }
if sym.name == method_name {
bridged_module = Some(mod_name.to_string());
}
});
if bridged_module.is_some() {
return Some(MethodResolution::CrossFile { class: cls.to_string(), def_module: bridged_module });
}
}
None
}
pub fn resolve_method_in_ancestors(
&self,
class_name: &str,
method_name: &str,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<MethodResolution> {
let mut result: Option<MethodResolution> = None;
self.for_each_ancestor_class(class_name, module_index, |cls| {
match self.method_resolution_on_class(cls, method_name, module_index) {
Some(r) => { result = Some(r); std::ops::ControlFlow::Break(()) }
None => std::ops::ControlFlow::Continue(()),
}
});
result
}
pub fn resolve_super_method(
&self,
enclosing: &str,
method_name: &str,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<MethodResolution> {
let mut result: Option<MethodResolution> = None;
let graph = crate::graph::GraphView::new(self, module_index);
graph.walk(
crate::graph::Node::Class(enclosing.to_string()),
crate::graph::EdgeKindMask::INHERITS,
&mut |n| {
let crate::graph::Node::Class(cls) = n else {
return std::ops::ControlFlow::Continue(());
};
match self.method_resolution_on_class(cls, method_name, module_index) {
Some(r) => { result = Some(r); std::ops::ControlFlow::Break(()) }
None => std::ops::ControlFlow::Continue(()),
}
},
);
result
}
pub fn class_has_unresolved_ancestor(
&self,
class_name: &str,
module_index: Option<&dyn CrossFileLookup>,
) -> bool {
let class_is_known = |name: &str| -> bool {
if name == APP_SURFACE_CLASS {
return true;
}
let local = self.symbols.iter().any(|s| {
matches!(s.kind, SymKind::Class | SymKind::Package | SymKind::Module)
&& s.name == name
});
if local {
return true;
}
module_index
.map(|idx| idx.get_cached(name).is_some())
.unwrap_or(false)
};
let mut incomplete = false;
self.for_each_ancestor_class(class_name, module_index, |cls| {
let dynamic_here = self.dynamic_parent_packages.contains(cls)
|| module_index
.and_then(|idx| idx.get_cached(cls))
.is_some_and(|c| c.analysis.dynamic_parent_packages.contains(cls));
if dynamic_here {
incomplete = true;
return std::ops::ControlFlow::Break(());
}
let parents = parents_of(
cls,
&self.package_parents,
module_index,
&self.app_surface_consumers,
);
for p in &parents {
if !class_is_known(p) {
incomplete = true;
return std::ops::ControlFlow::Break(());
}
}
std::ops::ControlFlow::Continue(())
});
incomplete
}
pub fn bridged_helper_provider(
&self,
class: &str,
name: &str,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<String> {
let res = self.resolve_method_in_ancestors(class, name, module_index)?;
let MethodResolution::CrossFile { class: on_class, def_module: Some(module) } = res
else {
return None;
};
let idx = module_index?;
let cached = idx.get_cached(&module)?;
let is_bridge = cached.analysis.plugin_namespaces.iter().any(|ns| {
ns.bridges
.iter()
.any(|b| matches!(b, Bridge::Class(c) if c == &on_class))
&& ns.entities.iter().any(|sid| {
cached.analysis.symbols.get(sid.0 as usize).is_some_and(|s| {
matches!(s.kind, SymKind::Sub | SymKind::Method) && s.name == name
})
})
});
is_bridge.then_some(module)
}
pub fn is_role_package(&self, pkg: &str) -> bool {
self.role_packages.contains(pkg)
}
pub fn unfulfilled_role_requires(
&self,
module_index: Option<&dyn CrossFileLookup>,
) -> Vec<UnfulfilledRequire> {
use std::collections::VecDeque;
let role_facts = |c: &str| -> Option<(Vec<String>, Vec<String>)> {
let is_local = self
.symbols
.iter()
.any(|s| matches!(s.kind, SymKind::Package | SymKind::Class) && s.name == c);
if is_local {
if !self.is_role_package(c) {
return None;
}
return Some((
self.role_requires.get(c).cloned().unwrap_or_default(),
self.package_parents.get(c).cloned().unwrap_or_default(),
));
}
let cached = module_index?.get_cached(c)?;
if !cached.analysis.is_role_package(c) {
return None;
}
Some((
cached.analysis.role_requires.get(c).cloned().unwrap_or_default(),
cached.analysis.package_parents.get(c).cloned().unwrap_or_default(),
))
};
let mut out: Vec<UnfulfilledRequire> = Vec::new();
let mut composers: Vec<&String> = self.package_parents.keys().collect();
composers.sort();
for pkg in composers {
if self.is_role_package(pkg) {
continue;
}
if self.class_has_unresolved_ancestor(pkg, module_index) {
continue;
}
if self
.resolve_method_in_ancestors(pkg, "AUTOLOAD", module_index)
.is_some()
{
continue;
}
let mut required: Vec<(String, String, String)> = Vec::new();
for direct in &self.package_parents[pkg] {
let mut queue: VecDeque<String> = VecDeque::from([direct.clone()]);
let mut seen: HashSet<String> = HashSet::new();
while let Some(c) = queue.pop_front() {
if !seen.insert(c.clone()) || seen.len() > 21 {
continue;
}
let Some((requires, parents)) = role_facts(&c) else { continue };
for n in requires {
required.push((n, c.clone(), direct.clone()));
}
queue.extend(parents);
}
}
let mut checked: HashSet<String> = HashSet::new();
for (name, role, via_parent) in required {
if !checked.insert(name.clone()) {
continue;
}
let mut provided = false;
self.for_each_ancestor_class(pkg, module_index, |a| {
let here = self.class_provides_method(a, &name)
|| module_index
.and_then(|idx| idx.get_cached(a))
.is_some_and(|c| c.analysis.provides_method_anywhere(&name))
|| module_index.is_some_and(|idx| {
idx.module_declaring_method_in_package(&name, a)
.and_then(|home| idx.get_cached(&home))
.is_some_and(|c| {
c.analysis.provides_method_in_package(&name, a)
}) || {
let mut hit = false;
idx.for_each_entity_bridged_to(a, &mut |_m, _c, sym| {
if !hit
&& matches!(sym.kind, SymKind::Sub | SymKind::Method)
&& sym.name == name
{
hit = true;
}
});
hit
}
});
if here {
provided = true;
std::ops::ControlFlow::Break(())
} else {
std::ops::ControlFlow::Continue(())
}
});
if !provided {
out.push(UnfulfilledRequire {
package: pkg.clone(),
role,
name,
via_parent,
});
}
}
}
out
}
fn class_provides_method(&self, cls: &str, name: &str) -> bool {
for &sid in self.symbols_named(name) {
let sym = self.symbol(sid);
if matches!(sym.kind, SymKind::Sub | SymKind::Method)
&& self.symbol_in_class(sid, cls)
&& !self.contract_symbols.contains(&sid)
{
return true;
}
}
self.plugin_namespaces.iter().any(|ns| {
ns.bridges.iter().any(|b| matches!(b, Bridge::Class(c) if c == cls))
&& ns.entities.iter().any(|sid| {
self.symbols.get(sid.0 as usize).is_some_and(|s| {
matches!(s.kind, SymKind::Sub | SymKind::Method) && s.name == name
})
})
})
}
pub fn provides_method_anywhere(&self, name: &str) -> bool {
self.symbols.iter().enumerate().any(|(i, s)| {
s.name == name
&& matches!(s.kind, SymKind::Sub | SymKind::Method)
&& !self.contract_symbols.contains(&SymbolId(i as u32))
})
}
pub fn provides_method_in_package(&self, name: &str, package: &str) -> bool {
self.symbols.iter().enumerate().any(|(i, s)| {
s.name == name
&& matches!(s.kind, SymKind::Sub | SymKind::Method)
&& s.package.as_deref() == Some(package)
&& !self.contract_symbols.contains(&SymbolId(i as u32))
})
}
pub fn for_each_dispatch_handler_on_class(
&self,
class_name: &str,
dispatcher: &str,
module_index: Option<&dyn CrossFileLookup>,
mut visit: impl FnMut(&Symbol, &str),
) {
let disp_matches = |dd: &[String]| dd.iter().any(|d| d == dispatcher);
self.for_each_ancestor_class(class_name, module_index, |cls| {
for sym in &self.symbols {
if let SymbolDetail::Handler { owner, dispatchers, .. } = &sym.detail {
let HandlerOwner::Class(n) = owner;
if n == cls && disp_matches(dispatchers) {
visit(sym, "this file");
}
}
}
for ns in &self.plugin_namespaces {
if !ns.bridges.iter().any(|b| matches!(b, Bridge::Class(c) if c == cls)) { continue; }
for sym_id in &ns.entities {
let Some(sym) = self.symbols.get(sym_id.0 as usize) else { continue };
if let SymbolDetail::Handler { dispatchers, .. } = &sym.detail {
if disp_matches(dispatchers) {
visit(sym, "this file");
}
}
}
}
if let Some(idx) = module_index {
idx.for_each_entity_bridged_to(cls, &mut |_mod, cached, sym| {
if let SymbolDetail::Handler { dispatchers, .. } = &sym.detail {
if disp_matches(dispatchers) {
let prov = cached.path.file_name()
.and_then(|s| s.to_str())
.unwrap_or("cross-file");
visit(sym, prov);
}
}
});
}
std::ops::ControlFlow::Continue(())
});
}
fn format_handler_hover(
&self,
name: &str,
owner: &HandlerOwner,
active_dispatcher: Option<&str>,
module_index: Option<&dyn CrossFileLookup>,
) -> String {
let class = match owner {
HandlerOwner::Class(n) => n.as_str(),
};
let mut registrations: Vec<(usize, Vec<String>)> = self.symbols.iter()
.filter(|s| s.name == name)
.filter_map(|s| match &s.detail {
SymbolDetail::Handler { owner: o, params, .. } if o == owner => {
Some((s.selection_span.start.row + 1, display_handler_params(params)))
}
_ => None,
})
.collect();
if let Some(idx) = module_index {
for module_name in idx.modules_with_symbol(name) {
let Some(cached) = idx.get_cached(&module_name) else { continue };
for sym in &cached.analysis.symbols {
if sym.name != name { continue; }
if let SymbolDetail::Handler { owner: o, params, .. } = &sym.detail {
if o == owner {
registrations.push((
sym.selection_span.start.row + 1,
display_handler_params(params),
));
}
}
}
}
}
registrations.sort();
registrations.dedup();
let registrations_ref: Vec<&Symbol> = self.symbols.iter()
.filter(|s| s.name == name)
.filter(|s| matches!(
&s.detail,
SymbolDetail::Handler { owner: o, .. } if o == owner
))
.collect();
let mut dispatchers: Vec<String> = registrations_ref.iter()
.filter_map(|s| match &s.detail {
SymbolDetail::Handler { dispatchers, .. } => Some(dispatchers.clone()),
_ => None,
})
.flatten()
.collect();
if dispatchers.is_empty() {
if let Some(idx) = module_index {
for module_name in idx.modules_with_symbol(name) {
let Some(cached) = idx.get_cached(&module_name) else { continue };
for sym in &cached.analysis.symbols {
if sym.name != name { continue; }
if let SymbolDetail::Handler { owner: o, dispatchers: ds, .. } = &sym.detail {
if o == owner { dispatchers.extend(ds.clone()); }
}
}
}
}
}
dispatchers.sort();
dispatchers.dedup();
let mut text = String::new();
text.push_str(&format!("**handler `{}`** on `{}`\n\n", name, class));
if registrations.is_empty() {
text.push_str("*no handler registered in this workspace — dispatch will be a no-op*");
return text;
}
let plural = if registrations.len() == 1 { "" } else { "s" };
text.push_str(&format!(
"*{} registration{} stack{}:*\n\n",
registrations.len(),
plural,
if registrations.len() == 1 { "s" } else { "" },
));
for (line, display) in ®istrations {
text.push_str(&format!(
"- **line {}:** `({})`\n",
line,
display.join(", "),
));
}
if !dispatchers.is_empty() {
text.push_str(&format!(
"\n*Dispatch via:* `{}`",
dispatchers.iter()
.map(|d| {
if Some(d.as_str()) == active_dispatcher {
format!("**->{}(...)**", d)
} else {
format!("->{}(...)", d)
}
})
.collect::<Vec<_>>()
.join(", "),
));
}
text
}
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,
module_index: Option<&dyn CrossFileLookup>,
) -> String {
self.format_symbol_hover_at(sym, source, sym.selection_span.end, module_index)
}
fn format_symbol_hover_at(
&self,
sym: &Symbol,
source: &str,
at: Point,
module_index: Option<&dyn CrossFileLookup>,
) -> 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_via_bag_ctx(&sym.name, at, module_index) {
text.push_str(&format!("\n\n*type: {}*", format_inferred_type(&it)));
}
}
if matches!(sym.kind, SymKind::Sub | SymKind::Method) {
if let Some(pkg) = sym.package.as_deref() {
text.push_str(&format!("\n\n*package {}*", pkg));
}
if let SymbolDetail::Sub { ref doc, .. } = sym.detail {
if let Some(rt) = self.symbol_return_type_via_bag(sym.id, None) {
text.push_str(&format!("\n\n*returns: {}*", format_inferred_type(&rt)));
}
if let Some(d) = doc {
text.push_str(&format!("\n\n{}", d));
}
}
}
text
}
pub fn document_symbols(&self) -> Vec<OutlineSymbol> {
let flat = self.outline_children_of(ScopeId(0));
self.nest_under_packages(flat)
}
fn nest_under_packages(&self, flat: Vec<OutlineSymbol>) -> Vec<OutlineSymbol> {
let is_container =
|k: SymKind| matches!(k, SymKind::Package | SymKind::Class);
if !flat.iter().any(|s| is_container(s.kind)) {
return flat;
}
let mut result: Vec<OutlineSymbol> = Vec::new();
for sym in flat {
if is_container(sym.kind) {
result.push(sym);
continue;
}
if let Some(owner) = self.package_at(sym.span.start) {
if let Some(container) = result
.iter_mut()
.rev()
.find(|c| is_container(c.kind) && c.name == owner)
{
container.children.push(sym);
continue;
}
}
result.push(sym);
}
for c in &mut result {
if !is_container(c.kind) {
continue;
}
if let Some(end) = c
.children
.iter()
.map(|ch| ch.span.end)
.max_by_key(|p| (p.row, p.column))
{
if (end.row, end.column) > (c.span.end.row, c.span.end.column) {
c.span.end = end;
}
}
}
result
}
pub fn scope_within_sub_body(&self, scope: ScopeId) -> bool {
for id in self.scope_chain(scope) {
match self.scopes[id.0 as usize].kind {
ScopeKind::Sub { .. } | ScopeKind::Method { .. } => return true,
ScopeKind::Class { .. } | ScopeKind::File => return false,
ScopeKind::Block | ScopeKind::ForLoop { .. } => {}
}
}
false
}
fn outline_children_of(&self, parent_scope: ScopeId) -> Vec<OutlineSymbol> {
let mut result = Vec::new();
let mut outline_seen: HashSet<(SymKind, String, usize, usize)> = HashSet::new();
for sym in &self.symbols {
if sym.scope != parent_scope {
continue;
}
let hidden = match &sym.detail {
SymbolDetail::Sub { hide_in_outline, .. } => *hide_in_outline,
SymbolDetail::Handler { hide_in_outline, .. } => *hide_in_outline,
_ => false,
};
if hidden { continue; }
if matches!(sym.kind, SymKind::Sub | SymKind::Method) && sym.namespace.is_framework() {
let key = (
sym.kind,
sym.name.clone(),
sym.span.start.row,
sym.span.start.column,
);
if !outline_seen.insert(key) {
continue;
}
}
let (name, detail, children) = match sym.kind {
SymKind::Sub | SymKind::Method => {
let body_scope = self.find_body_scope(sym);
let children = body_scope
.map(|s| self.outline_children_of(s))
.unwrap_or_default();
let disp = sub_display_override(&sym.detail);
let default_word = if matches!(sym.kind, SymKind::Method) { "method" } else { "sub" };
let identifier = sym.outline_label.clone().unwrap_or_else(|| sym.name.clone());
let params_suffix = match &sym.detail {
SymbolDetail::Sub { params, .. } => {
let visible: Vec<&str> = params.iter()
.filter(|p| !p.is_invocant)
.map(|p| p.name.as_str())
.collect();
if visible.is_empty() { String::new() }
else { format!(" ({})", visible.join(", ")) }
}
_ => String::new(),
};
let (label, outline_detail) = match disp.and_then(|d| d.outline_word()) {
Some(plugin_word) => (
format!("<{}> {}{}", plugin_word, identifier, params_suffix),
Some(plugin_word.to_string()),
),
None => (
identifier,
Some(format!("{}{}", default_word, params_suffix)),
),
};
(label, outline_detail, 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 => continue,
SymKind::Variable => {
if self.scope_within_sub_body(sym.scope) { continue; }
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, SymKind::Handler => {
let (word, params_suffix) = match &sym.detail {
SymbolDetail::Handler { params, display, .. } => {
let word = display.outline_word().unwrap_or("handler");
let visible: Vec<&str> = params
.iter()
.filter(|p| !p.is_invocant)
.map(|p| p.name.as_str())
.collect();
let suf = if visible.is_empty() { String::new() }
else { format!(" ({})", visible.join(", ")) };
(word, suf)
}
_ => ("handler", String::new()),
};
let identifier = sym.outline_label.clone().unwrap_or_else(|| sym.name.clone());
let label = format!("<{}> {}{}", word, identifier, params_suffix);
(label, Some(word.to_string()), Vec::new())
}
SymKind::Namespace => {
continue;
}
};
let handler_display = match &sym.detail {
SymbolDetail::Handler { display, .. } => Some(*display),
SymbolDetail::Sub { display: Some(d), .. } => Some(*d),
_ => None,
};
result.push(OutlineSymbol {
name,
detail,
kind: sym.kind,
span: sym.span,
selection_span: sym.selection_span,
children,
handler_display,
});
}
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 = crate::conventions::is_conventional_invocant_name(&sym.name);
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 => {
let is_constant = matches!(sym.detail, SymbolDetail::Sub { is_constant: true, .. });
let token_type = if is_constant { TOK_ENUM_MEMBER } else { TOK_FUNCTION };
tokens.push(PerlSemanticToken {
span: sym.selection_span,
token_type,
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.local_name.as_str()))
.collect();
let constant_names: std::collections::HashSet<(&str, &str)> = self.symbols.iter()
.filter_map(|s| match &s.detail {
SymbolDetail::Sub { is_constant: true, .. } =>
s.package.as_deref().map(|p| (p, s.name.as_str())),
_ => None,
})
.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 =
crate::conventions::is_conventional_invocant_name(&r.target_name);
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 { resolved_package } => {
let is_const = resolved_package.as_deref().map_or(false, |pkg| {
constant_names.contains(&(pkg, r.unqualified_target_name()))
});
let token_type = if is_const {
TOK_ENUM_MEMBER
} else 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 });
}
RefKind::DispatchCall { .. } => {
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)]
pub struct UnfulfilledRequire {
pub package: String,
pub role: String,
pub name: String,
pub via_parent: String,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum MethodResolution {
Local { class: String, sym_id: SymbolId },
CrossFile { class: String, def_module: Option<String> },
}
impl MethodResolution {
pub fn class(&self) -> &str {
match self {
MethodResolution::Local { class, .. } | MethodResolution::CrossFile { class, .. } => class,
}
}
}
pub enum ResolvedSub<'a> {
Local(&'a Symbol),
CrossFile {
params: Vec<ParamInfo>,
param_types: Vec<Option<InferredType>>,
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)>,
pub display_override: Option<HandlerDisplay>,
}
#[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,
module_index: Option<&dyn CrossFileLookup>,
) -> Vec<CompletionCandidate> {
let class_name = 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, module_index);
if !candidates.is_empty() {
return candidates;
}
}
let mut seen = HashSet::<String>::new();
self.symbols
.iter()
.filter(|s| matches!(s.kind, SymKind::Sub | SymKind::Method))
.filter(|s| !s.namespace.is_framework())
.filter(|s| seen.insert(s.name.clone()))
.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![],
display_override: sub_display_override(&s.detail),
})
.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![],
display_override: None,
});
}
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> {
let mut out = Vec::new();
let mut seen = HashSet::new();
let push_unique = |cands: Vec<CompletionCandidate>, out: &mut Vec<CompletionCandidate>, seen: &mut HashSet<String>| {
for c in cands {
if seen.insert(c.label.clone()) {
out.push(c);
}
}
};
for sym in &self.symbols {
if (sym.kind == SymKind::Sub || sym.kind == SymKind::Method) && sym.name == sub_name {
let owner = HashKeyOwner::Sub { package: sym.package.clone(), name: sub_name.to_string() };
push_unique(self.complete_hash_keys_for_owner(&owner), &mut out, &mut seen);
}
}
let imported_owner = HashKeyOwner::Sub { package: None, name: sub_name.to_string() };
push_unique(self.complete_hash_keys_for_owner(&imported_owner), &mut out, &mut seen);
let pseudo_syms: Vec<_> = self.symbols.iter()
.filter(|s| {
if !matches!(s.kind, SymKind::HashKeyDef) { return false; }
matches!(&s.detail, SymbolDetail::HashKeyDef {
owner: HashKeyOwner::Sub { name, .. }, ..
} if name == sub_name)
})
.collect();
for def in pseudo_syms {
if seen.insert(def.name.clone()) {
let detail = format!("{}() option", sub_name);
out.push(CompletionCandidate {
label: def.name.clone(),
kind: SymKind::Variable,
detail: Some(detail),
insert_text: None,
sort_priority: PRIORITY_FILE_WIDE,
additional_edits: vec![],
display_override: None,
});
}
}
for sym in &self.symbols {
if sym.name != sub_name { continue; }
if !matches!(sym.kind, SymKind::Sub | SymKind::Method) { continue; }
let params = match &sym.detail {
SymbolDetail::Sub { params, .. } => params,
_ => continue,
};
let hashish = params
.iter()
.find(|p| p.is_slurpy && p.name.starts_with('%'))
.or_else(|| params
.last()
.filter(|p| !p.is_invocant && p.name.starts_with('$')));
let bare_name = match hashish {
Some(p) if p.name.len() > 1 => &p.name[1..],
_ => continue,
};
let Some(body_scope) = self.find_body_scope(sym) else { continue };
for k in self.hash_keys_in_scope(bare_name, body_scope) {
if seen.insert(k.clone()) {
out.push(CompletionCandidate {
label: k,
kind: SymKind::Variable,
detail: Some(format!("{}() option", sub_name)),
insert_text: None,
sort_priority: PRIORITY_FILE_WIDE,
additional_edits: vec![],
display_override: None,
});
}
}
}
out
}
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![],
display_override: None,
});
}
}
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![],
display_override: None,
});
}
}
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<&dyn CrossFileLookup>,
) -> Vec<CompletionCandidate> {
if crate::conventions::is_constructor_name(call_name) {
if let Some(inv) = invocant {
let class_name = 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 hashish = params
.iter()
.find(|p| p.is_slurpy && p.name.starts_with('%'))
.or_else(|| params
.last()
.filter(|p| !p.is_invocant && p.name.starts_with('$')));
let slurpy_name = match hashish {
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(),
};
if keys.is_empty() { return 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![],
display_override: None,
})
.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![],
display_override: None,
})
.collect()
}
}
}
pub fn signature_for_call(
&self,
name: &str,
is_method: bool,
invocant: Option<&str>,
point: Point,
module_index: Option<&dyn CrossFileLookup>,
) -> 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| {
crate::conventions::is_conventional_invocant_name(&p.name)
});
if !params.is_empty() && params[0].is_invocant {
params.remove(0);
}
Some(SignatureInfo {
name: name.to_string(),
params,
is_method,
body_end: sub_sym.span.end,
param_types: None, })
}
ResolvedSub::CrossFile {
params: cross_params,
param_types: cross_param_types,
is_method: cf_is_method,
..
} => {
let mut params: Vec<ParamInfo> = cross_params;
let mut param_types: Vec<Option<String>> = cross_param_types
.into_iter()
.map(|t| t.as_ref().map(inferred_type_to_tag))
.collect();
let is_method = is_method
|| cf_is_method
|| params.first().map_or(false, |p| {
crate::conventions::is_conventional_invocant_name(&p.name)
});
if !params.is_empty() && params[0].is_invocant {
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<&dyn CrossFileLookup>,
) -> Option<ResolvedSub<'s>> {
let scope = self.scope_at(point).unwrap_or(ScopeId(0));
let token = crate::conventions::MethodToken::parse(name);
let name = token.name();
let fq_class = match token {
crate::conventions::MethodToken::Super(tail) => self
.enclosing_class_for_scope(scope)
.and_then(|e| self.resolve_super_method(&e, tail, module_index))
.map(|r| r.class().to_string()),
t => t.literal_package().map(str::to_string),
};
let class_name = if fq_class.is_some() {
fq_class
} else if is_method {
invocant.and_then(|inv| self.resolve_invocant_class(inv, scope, 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(cached) = idx.get_cached(class) {
if let Some(sub_info) = cached.sub_info(name) {
return Some(cross_file_resolved(&sub_info));
}
}
}
}
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 {
let Some(cached) = idx.get_cached(&import.module_name) else { continue };
let surface = cached.analysis.export_surface_with_index(idx);
let bound = imported_names(import, &surface);
if let Some((_local, remote)) = bound.iter().find(|(local, _)| local == name) {
if let Some(cached) = idx.defining_module_cached(&import.module_name, remote) {
if let Some(sub_info) = cached.sub_info(remote) {
return Some(cross_file_resolved(&sub_info));
}
}
}
}
}
}
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_via_bag(var_text, point) {
if let Some(cn) = it.hash_key_class() {
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)
{
let package = self.sub_defining_package(&cb.func_name);
return Some(HashKeyOwner::Sub { package, name: 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)
{
let package = self.sub_defining_package(&mcb.method_name);
return Some(HashKeyOwner::Sub { package, name: 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 sub_defining_package(&self, name: &str) -> Option<String> {
for sym in &self.symbols {
if (sym.kind == SymKind::Sub || sym.kind == SymKind::Method) && sym.name == name {
return sym.package.clone();
}
}
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.bare_name().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![],
display_override: None,
});
}
}
}
}
}
}
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![],
display_override: None,
});
}
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![],
display_override: None,
});
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![],
display_override: None,
});
}
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![],
display_override: None,
});
}
}
'@' => {
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![],
display_override: None,
});
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![],
display_override: None,
});
}
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![],
display_override: None,
});
}
}
'%' => {
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![],
display_override: None,
});
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![],
display_override: None,
});
}
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![],
display_override: None,
});
}
}
_ => {}
}
}
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 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 camelize_controller(token: &str) -> String {
if token.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
return token.to_string();
}
token
.split('-')
.map(|segment| {
segment
.split('_')
.map(ucfirst_lc)
.collect::<Vec<_>>()
.join("")
})
.collect::<Vec<_>>()
.join("::")
}
fn ucfirst_lc(piece: &str) -> String {
let lower = piece.to_lowercase();
let mut chars = lower.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
fn module_tail_matches(module: &str, camelized: &str) -> bool {
module == camelized
|| module
.strip_suffix(camelized)
.is_some_and(|prefix| prefix.ends_with("::"))
}
fn is_controller_shaped(class: &str) -> bool {
class.contains("::Controller::") || class.ends_with("::Controller")
}
fn display_handler_params(params: &[ParamInfo]) -> Vec<String> {
params
.iter()
.filter(|p| !p.is_invocant)
.map(|p| p.name.clone())
.collect()
}
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::HashWithKeys { .. } => "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(),
InferredType::Parametric(p) => match p.class_name() {
Some(c) => format!("Object:{}", c),
None => "Parametric".to_string(),
},
InferredType::Sequence(_) => "Sequence".to_string(),
InferredType::TypeConstraintOf(_) => "Object:Type::Tiny".to_string(),
InferredType::BrandedRoute { base, .. } => format!("Object:{}", base),
InferredType::Optional(inner) => format!("Maybe:{}", inferred_type_to_tag(inner)),
InferredType::Undef => "Undef".to_string(),
}
}
fn format_cross_file_signature(method_name: &str, sub_info: &SubInfo<'_>) -> String {
let params = sub_info.params();
if params.is_empty() {
format!("sub {}()", method_name)
} else {
let names: Vec<&str> = params.iter().map(|p| p.name.as_str()).collect();
format!("sub {}({})", method_name, names.join(", "))
}
}
fn cross_file_resolved(sub_info: &SubInfo<'_>) -> ResolvedSub<'static> {
let params: Vec<ParamInfo> = sub_info.params().to_vec();
let param_types: Vec<Option<InferredType>> = params
.iter()
.map(|p| sub_info.param_inferred_type(&p.name))
.collect();
ResolvedSub::CrossFile {
params,
param_types,
is_method: sub_info.is_method(),
hash_keys: sub_info.hash_keys().to_vec(),
}
}
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::HashWithKeys { .. } => "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(),
InferredType::Parametric(p) => format_parametric_type(p),
InferredType::Sequence(elems) => {
let mut parts: Vec<String> =
elems.iter().take(4).map(format_inferred_type).collect();
if elems.len() > 4 {
parts.push("…".to_string());
}
format!("Sequence<{}>", parts.join(", "))
}
InferredType::TypeConstraintOf(inner) => {
format!("TypeConstraint<{}>", format_inferred_type(inner))
}
InferredType::BrandedRoute { base, controller, .. } => match controller {
Some(c) => format!("{}<controller={}>", base, c),
None => base.clone(),
},
InferredType::Optional(inner) => format!("Maybe<{}>", format_inferred_type(inner)),
InferredType::Undef => "Undef".to_string(),
}
}
fn format_parametric_type(p: &ParametricType) -> String {
match p {
ParametricType::ResultSet { base, row } => {
format!("{}<{}>", base, row)
}
}
}
#[cfg(test)]
#[path = "file_analysis_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "call_ref_index_tests.rs"]
mod call_ref_index_tests;
#[cfg(test)]
#[path = "parametric_resultset_tests.rs"]
mod parametric_resultset_tests;
#[cfg(test)]
#[path = "return_expr_tests.rs"]
mod return_expr_tests;