use std::path::PathBuf;
use tower_lsp::lsp_types::Url;
use crate::file_analysis::{AccessKind, CrossFileLookup, FileAnalysis, HandlerOwner, RefKind, Span, SymKind};
use crate::file_store::{FileKey, FileStore};
bitflags::bitflags! {
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct RoleMask: u8 {
const OPEN = 1 << 0;
const WORKSPACE = 1 << 1;
const DEPENDENCY = 1 << 2;
const BUILTIN = 1 << 3;
const EDITABLE = Self::OPEN.bits() | Self::WORKSPACE.bits();
const VISIBLE = Self::OPEN.bits() | Self::WORKSPACE.bits() | Self::DEPENDENCY.bits() | Self::BUILTIN.bits();
}
}
#[derive(Debug, Clone)]
pub struct TargetRef {
pub name: String,
pub kind: TargetKind,
pub method_classes: Vec<String>,
}
impl TargetRef {
pub fn method(
name: String,
class: String,
origin: &FileAnalysis,
module_index: Option<&dyn CrossFileLookup>,
) -> Self {
let method_classes = origin.method_rename_chain(&class, &name, module_index);
TargetRef {
name,
kind: TargetKind::Method { class },
method_classes,
}
}
pub fn new(name: String, kind: TargetKind) -> Self {
debug_assert!(
!matches!(kind, TargetKind::Method { .. }),
"use TargetRef::method so the rename chain is populated"
);
TargetRef { name, kind, method_classes: Vec::new() }
}
pub fn supports_cross_file_rename(&self) -> bool {
matches!(
self.kind,
TargetKind::Sub { .. } | TargetKind::Method { .. } | TargetKind::Package
)
}
pub fn from_rename_kind(
kind: crate::file_analysis::RenameKind,
origin: &FileAnalysis,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<Self> {
use crate::file_analysis::RenameKind;
Some(match kind {
RenameKind::Function { name, package } => {
TargetRef::new(name, TargetKind::Sub { package })
}
RenameKind::Method { name, class } => {
TargetRef::method(name, class, origin, module_index)
}
RenameKind::Package(name) => TargetRef::new(name, TargetKind::Package),
RenameKind::Handler { owner, name } => {
TargetRef::new(name.clone(), TargetKind::Handler { owner, name })
}
RenameKind::HashKey(_) | RenameKind::Variable => return None,
})
}
}
#[derive(Debug, Clone)]
pub enum ResolvedTarget {
Target(TargetRef),
Group {
local_spans: Vec<Span>,
pinned_spans: Vec<(PathBuf, Span)>,
members: Vec<GroupMember>,
},
Local,
}
#[derive(Debug, Clone)]
pub struct GroupMember {
pub target: TargetRef,
pub rename: MemberRename,
}
#[derive(Debug, Clone)]
pub enum MemberRename {
Bare,
Affixed { prefix: String, suffix: String },
Skip,
}
impl MemberRename {
fn text_for(&self, bare_new: &str) -> Option<String> {
match self {
MemberRename::Bare => Some(bare_new.to_string()),
MemberRename::Affixed { prefix, suffix } => {
Some(format!("{}{}{}", prefix, bare_new, suffix))
}
MemberRename::Skip => None,
}
}
}
pub fn resolve_symbol(
analysis: &FileAnalysis,
point: tree_sitter::Point,
module_index: Option<&dyn CrossFileLookup>,
) -> Option<ResolvedTarget> {
use crate::file_analysis::{HashKeyOwner, RenameKind};
if let Some(p) = analysis.field_projections_at(point) {
return Some(group_from_projections(p, analysis, None, module_index));
}
if let (Some(idx), Some(r)) = (module_index, analysis.ref_at(point)) {
use crate::file_analysis::HashKeyOwner;
match &r.kind {
RefKind::HashKeyAccess { owner: None, .. } => {
if let Some(HashKeyOwner::Sub { package: Some(class), name }) =
analysis.deferred_hash_key_owner(r, module_index)
{
if crate::conventions::is_constructor_name(&name) {
if let Some(cached) = idx.get_cached(&class) {
if let Some(p) = cached
.analysis
.field_projections_named(&r.target_name, &class)
{
return Some(group_from_projections(
p,
&cached.analysis,
Some(cached.path.clone()),
module_index,
));
}
}
}
}
}
RefKind::MethodCall { .. } => {
let bare = r.unqualified_target_name().to_string();
if let Some(class) = analysis.method_call_invocant_class(r, module_index) {
if let Some(cached) = idx.get_cached(&class) {
if let Some(p) =
cached.analysis.field_projections_named(&bare, &class)
{
if p.has_reader {
return Some(group_from_projections(
p,
&cached.analysis,
Some(cached.path.clone()),
module_index,
));
}
}
}
}
}
_ => {}
}
}
Some(match analysis.rename_kind_at(point, module_index)? {
RenameKind::Variable => ResolvedTarget::Local,
RenameKind::HashKey(name) => match analysis.hash_key_owner_at(point) {
Some(HashKeyOwner::Sub { package, name: sub_name }) => ResolvedTarget::Target(
TargetRef::new(name, TargetKind::HashKeyOfSub { package, name: sub_name }),
),
Some(HashKeyOwner::Class(class)) => {
ResolvedTarget::Target(TargetRef::new(name, TargetKind::HashKeyOfClass(class)))
}
_ => ResolvedTarget::Local,
},
kind => ResolvedTarget::Target(
TargetRef::from_rename_kind(kind, analysis, module_index)
.expect("Function/Method/Package/Handler kinds map to a target"),
),
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TargetKind {
Sub { package: Option<String> },
Method { class: String },
Package,
HashKeyOfSub { package: Option<String>, name: String },
HashKeyOfClass(String),
InternalHashKey { class: String },
Handler {
owner: HandlerOwner,
name: String,
},
}
#[derive(Debug, Clone)]
pub struct RefLocation {
pub key: FileKey,
pub span: Span,
#[allow(dead_code)]
pub access: AccessKind,
}
impl RefLocation {
pub fn to_url(&self) -> Option<Url> {
match &self.key {
FileKey::Url(u) => Some(u.clone()),
FileKey::Path(p) => Url::from_file_path(p).ok(),
}
}
}
fn group_from_projections(
p: crate::file_analysis::FieldProjections,
class_analysis: &FileAnalysis,
pinned_path: Option<PathBuf>,
module_index: Option<&dyn CrossFileLookup>,
) -> ResolvedTarget {
let mut members = Vec::new();
if p.has_reader {
members.push(GroupMember {
target: TargetRef::method(
p.bare.clone(),
p.class.clone(),
class_analysis,
module_index,
),
rename: MemberRename::Bare,
});
}
if p.has_param {
members.push(GroupMember {
target: TargetRef::new(
p.bare.clone(),
TargetKind::HashKeyOfSub {
package: Some(p.class.clone()),
name: "new".to_string(),
},
),
rename: MemberRename::Bare,
});
}
if p.has_internal {
members.push(GroupMember {
target: TargetRef::new(
p.bare.clone(),
TargetKind::InternalHashKey { class: p.class.clone() },
),
rename: MemberRename::Bare,
});
}
for m in &p.mapped {
members.push(GroupMember {
target: TargetRef::method(
m.method.clone(),
p.class.clone(),
class_analysis,
module_index,
),
rename: match &m.affix {
Some((pre, suf)) => MemberRename::Affixed {
prefix: pre.clone(),
suffix: suf.clone(),
},
None => MemberRename::Skip,
},
});
}
match pinned_path {
None => ResolvedTarget::Group {
local_spans: p.variable_spans,
pinned_spans: Vec::new(),
members,
},
Some(path) => ResolvedTarget::Group {
local_spans: Vec::new(),
pinned_spans: p
.variable_spans
.into_iter()
.map(|s| (path.clone(), s))
.collect(),
members,
},
}
}
pub fn group_refs(
files: &FileStore,
module_index: Option<&dyn CrossFileLookup>,
origin: &FileKey,
local_spans: &[Span],
pinned_spans: &[(PathBuf, Span)],
members: &[GroupMember],
mask_override: Option<RoleMask>,
) -> Vec<RefLocation> {
let mut out: Vec<RefLocation> = local_spans
.iter()
.map(|span| RefLocation {
key: origin.clone(),
span: *span,
access: AccessKind::Read,
})
.collect();
out.extend(pinned_spans.iter().map(|(path, span)| RefLocation {
key: FileKey::Path(path.clone()),
span: *span,
access: AccessKind::Read,
}));
for m in members {
let mask = mask_override
.unwrap_or_else(|| references_mask_for(files, module_index, &m.target));
out.extend(refs_to(files, module_index, &m.target, mask));
}
out.sort_by(|a, b| {
key_for_sort(&a.key)
.cmp(&key_for_sort(&b.key))
.then_with(|| {
(a.span.start.row, a.span.start.column)
.cmp(&(b.span.start.row, b.span.start.column))
})
});
out.dedup_by(|a, b| file_key_eq(&a.key, &b.key) && a.span == b.span);
out
}
pub fn group_rename_edits(
files: &FileStore,
module_index: Option<&dyn CrossFileLookup>,
origin: &FileKey,
local_spans: &[Span],
pinned_spans: &[(PathBuf, Span)],
members: &[GroupMember],
bare_new: &str,
) -> Vec<(RefLocation, String)> {
let mut out: Vec<(RefLocation, String)> = local_spans
.iter()
.map(|span| {
(
RefLocation { key: origin.clone(), span: *span, access: AccessKind::Read },
bare_new.to_string(),
)
})
.collect();
out.extend(pinned_spans.iter().map(|(path, span)| {
(
RefLocation {
key: FileKey::Path(path.clone()),
span: *span,
access: AccessKind::Read,
},
bare_new.to_string(),
)
}));
let mut ordered: Vec<&GroupMember> = members
.iter()
.filter(|m| matches!(m.rename, MemberRename::Bare))
.collect();
ordered.extend(
members
.iter()
.filter(|m| !matches!(m.rename, MemberRename::Bare)),
);
for m in ordered {
let Some(text) = m.rename.text_for(bare_new) else { continue };
for loc in refs_to(files, module_index, &m.target, RoleMask::EDITABLE) {
out.push((loc, text.clone()));
}
}
let mut seen = std::collections::HashSet::new();
out.retain(|(loc, _)| seen.insert((key_for_sort(&loc.key), loc.span)));
out
}
pub fn refs_to(
files: &FileStore,
module_index: Option<&dyn CrossFileLookup>,
target: &TargetRef,
mask: RoleMask,
) -> Vec<RefLocation> {
let mut out = Vec::new();
let mut covered_paths: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
if mask.contains(RoleMask::OPEN) {
files.for_each_open_mut(|url, doc| {
let url = url.clone();
if let Ok(p) = url.to_file_path() {
covered_paths.insert(p);
}
collect_from_analysis(&FileKey::Url(url), &doc.analysis, target, module_index, &mut out);
});
} else {
files.for_each_open_mut(|url, _doc| {
if let Ok(p) = url.to_file_path() {
covered_paths.insert(p);
}
});
}
if mask.contains(RoleMask::WORKSPACE) {
for entry in files.workspace_raw().iter() {
if covered_paths.contains(entry.key()) {
continue;
}
collect_from_analysis(&FileKey::Path(entry.key().clone()), entry.value(), target, module_index, &mut out);
}
}
if mask.contains(RoleMask::DEPENDENCY) {
if let Some(idx) = module_index {
idx.for_each_cached(&mut |_module_name, cached| {
let key = FileKey::Path(cached.path.clone());
collect_from_analysis(&key, &cached.analysis, target, module_index, &mut out);
});
}
}
out.sort_by(|a, b| {
key_for_sort(&a.key)
.cmp(&key_for_sort(&b.key))
.then_with(|| {
(a.span.start.row, a.span.start.column)
.cmp(&(b.span.start.row, b.span.start.column))
})
});
out.dedup_by(|a, b| file_key_eq(&a.key, &b.key) && a.span == b.span);
out
}
pub fn implementations_of(
origin: &FileAnalysis,
module_index: Option<&dyn CrossFileLookup>,
target: &TargetRef,
) -> Vec<RefLocation> {
let TargetKind::Method { class } = &target.kind else {
return Vec::new();
};
let Some(idx) = module_index else {
return Vec::new();
};
let mut descendants: Vec<String> = Vec::new();
let probe = crate::graph::GraphView::new(origin, Some(idx));
probe.walk(
crate::graph::Node::Class(class.clone()),
crate::graph::EdgeKindMask::INHERITS_INV,
&mut |n| {
if let crate::graph::Node::Class(c) = n {
descendants.push(c.clone());
}
std::ops::ControlFlow::Continue(())
},
);
let mut out: Vec<RefLocation> = Vec::new();
for pkg in &descendants {
let mut homes: Vec<std::sync::Arc<crate::file_analysis::CachedModule>> = Vec::new();
if let Some(c) = idx.get_cached(pkg) {
homes.push(c);
} else {
for m in idx.modules_with_symbol(pkg) {
if let Some(c) = idx.get_cached(&m) {
let declares = c.analysis.symbols.iter().any(|s| {
matches!(s.kind, SymKind::Package | SymKind::Class) && &s.name == pkg
});
if declares {
homes.push(c);
}
}
}
}
for cached in homes {
let is_marker = cached
.analysis
.role_requires
.get(pkg.as_str())
.is_some_and(|reqs| reqs.iter().any(|r| r == &target.name));
if is_marker {
continue;
}
for s in &cached.analysis.symbols {
if s.name == target.name
&& matches!(s.kind, SymKind::Sub | SymKind::Method)
&& s.package.as_deref() == Some(pkg.as_str())
{
out.push(RefLocation {
key: FileKey::Path(cached.path.clone()),
span: s.selection_span,
access: AccessKind::Declaration,
});
}
}
}
}
out.sort_by(|a, b| {
key_for_sort(&a.key)
.cmp(&key_for_sort(&b.key))
.then_with(|| {
(a.span.start.row, a.span.start.column)
.cmp(&(b.span.start.row, b.span.start.column))
})
});
out.dedup_by(|a, b| file_key_eq(&a.key, &b.key) && a.span == b.span);
out
}
fn key_for_sort(k: &FileKey) -> PathBuf {
match k {
FileKey::Path(p) => p.clone(),
FileKey::Url(u) => u.to_file_path().unwrap_or_else(|_| PathBuf::from(u.as_str())),
}
}
fn file_key_eq(a: &FileKey, b: &FileKey) -> bool {
key_for_sort(a) == key_for_sort(b)
}
fn symbol_defines_target(sym: &crate::file_analysis::Symbol, target: &TargetRef) -> bool {
use crate::file_analysis::{HashKeyOwner, SymbolDetail};
if sym.name != target.name {
return false;
}
match &target.kind {
TargetKind::Sub { package } => {
matches!(sym.kind, SymKind::Sub | SymKind::Method) && sym.package == *package
}
TargetKind::Method { class } => {
let on_chain = target
.method_classes
.iter()
.any(|c| Some(c.as_str()) == sym.package.as_deref())
|| sym.package.as_deref() == Some(class.as_str());
matches!(sym.kind, SymKind::Sub | SymKind::Method) && on_chain
}
TargetKind::Package => matches!(
sym.kind,
SymKind::Package | SymKind::Class | SymKind::Module
),
TargetKind::HashKeyOfSub { package, name } => matches!(
&sym.detail,
SymbolDetail::HashKeyDef {
owner: HashKeyOwner::Sub { package: op, name: on },
..
} if op == package && on == name
),
TargetKind::HashKeyOfClass(wanted) => matches!(
&sym.detail,
SymbolDetail::HashKeyDef { owner: HashKeyOwner::Class(n), .. } if n == wanted
),
TargetKind::InternalHashKey { .. } => false,
TargetKind::Handler { owner, name: hname } => {
sym.name == *hname
&& matches!(
&sym.detail,
SymbolDetail::Handler { owner: o, .. } if o == owner
)
}
}
}
pub fn references_mask_for(
files: &FileStore,
module_index: Option<&dyn CrossFileLookup>,
target: &TargetRef,
) -> RoleMask {
let mut found_in_editable = false;
files.for_each_open_mut(|_url, doc| {
if doc.analysis.symbols.iter().any(|s| symbol_defines_target(s, target)) {
found_in_editable = true;
}
});
if !found_in_editable {
for entry in files.workspace_raw().iter() {
if entry.value().symbols.iter().any(|s| symbol_defines_target(s, target)) {
found_in_editable = true;
break;
}
}
}
if !found_in_editable {
if let (TargetKind::Method { class }, Some(_idx)) = (&target.kind, module_index) {
let mut declared_in_workspace = false;
for entry in files.workspace_raw().iter() {
if entry.value().symbols.iter().any(|s| {
matches!(s.kind, SymKind::Package | SymKind::Class)
&& s.name == *class
}) {
declared_in_workspace = true;
break;
}
}
if declared_in_workspace {
found_in_editable = true;
}
}
}
if found_in_editable {
RoleMask::EDITABLE
} else {
RoleMask::VISIBLE
}
}
fn collect_from_analysis(
key: &FileKey,
analysis: &FileAnalysis,
target: &TargetRef,
module_index: Option<&dyn CrossFileLookup>,
out: &mut Vec<RefLocation>,
) {
use crate::file_analysis::HashKeyOwner;
let mut rename_chain_cache: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for sym in &analysis.symbols {
if symbol_defines_target(sym, target) {
out.push(RefLocation {
key: key.clone(),
span: sym.selection_span,
access: AccessKind::Declaration,
});
}
}
let callable_scope_for_refs: Option<Option<String>> = match &target.kind {
TargetKind::Sub { package } => Some(package.clone()),
TargetKind::Method { class } => Some(Some(class.clone())),
_ => None,
};
for r in &analysis.refs {
let name_matches = if matches!(r.kind, RefKind::FunctionCall { .. } | RefKind::MethodCall { .. }) {
r.unqualified_target_name() == target.name
} else {
r.target_name == target.name
};
if !name_matches {
continue;
}
let matches_kind = match (&target.kind, &r.kind) {
(TargetKind::Sub { .. } | TargetKind::Method { .. },
RefKind::FunctionCall { resolved_package }) => {
let scope = callable_scope_for_refs.as_ref().unwrap();
resolved_package == scope
}
(TargetKind::Sub { .. } | TargetKind::Method { .. },
RefKind::MethodCall { .. }) => {
let scope = callable_scope_for_refs.as_ref().unwrap();
let method = r.unqualified_target_name();
{
let resolved_class = match r.resolved_method_target.as_ref() {
Some(edge) => Some(edge.invocant_class().to_string()),
None => analysis.method_call_invocant_class(r, module_index),
};
match (resolved_class, scope) {
(Some(cn), Some(pkg)) => {
cn == *pkg || rename_chain_cache
.entry(cn.clone())
.or_insert_with(|| {
analysis.method_rename_chain(&cn, method, module_index)
})
.iter()
.any(|c| c == pkg)
}
_ => false,
}
}
}
(TargetKind::Package, RefKind::PackageRef) => true,
(
TargetKind::HashKeyOfSub { package, name },
RefKind::HashKeyAccess { owner, .. },
) => match owner {
Some(HashKeyOwner::Sub { package: op, name: on }) => {
op == package && on == name
}
None => analysis
.deferred_hash_key_owner(r, module_index)
.is_some_and(|o| {
matches!(
o,
HashKeyOwner::Sub { package: op, name: on }
if op == *package && on == *name
)
}),
Some(_) => false,
},
(TargetKind::HashKeyOfClass(wanted), RefKind::HashKeyAccess { owner, .. }) => {
let target_owner = HashKeyOwner::Class(wanted.clone());
matches!(owner, Some(o) if o.found_by(&target_owner))
}
(TargetKind::InternalHashKey { class },
RefKind::HashKeyAccess { owner, .. }) => {
matches!(
owner,
Some(HashKeyOwner::Class(c))
if c == class || analysis.class_isa(c, class, module_index)
)
}
(TargetKind::Handler { owner, name: hname },
RefKind::DispatchCall { owner: ref_owner, .. }) => {
r.target_name == *hname
&& matches!(ref_owner, Some(o) if o == owner)
}
_ => false,
};
if matches_kind {
let span = if let RefKind::MethodCall { method_name_span, .. } = &r.kind {
*method_name_span
} else {
r.span
};
out.push(RefLocation {
key: key.clone(),
span,
access: r.access,
});
}
}
if let TargetKind::Handler { owner, name: hname } = &target.kind {
for applied in analysis.applicable_dispatches(module_index) {
if &applied.name == hname && &applied.owner == owner {
out.push(RefLocation {
key: key.clone(),
span: applied.span,
access: AccessKind::Read,
});
}
}
}
}
#[cfg(test)]
#[path = "resolve_tests.rs"]
mod tests;