use std::collections::{HashMap, HashSet};
use rayon::prelude::*;
use crate::db::Database;
use crate::types::*;
const CROSS_FILE_BLOCKLIST: &[&str] = &[
"Result",
"Option",
"String",
"Vec",
"Box",
"Arc",
"Rc",
"Ok",
"Err",
"Some",
"None",
"fmt",
"format",
"display",
"to_string",
"clone",
"clone_from",
"default",
"from",
"into",
"try_from",
"try_into",
"new",
"build",
"builder",
"parse",
"from_str",
"eq",
"ne",
"cmp",
"partial_cmp",
"hash",
"next",
"iter",
"into_iter",
"drop",
"deref",
"deref_mut",
"as_ref",
"as_mut",
"borrow",
"borrow_mut",
"read",
"write",
"flush",
"close",
"len",
"is_empty",
"contains",
"push",
"pop",
"insert",
"remove",
"get",
"unwrap",
"expect",
"map",
"and_then",
"or_else",
"unwrap_or",
"assert",
"assert_eq",
"assert_ne",
"debug_assert",
"run",
"start",
"stop",
"init",
"setup",
"status",
"modified",
"output",
"exists",
"join",
"display",
"to_owned",
"collect",
"filter",
"find",
"take",
"skip",
"count",
"sum",
"max",
"min",
"sort",
"extend",
"chain",
"zip",
"enumerate",
"flatten",
"open",
"create",
"metadata",
"canonicalize",
"spawn",
"wait",
"send",
"recv",
"lock",
"try_lock",
];
pub fn simple_ref_name(name: &str) -> &str {
let after_path = name.rsplit("::").next().unwrap_or(name);
after_path.rsplit('.').next().unwrap_or(after_path)
}
fn ruby_constant_name(node: &Node) -> &str {
let mut name = node.qualified_name.as_str();
while let Some(unqualified) = name
.strip_prefix(&node.file_path)
.and_then(|name| name.strip_prefix("::"))
{
name = unqualified;
}
name
}
fn split_ruby_receiver_call(reference_name: &str) -> Option<(&str, &str)> {
let separators = ["&.", ".", "::"];
let (index, separator) = separators
.iter()
.filter_map(|separator| {
reference_name.rfind(separator).and_then(|index| {
if *separator == "." && reference_name[..index].ends_with('&') {
None
} else {
Some((index, *separator))
}
})
})
.max_by_key(|(index, _)| *index)?;
let receiver = &reference_name[..index];
let method_name = &reference_name[index + separator.len()..];
(!receiver.is_empty() && !method_name.is_empty()).then_some((receiver, method_name))
}
fn suppress_go_selector_bare_siblings(resolved: &mut Vec<ResolvedRef>) {
let suppressed: HashSet<(&str, &str, u32, u32, &str)> = resolved
.iter()
.filter(|r| r.resolved_by == "go-selector-import")
.filter_map(|r| {
let bare = r.original.reference_name.rsplit('.').next()?;
Some((
r.original.from_node_id.as_str(),
r.original.file_path.as_str(),
r.original.line,
r.original.column,
bare,
))
})
.collect();
if suppressed.is_empty() {
return;
}
let keep: Vec<bool> = resolved
.iter()
.map(|r| {
r.original.reference_name.contains('.')
|| !suppressed.contains(&(
r.original.from_node_id.as_str(),
r.original.file_path.as_str(),
r.original.line,
r.original.column,
r.original.reference_name.as_str(),
))
})
.collect();
drop(suppressed);
let mut idx = 0;
resolved.retain(|_| {
let k = keep[idx];
idx += 1;
k
});
}
const GDSCRIPT_TYPED: &str = "gdscript-typed-receiver";
const SIMPLE_NAME_MATCH: &str = "simple-name-match";
const PATH_TAIL_MATCH: &str = "path-tail-match";
pub fn is_gdscript(path: &str) -> bool {
path.rsplit_once('.')
.is_some_and(|(_, ext)| ext.eq_ignore_ascii_case("gd"))
}
const CSHARP_TYPED: &str = "csharp-typed-receiver";
pub fn is_csharp(path: &str) -> bool {
path.rsplit_once('.')
.is_some_and(|(_, ext)| ext.eq_ignore_ascii_case("cs"))
}
pub fn has_typed_receiver_refs(path: &str) -> bool {
is_gdscript(path) || is_csharp(path)
}
fn is_typed_receiver_tag(tag: &str) -> bool {
tag == GDSCRIPT_TYPED || tag == CSHARP_TYPED
}
type CallSite<'r> = (&'r str, &'r str, u32, u32, &'r str);
fn gdscript_typed_sites(resolved: &[ResolvedRef]) -> HashSet<CallSite<'_>> {
resolved
.iter()
.filter(|r| is_typed_receiver_tag(&r.resolved_by))
.map(|r| {
(
r.original.from_node_id.as_str(),
r.original.file_path.as_str(),
r.original.line,
r.original.column,
simple_ref_name(&r.original.reference_name),
)
})
.collect()
}
fn suppress_gdscript_typed_siblings(resolved: &mut Vec<ResolvedRef>) {
let keep: Vec<bool> = {
let sites = gdscript_typed_sites(resolved);
if sites.is_empty() {
return;
}
resolved
.iter()
.map(|r| {
is_typed_receiver_tag(&r.resolved_by)
|| r.original.reference_kind != EdgeKind::Calls
|| !sites.contains(&(
r.original.from_node_id.as_str(),
r.original.file_path.as_str(),
r.original.line,
r.original.column,
simple_ref_name(&r.original.reference_name),
))
})
.collect()
};
let mut idx = 0;
resolved.retain(|_| {
let k = keep[idx];
idx += 1;
k
});
}
fn gdscript_extends(signature: &str) -> Option<&str> {
let (_, rest) = signature.split_once(" extends ")?;
let base = rest
.trim()
.split(|c: char| c.is_whitespace() || c == ':')
.next()?;
is_gdscript_ident(base).then_some(base)
}
fn gdscript_return_type(signature: &str) -> Option<&str> {
let (_, ty) = signature.rsplit_once("->")?;
let ty = ty.trim().trim_end_matches(':').trim();
(is_gdscript_ident(ty) && ty != "void").then_some(ty)
}
fn gdscript_field_type(signature: &str) -> Option<&str> {
let (_, rest) = signature.split_once("var ")?;
let rest = rest.trim_start();
let after_name = rest.trim_start_matches(|c: char| c.is_alphanumeric() || c == '_');
let after_colon = after_name.trim_start().strip_prefix(':')?;
if after_colon.starts_with('=') {
return None;
}
let after_colon = after_colon.trim_start();
let end = after_colon
.find(|c: char| !(c.is_alphanumeric() || c == '_'))
.unwrap_or(after_colon.len());
let (ty, rest) = after_colon.split_at(end);
let plain = !rest.starts_with('[') && !rest.starts_with('.');
(plain && is_gdscript_ident(ty)).then_some(ty)
}
fn is_gdscript_ident(s: &str) -> bool {
let mut chars = s.chars();
chars.next().is_some_and(|c| c.is_alphabetic() || c == '_')
&& chars.all(|c| c.is_alphanumeric() || c == '_')
}
fn is_gdscript_callable(kind: &NodeKind) -> bool {
matches!(
kind,
NodeKind::Function | NodeKind::Method | NodeKind::Constructor
)
}
fn first_indexed_candidate<'n>(
file_nodes: &HashMap<&str, &'n Node>,
importer: &str,
specifier: &str,
) -> Option<&'n Node> {
super::js_specifier::relative_module_candidates(importer, specifier)
.iter()
.find_map(|path| file_nodes.get(path.as_str()).copied())
}
fn is_csharp_callable(kind: &NodeKind) -> bool {
matches!(
kind,
NodeKind::Method | NodeKind::Function | NodeKind::Constructor
)
}
pub fn csharp_type_name(raw: &str) -> Option<&str> {
let s = raw.trim();
let s = s.strip_prefix("global::").unwrap_or(s);
let end = s
.find(|c: char| matches!(c, '<' | '[' | '(' | '?' | '*') || c.is_whitespace())
.unwrap_or(s.len());
let s = s[..end].rsplit(['.', ':']).next()?;
let mut chars = s.chars();
let ident = chars.next().is_some_and(|c| c.is_alphabetic() || c == '_')
&& chars.all(|c| c.is_alphanumeric() || c == '_');
(ident && s != "var").then_some(s)
}
fn csharp_declared_type<'s>(signature: &'s str, name: &str) -> Option<&'s str> {
let bytes = signature.as_bytes();
let mut depth = 0i32;
for i in 0..bytes.len() {
match bytes[i] {
b'(' | b'[' | b'<' | b'{' => depth += 1,
b')' | b']' | b'>' | b'}' => depth -= 1,
_ => {}
}
if depth != 0 || !bytes[i..].starts_with(name.as_bytes()) {
continue;
}
let before_ok = i == 0 || bytes[i - 1].is_ascii_whitespace();
let after_ok = bytes.get(i + name.len()).is_none_or(|c| {
matches!(c, b'(' | b'<' | b';' | b'=' | b',' | b'{') || c.is_ascii_whitespace()
});
if before_ok && after_ok {
return preceding_type_token(signature.get(..i)?);
}
}
None
}
fn preceding_type_token(s: &str) -> Option<&str> {
let s = s.trim_end();
let bytes = s.as_bytes();
let mut depth = 0i32;
let mut start = bytes.len();
for (i, &c) in bytes.iter().enumerate().rev() {
match c {
b')' | b']' | b'>' => depth += 1,
b'(' | b'[' | b'<' => depth -= 1,
_ => {}
}
if depth == 0 && c.is_ascii_whitespace() {
break;
}
start = i;
}
s.get(start..).filter(|t| !t.is_empty())
}
fn unwrap_task(ty: &str) -> Option<&str> {
if !matches!(csharp_type_name(ty), Some("Task" | "ValueTask")) {
return None;
}
let (_, inner) = ty.split_once('<')?;
Some(inner.trim_end().strip_suffix('>')?.trim())
}
fn csharp_bases(signature: &str) -> Vec<&str> {
let bytes = signature.as_bytes();
let mut depth = 0i32;
let mut colon = None;
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'(' | b'[' | b'<' | b'{' => depth += 1,
b')' | b']' | b'>' | b'}' => depth -= 1,
b':' if bytes.get(i + 1) == Some(&b':') => i += 1,
b':' if depth == 0 => {
colon = Some(i);
break;
}
b'w' if depth == 0
&& bytes[i..].starts_with(b"where")
&& i > 0
&& bytes[i - 1].is_ascii_whitespace() =>
{
return Vec::new();
}
_ => {}
}
i += 1;
}
let Some(colon) = colon else {
return Vec::new();
};
let rest = signature.get(colon + 1..).unwrap_or("");
let mut out = Vec::new();
let mut depth = 0i32;
let mut start = 0;
let rb = rest.as_bytes();
for (j, &c) in rb.iter().enumerate() {
match c {
b'(' | b'[' | b'<' => depth += 1,
b')' | b']' | b'>' => depth -= 1,
_ => {}
}
let at_where =
depth == 0 && rb[j..].starts_with(b"where") && j > 0 && rb[j - 1].is_ascii_whitespace();
if depth == 0 && (c == b',' || at_where) {
out.extend(rest.get(start..j).and_then(csharp_type_name));
start = j + 1;
if at_where {
return out;
}
}
}
out.extend(rest.get(start..).and_then(csharp_type_name));
out
}
fn lang_from_path(path: &str) -> &'static str {
match path.rsplit('.').next().unwrap_or("") {
"rs" => "rust",
"go" => "go",
"py" | "pyi" => "python",
"js" | "jsx" | "mjs" | "cjs" => "javascript",
"ts" | "tsx" | "mts" | "cts" => "typescript",
"java" => "java",
"kt" | "kts" => "kotlin",
"swift" => "swift",
"c" | "h" => "c",
"cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" | "inl" | "ipp" | "tcc" => "cpp",
"cs" => "csharp",
"rb" | "rake" => "ruby",
"php" => "php",
"scala" | "sc" => "scala",
"dart" => "dart",
"lua" => "lua",
"pl" | "pm" => "perl",
"sh" | "bash" => "bash",
"nix" => "nix",
"tf" | "tfvars" => "terraform",
"zig" => "zig",
"proto" => "proto",
"vhd" | "vhdl" => "vhdl",
"v" | "vh" | "sv" | "svh" => "systemverilog",
_ => "unknown",
}
}
fn same_language_family(a: &str, b: &str) -> bool {
a == b || matches!((a, b), ("c", "cpp") | ("cpp", "c"))
}
fn is_c_family(lang: &str) -> bool {
matches!(lang, "c" | "cpp")
}
fn is_header_path(path: &str) -> bool {
matches!(
path.rsplit('.').next().unwrap_or(""),
"h" | "hpp" | "hxx" | "hh" | "inl" | "ipp" | "tcc"
)
}
fn path_proximity(a: &str, b: &str) -> i64 {
let seg_a: Vec<&str> = a.split('/').collect();
let seg_b: Vec<&str> = b.split('/').collect();
let shared = seg_a
.iter()
.zip(seg_b.iter())
.take_while(|(x, y)| x == y)
.count();
(shared as i64 * 5).min(40)
}
fn go_file_in_package(file_path: &str, import_path: &str) -> bool {
let Some((dir, _)) = file_path.rsplit_once('/') else {
return !import_path.contains('/');
};
let dir_segs: Vec<&str> = dir.split('/').filter(|s| !s.is_empty()).collect();
let imp_segs: Vec<&str> = import_path.split('/').filter(|s| !s.is_empty()).collect();
if dir_segs.is_empty() || dir_segs.len() > imp_segs.len() {
return false;
}
dir_segs
.iter()
.rev()
.zip(imp_segs.iter().rev())
.all(|(d, i)| d == i)
}
pub struct ReferenceResolver<'a> {
#[allow(dead_code)]
db: &'a Database,
name_cache: HashMap<&'a str, Vec<&'a Node>>,
qualified_name_cache: HashMap<&'a str, Vec<&'a Node>>,
node_id_cache: HashMap<&'a str, &'a Node>,
ruby_constant_bindings: HashMap<&'a str, Vec<&'a Node>>,
suffix_cache: HashMap<&'a str, Vec<&'a str>>,
known_names: HashSet<&'a str>,
import_index: HashMap<String, HashSet<String>>,
go_import_qualifiers: HashMap<String, HashMap<String, String>>,
file_nodes: HashMap<&'a str, &'a Node>,
}
type IndexedRefs<'r> = Vec<(usize, &'r UnresolvedRef)>;
impl<'a> ReferenceResolver<'a> {
pub fn from_nodes(db: &'a Database, all_nodes: &'a [Node]) -> Self {
let mut name_cache: HashMap<&'a str, Vec<&'a Node>> = HashMap::new();
let mut qualified_name_cache: HashMap<&'a str, Vec<&'a Node>> = HashMap::new();
let mut node_id_cache: HashMap<&'a str, &'a Node> = HashMap::new();
let mut ruby_constant_bindings: HashMap<&'a str, Vec<&'a Node>> = HashMap::new();
let mut suffix_cache: HashMap<&'a str, Vec<&'a str>> = HashMap::new();
let mut file_nodes: HashMap<&'a str, &'a Node> = HashMap::new();
for node in all_nodes {
node_id_cache.insert(node.id.as_str(), node);
if node.kind == NodeKind::File {
file_nodes.insert(node.file_path.as_str(), node);
}
if node.kind == NodeKind::Use {
continue;
}
name_cache.entry(node.name.as_str()).or_default().push(node);
let qn = node.qualified_name.as_str();
qualified_name_cache.entry(qn).or_default().push(node);
let mut pos = 0;
while let Some(idx) = qn[pos..].find("::") {
let suffix = &qn[pos + idx + 2..];
if !suffix.is_empty() {
suffix_cache.entry(suffix).or_default().push(qn);
}
pos += idx + 2;
}
if lang_from_path(&node.file_path) == "ruby"
&& matches!(
node.kind,
NodeKind::Class | NodeKind::Module | NodeKind::Const
)
{
let constant_name = ruby_constant_name(node);
ruby_constant_bindings
.entry(constant_name)
.or_default()
.push(node);
}
}
for entries in suffix_cache.values_mut() {
entries.sort_unstable();
entries.dedup();
}
let mut known_names: HashSet<&'a str> = HashSet::new();
known_names.extend(name_cache.keys().copied());
known_names.extend(qualified_name_cache.keys().copied());
known_names.extend(suffix_cache.keys().copied());
let mut import_index: HashMap<String, HashSet<String>> = HashMap::new();
for node in all_nodes {
if node.kind == NodeKind::Use {
let imported = node.name.rsplit("::").next().unwrap_or(&node.name);
if imported != "*" {
import_index
.entry(node.file_path.clone())
.or_default()
.insert(imported.to_string());
}
if let Some(target) =
first_indexed_candidate(&file_nodes, &node.file_path, &node.name)
{
import_index
.entry(node.file_path.clone())
.or_default()
.insert(target.file_path.clone());
}
}
}
let mut go_import_qualifiers: HashMap<String, HashMap<String, String>> = HashMap::new();
for node in all_nodes {
if node.kind != NodeKind::Use || lang_from_path(&node.file_path) != "go" {
continue;
}
let path = node
.name
.split_once(" as ")
.map_or(node.name.as_str(), |(p, _)| p)
.trim();
let Some(qualifier) = crate::go_import::import_identifier(&node.name) else {
continue;
};
if qualifier == "_" || qualifier == "." {
continue;
}
go_import_qualifiers
.entry(node.file_path.clone())
.or_default()
.insert(qualifier, path.to_string());
}
Self {
db,
name_cache,
qualified_name_cache,
node_id_cache,
ruby_constant_bindings,
suffix_cache,
known_names,
import_index,
go_import_qualifiers,
file_nodes,
}
}
pub fn resolve_one(&self, uref: &UnresolvedRef) -> Option<ResolvedRef> {
if uref.reference_kind == EdgeKind::Uses {
let name = &uref.reference_name;
if name.starts_with("std::")
|| name.starts_with("core::")
|| name.starts_with("alloc::")
|| name.starts_with("serde")
|| name.starts_with("tokio::")
|| name.starts_with("rayon::")
|| name.starts_with("clap::")
|| name.starts_with("glob::")
|| name.starts_with("libsql::")
|| name.starts_with("sha2::")
|| name.starts_with("tree_sitter::")
|| name.starts_with("serde_json::")
|| name.starts_with("toml::")
|| name.starts_with("tempfile::")
|| name.starts_with("dirs::")
|| name.starts_with("bincode::")
|| name.contains("::*")
{
return None;
}
}
if uref.reference_kind == EdgeKind::Uses
&& super::js_specifier::is_js_family_importer(&uref.file_path)
&& super::js_specifier::is_relative_specifier(&uref.reference_name)
{
return first_indexed_candidate(
&self.file_nodes,
&uref.file_path,
&uref.reference_name,
)
.map(|target| ResolvedRef {
original: uref.clone(),
target_node_id: target.id.clone(),
confidence: 0.95,
resolved_by: ResolvedBy::RelativeImport.as_str().to_string(),
});
}
if uref.reference_kind == EdgeKind::Calls
&& is_gdscript(&uref.file_path)
&& uref.reference_name.contains("::")
{
return self.try_gdscript_typed_match(uref);
}
if uref.reference_kind == EdgeKind::Calls
&& is_csharp(&uref.file_path)
&& uref.reference_name.contains("::")
{
return self.try_csharp_typed_match(uref);
}
if uref.reference_kind == EdgeKind::Calls
&& lang_from_path(&uref.file_path) == "ruby"
&& (uref.reference_name.contains('.') || uref.reference_name.contains("::"))
{
return self.try_ruby_receiver_match(uref);
}
if uref.reference_kind == EdgeKind::Uses
&& lang_from_path(&uref.file_path) == "terraform"
&& uref.reference_name.contains('.')
{
return self.try_exact_name_match(uref);
}
if uref.reference_name.contains("::") {
if let Some(resolved) = self.try_qualified_match(uref) {
return Some(resolved);
}
let simple_name = uref
.reference_name
.rsplit("::")
.next()
.unwrap_or(&uref.reference_name);
if let Some(resolved) =
self.try_exact_name_match_simple(uref, simple_name, false, PATH_TAIL_MATCH)
{
return Some(resolved);
}
return None;
}
if uref.reference_name.contains('.') {
if let Some(resolved) = self.try_go_selector_match(uref) {
return Some(resolved);
}
let simple_name = uref
.reference_name
.rsplit('.')
.next()
.unwrap_or(&uref.reference_name);
if simple_name != uref.reference_name
&& uref.reference_kind == EdgeKind::Calls
&& is_csharp(&uref.file_path)
{
return self.try_csharp_receiver_fallback(uref, simple_name);
}
if simple_name != uref.reference_name {
if let Some(resolved) =
self.try_exact_name_match_simple(uref, simple_name, true, SIMPLE_NAME_MATCH)
{
return Some(resolved);
}
}
return None;
}
self.try_exact_name_match(uref)
}
fn is_known_name(&self, name: &str) -> bool {
self.known_names.contains(name)
}
fn is_relative_import(&self, uref: &UnresolvedRef) -> bool {
uref.reference_kind == EdgeKind::Uses
&& first_indexed_candidate(&self.file_nodes, &uref.file_path, &uref.reference_name)
.is_some()
}
pub fn known_names(&self) -> &HashSet<&'a str> {
&self.known_names
}
pub fn resolve_all(&self, refs: &[UnresolvedRef]) -> ResolutionResult {
let total = refs.len();
let (mut resolved, mut ambiguous, unresolved) = self.resolve_batch_inner(refs);
self.finalize_ambiguous(&resolved, &mut ambiguous);
self.finalize_resolved(&mut resolved);
let resolved_count = resolved.len();
ResolutionResult {
resolved,
unresolved,
total,
resolved_count,
ambiguous,
}
}
pub fn resolve_batch(&self, refs: &[UnresolvedRef]) -> (Vec<ResolvedRef>, Vec<AmbiguousCall>) {
let (resolved, ambiguous, _) = self.resolve_batch_inner(refs);
(resolved, ambiguous)
}
pub fn finalize_resolved(&self, resolved: &mut Vec<ResolvedRef>) {
suppress_go_selector_bare_siblings(resolved);
suppress_gdscript_typed_siblings(resolved);
}
pub fn finalize_ambiguous(&self, resolved: &[ResolvedRef], ambiguous: &mut Vec<AmbiguousCall>) {
let sites = gdscript_typed_sites(resolved);
if sites.is_empty() {
return;
}
ambiguous.retain(|a| {
!sites.contains(&(
a.from_node_id.as_str(),
a.file_path.as_str(),
a.line,
a.column,
simple_ref_name(&a.reference_name),
))
});
}
fn resolve_batch_inner(
&self,
refs: &[UnresolvedRef],
) -> (Vec<ResolvedRef>, Vec<AmbiguousCall>, Vec<u32>) {
let (candidates, hopeless): (IndexedRefs<'_>, IndexedRefs<'_>) =
refs.iter().enumerate().partition(|(_, uref)| {
self.is_known_name(&uref.reference_name)
|| self.is_known_name(simple_ref_name(&uref.reference_name))
|| self.is_relative_import(uref)
});
let results: Vec<_> = candidates
.par_iter()
.map(|(i, uref)| (*i, *uref, self.resolve_one(uref)))
.collect();
let mut resolved = Vec::new();
let mut failed: IndexedRefs<'_> = hopeless;
for (i, uref, res) in results {
match res {
Some(r) if r.confidence >= 0.6 => resolved.push(r),
Some(_) | None => failed.push((i, uref)), }
}
let ambiguous: Vec<AmbiguousCall> = failed
.iter()
.filter_map(|(_, uref)| self.explain_ambiguity(uref))
.collect();
let mut unresolved: Vec<u32> = failed
.iter()
.map(|(i, _)| u32::try_from(*i).unwrap_or(u32::MAX))
.collect();
unresolved.sort_unstable();
(resolved, ambiguous, unresolved)
}
pub fn create_edges(&self, resolved: &[ResolvedRef]) -> Vec<Edge> {
let mut edges: Vec<Edge> = resolved
.iter()
.map(|r| Edge {
source: r.original.from_node_id.clone(),
target: r.target_node_id.clone(),
kind: self.edge_kind_for(r),
line: Some(r.original.line),
resolved_by: ResolvedBy::from_name(&r.resolved_by),
})
.collect();
edges.sort_unstable_by(|a, b| {
(
&a.source,
&a.target,
a.kind.as_str(),
&a.line,
a.provenance_key(),
)
.cmp(&(
&b.source,
&b.target,
b.kind.as_str(),
&b.line,
b.provenance_key(),
))
});
edges.dedup_by(|a, b| {
a.source == b.source && a.target == b.target && a.kind == b.kind && a.line == b.line
});
edges
}
fn edge_kind_for(&self, r: &ResolvedRef) -> EdgeKind {
let kind = r.original.reference_kind;
if kind == EdgeKind::Extends
&& lang_from_path(&r.original.file_path) == "csharp"
&& self
.node_id_cache
.get(r.target_node_id.as_str())
.is_some_and(|n| n.kind == NodeKind::Interface)
{
return EdgeKind::Implements;
}
kind
}
fn try_qualified_match(&self, uref: &UnresolvedRef) -> Option<ResolvedRef> {
if let Some(candidates) = self.qualified_name_cache.get(uref.reference_name.as_str()) {
if let Some(node) = candidates.iter().find(|n| kind_compatible(uref, &n.kind)) {
return Some(ResolvedRef {
original: uref.clone(),
target_node_id: node.id.clone(),
confidence: 0.95,
resolved_by: "qualified-match".to_string(),
});
}
}
if let Some(full_names) = self.suffix_cache.get(uref.reference_name.as_str()) {
for full_name in full_names {
if let Some(candidates) = self.qualified_name_cache.get(full_name) {
if let Some(node) = candidates.iter().find(|n| kind_compatible(uref, &n.kind)) {
return Some(ResolvedRef {
original: uref.clone(),
target_node_id: node.id.clone(),
confidence: 0.95,
resolved_by: "qualified-match".to_string(),
});
}
}
}
}
None
}
fn try_go_selector_match(&self, uref: &UnresolvedRef) -> Option<ResolvedRef> {
if lang_from_path(&uref.file_path) != "go" {
return None;
}
let (qualifier, name) = uref.reference_name.split_once('.')?;
if name.contains('.') {
return None;
}
let import_path = self
.go_import_qualifiers
.get(&uref.file_path)?
.get(qualifier)?;
let candidates = self.name_cache.get(name)?;
let mut matched: Vec<&Node> = candidates
.iter()
.copied()
.filter(|n| kind_compatible(uref, &n.kind))
.filter(|n| go_file_in_package(&n.file_path, import_path))
.collect();
if matched.len() == 1 {
return Some(ResolvedRef {
original: uref.clone(),
target_node_id: matched.remove(0).id.clone(),
confidence: 0.95,
resolved_by: "go-selector-import".to_string(),
});
}
if matched.len() > 1 {
let winners = Self::find_best_matches(uref, &matched, &self.import_index);
let [best] = winners.as_slice() else {
return None;
};
return Some(ResolvedRef {
original: uref.clone(),
target_node_id: best.id.clone(),
confidence: 0.9,
resolved_by: "go-selector-import".to_string(),
});
}
None
}
fn try_ruby_receiver_match(&self, uref: &UnresolvedRef) -> Option<ResolvedRef> {
let (receiver, method_name) = split_ruby_receiver_call(&uref.reference_name)?;
let (owners, resolved_by): (Vec<&Node>, &str) = if receiver == "self" {
let caller = self.node_id_cache.get(uref.from_node_id.as_str())?;
let owner = match caller.kind {
NodeKind::Class | NodeKind::Module => *caller,
NodeKind::SingletonMethod => caller
.parent_id
.as_deref()
.and_then(|id| self.node_id_cache.get(id))?,
_ => return None,
};
(vec![owner], "ruby-self-receiver")
} else {
let constant_path = receiver.strip_prefix("::").unwrap_or(receiver);
let owners = if receiver.starts_with("::") {
self.ruby_constant_owners_at(constant_path)?
} else {
let caller = self.node_id_cache.get(uref.from_node_id.as_str())?;
self.ruby_lexical_constant_owners(caller, constant_path)?
};
(owners, "ruby-constant-receiver")
};
let owner_ids: HashSet<&str> = owners.iter().map(|owner| owner.id.as_str()).collect();
let mut targets = self
.name_cache
.get(method_name)?
.iter()
.copied()
.filter(|node| node.kind == NodeKind::SingletonMethod)
.filter(|node| lang_from_path(&node.file_path) == "ruby")
.filter(|node| {
node.parent_id
.as_deref()
.is_some_and(|parent| owner_ids.contains(parent))
});
let target = targets.next()?;
if targets.next().is_some() {
return None;
}
Some(ResolvedRef {
original: uref.clone(),
target_node_id: target.id.clone(),
confidence: 0.95,
resolved_by: resolved_by.to_string(),
})
}
fn try_gdscript_typed_match(&self, uref: &UnresolvedRef) -> Option<ResolvedRef> {
let mut segments = uref.reference_name.split("::");
let root = segments.next()?;
let mut steps: Vec<&str> = segments.collect();
let method = steps.pop()?;
let mut class = self.gdscript_class(root)?;
for step in steps {
let ty = if let Some(name) = step.strip_suffix("()") {
let callee = self.gdscript_member(class, name, is_gdscript_callable)?;
gdscript_return_type(callee.signature.as_deref()?)?
} else {
let field = self.gdscript_member(class, step, |k| *k == NodeKind::Field)?;
gdscript_field_type(field.signature.as_deref()?)?
};
class = self.gdscript_class(ty)?;
}
let target = self.gdscript_member(class, method, is_gdscript_callable)?;
Some(ResolvedRef {
original: uref.clone(),
target_node_id: target.id.clone(),
confidence: 0.95,
resolved_by: GDSCRIPT_TYPED.to_string(),
})
}
fn gdscript_class(&self, name: &str) -> Option<&'a Node> {
let candidates = self.name_cache.get(name)?;
let unique = |kind: NodeKind| {
let mut it = candidates
.iter()
.copied()
.filter(|n| n.kind == kind && is_gdscript(&n.file_path));
let first = it.next()?;
it.next().is_none().then_some(first)
};
unique(NodeKind::Class).or_else(|| unique(NodeKind::InnerClass))
}
fn gdscript_member(
&self,
class: &'a Node,
name: &str,
kind_ok: impl Fn(&NodeKind) -> bool,
) -> Option<&'a Node> {
const MAX_DEPTH: usize = 32;
let mut class = class;
for _ in 0..MAX_DEPTH {
let qn = format!("{}.{name}", class.qualified_name);
if let Some(found) = self
.qualified_name_cache
.get(qn.as_str())
.and_then(|nodes| nodes.iter().copied().find(|n| kind_ok(&n.kind)))
{
return Some(found);
}
let base = gdscript_extends(class.signature.as_deref()?)?;
class = self.gdscript_class(base)?;
}
None
}
fn try_csharp_typed_match(&self, uref: &UnresolvedRef) -> Option<ResolvedRef> {
let mut segments = uref.reference_name.split("::");
let root = segments.next()?;
let mut steps: Vec<&str> = segments.collect();
let method = steps.pop()?;
let mut types = self.csharp_types(root);
for step in steps {
let (awaited, step) = match step.strip_prefix("await ") {
Some(s) => (true, s),
None => (false, step),
};
let members = match step.strip_suffix("()") {
Some(name) => self.csharp_members(&types, name, is_csharp_callable),
None => self.csharp_members(&types, step, |k| {
matches!(k, NodeKind::Field | NodeKind::CSharpProperty)
}),
};
let mut next: Vec<&str> = members
.iter()
.filter_map(|m| {
let raw = csharp_declared_type(m.signature.as_deref()?, &m.name)?;
let raw = if awaited { unwrap_task(raw)? } else { raw };
csharp_type_name(raw)
})
.collect();
next.sort_unstable();
next.dedup();
let [ty] = next.as_slice() else {
return None;
};
types = self.csharp_types(ty);
}
let mut targets = self.csharp_members(&types, method, is_csharp_callable);
targets.sort_by(|a, b| {
(a.qualified_name.as_str(), a.start_line)
.cmp(&(b.qualified_name.as_str(), b.start_line))
});
targets.dedup_by(|a, b| a.qualified_name == b.qualified_name);
let (target_node_id, confidence) = match targets.as_slice() {
[] => return None,
[one] => (one.id.clone(), 0.95),
many => {
let winners = Self::find_best_matches(uref, many, &self.import_index);
let [best] = winners.as_slice() else {
return None;
};
(best.id.clone(), 0.9)
}
};
Some(ResolvedRef {
original: uref.clone(),
target_node_id,
confidence,
resolved_by: CSHARP_TYPED.to_string(),
})
}
fn csharp_types(&self, name: &str) -> Vec<&'a Node> {
self.name_cache
.get(name)
.map(|nodes| {
nodes
.iter()
.copied()
.filter(|n| {
matches!(
n.kind,
NodeKind::Class
| NodeKind::InnerClass
| NodeKind::Struct
| NodeKind::Interface
| NodeKind::Record
) && is_csharp(&n.file_path)
})
.collect()
})
.unwrap_or_default()
}
fn csharp_members(
&self,
types: &[&'a Node],
name: &str,
kind_ok: impl Fn(&NodeKind) -> bool,
) -> Vec<&'a Node> {
const MAX_DEPTH: usize = 16;
let mut seen: HashSet<&str> = types.iter().map(|t| t.id.as_str()).collect();
let mut frontier: Vec<&'a Node> = types.to_vec();
for _ in 0..MAX_DEPTH {
if frontier.is_empty() {
break;
}
let found: Vec<&'a Node> = frontier
.iter()
.filter_map(|t| {
self.qualified_name_cache
.get(format!("{}::{name}", t.qualified_name).as_str())
})
.flat_map(|nodes| nodes.iter().copied().filter(|n| kind_ok(&n.kind)))
.collect();
if !found.is_empty() {
return found;
}
let mut next = Vec::new();
for t in &frontier {
for base in csharp_bases(t.signature.as_deref().unwrap_or("")) {
for b in self.csharp_types(base) {
if seen.insert(b.id.as_str()) {
next.push(b);
}
}
}
}
frontier = next;
}
Vec::new()
}
fn try_csharp_receiver_fallback(
&self,
uref: &UnresolvedRef,
simple_name: &str,
) -> Option<ResolvedRef> {
let receiver = uref
.reference_name
.rsplit_once('.')
.map_or("", |(recv, _)| recv);
let own_scope = self
.node_id_cache
.get(uref.from_node_id.as_str())
.and_then(|caller| caller.qualified_name.rsplit_once("::"))
.map(|(scope, _)| scope);
let Some(own_scope) = own_scope.filter(|_| !matches!(receiver, "this" | "base")) else {
return self.try_exact_name_match_simple(uref, simple_name, true, SIMPLE_NAME_MATCH);
};
let in_own_scope = |n: &Node| {
n.qualified_name
.rsplit_once("::")
.is_some_and(|(scope, _)| scope == own_scope)
};
let compatible: Vec<&Node> = self
.name_cache
.get(simple_name)?
.iter()
.copied()
.filter(|n| kind_compatible(uref, &n.kind))
.collect();
if !compatible.iter().any(|n| in_own_scope(n)) {
return self.try_exact_name_match_simple(uref, simple_name, true, SIMPLE_NAME_MATCH);
}
if CROSS_FILE_BLOCKLIST.contains(&simple_name) {
return None;
}
let others: Vec<&Node> = compatible
.into_iter()
.filter(|n| !in_own_scope(n))
.collect();
if others.is_empty() {
return None;
}
resolve_from_filtered_named(
uref,
&others,
SIMPLE_NAME_MATCH,
&self.import_index,
true,
&self.node_id_cache,
)
}
fn ruby_constant_owners_at(&self, constant_path: &str) -> Option<Vec<&Node>> {
let bindings = self.ruby_constant_bindings.get(constant_path)?;
bindings
.iter()
.all(|node| matches!(node.kind, NodeKind::Class | NodeKind::Module))
.then(|| bindings.clone())
}
fn ruby_lexical_constant_owners(
&self,
caller: &Node,
constant_path: &str,
) -> Option<Vec<&Node>> {
let first_segment = constant_path.split("::").next()?;
let mut scope = if matches!(caller.kind, NodeKind::Class | NodeKind::Module) {
Some(caller)
} else {
caller
.parent_id
.as_deref()
.and_then(|id| self.node_id_cache.get(id).copied())
};
while let Some(node) = scope {
if matches!(node.kind, NodeKind::Class | NodeKind::Module) {
let scope_name = ruby_constant_name(node);
let desired = format!("{scope_name}::{constant_path}");
if self.ruby_constant_bindings.contains_key(desired.as_str()) {
return self.ruby_constant_owners_at(&desired);
}
let lexical_head = format!("{scope_name}::{first_segment}");
if self
.ruby_constant_bindings
.contains_key(lexical_head.as_str())
{
return None;
}
}
scope = node
.parent_id
.as_deref()
.and_then(|id| self.node_id_cache.get(id).copied());
}
self.ruby_constant_owners_at(constant_path)
}
fn try_exact_name_match(&self, uref: &UnresolvedRef) -> Option<ResolvedRef> {
if CROSS_FILE_BLOCKLIST.contains(&uref.reference_name.as_str()) {
let candidates = self.name_cache.get(uref.reference_name.as_str())?;
let same_file: Vec<&Node> = candidates
.iter()
.copied()
.filter(|n| n.file_path == uref.file_path)
.filter(|n| kind_compatible(uref, &n.kind))
.collect();
if same_file.len() == 1 {
return Some(ResolvedRef {
original: uref.clone(),
target_node_id: same_file[0].id.clone(),
confidence: 0.9,
resolved_by: "same-file-blocklist".to_string(),
});
}
return None;
}
let raw_candidates = self.name_cache.get(uref.reference_name.as_str())?;
let kind_filtered: Vec<&Node> = raw_candidates
.iter()
.copied()
.filter(|n| kind_compatible(uref, &n.kind))
.collect();
if kind_filtered.is_empty() {
return None;
}
let candidates: &[&Node] = if kind_filtered.len() == raw_candidates.len() {
raw_candidates
} else {
return resolve_from_filtered(
uref,
&kind_filtered,
&self.import_index,
&self.node_id_cache,
);
};
if candidates.len() == 1 {
let ref_lang = lang_from_path(&uref.file_path);
if bare_name_needs_evidence(ref_lang)
&& !is_plausibly_reachable(
uref,
candidates[0],
&self.import_index,
&self.node_id_cache,
)
{
return None;
}
let candidate_lang = lang_from_path(&candidates[0].file_path);
let confidence = if ref_lang != "unknown"
&& candidate_lang != "unknown"
&& !same_language_family(ref_lang, candidate_lang)
{
0.5
} else {
0.9
};
return Some(ResolvedRef {
original: uref.clone(),
target_node_id: candidates[0].id.clone(),
confidence,
resolved_by: "exact-match".to_string(),
});
}
let winners = Self::find_best_matches(uref, candidates, &self.import_index);
let [best] = winners.as_slice() else {
return None;
};
Some(ResolvedRef {
original: uref.clone(),
target_node_id: best.id.clone(),
confidence: 0.7,
resolved_by: "exact-match-scored".to_string(),
})
}
fn try_exact_name_match_simple(
&self,
uref: &UnresolvedRef,
simple_name: &str,
require_reachable: bool,
tag: &str,
) -> Option<ResolvedRef> {
if CROSS_FILE_BLOCKLIST.contains(&simple_name) {
let candidates = self.name_cache.get(simple_name)?;
let same_file: Vec<&Node> = candidates
.iter()
.copied()
.filter(|n| n.file_path == uref.file_path)
.filter(|n| kind_compatible(uref, &n.kind))
.collect();
if same_file.len() == 1 {
return Some(ResolvedRef {
original: uref.clone(),
target_node_id: same_file[0].id.clone(),
confidence: 0.9,
resolved_by: "same-file-blocklist".to_string(),
});
}
return None;
}
let raw_candidates = self.name_cache.get(simple_name)?;
let kind_filtered: Vec<&Node> = raw_candidates
.iter()
.copied()
.filter(|n| kind_compatible(uref, &n.kind))
.collect();
if kind_filtered.is_empty() {
return None;
}
let candidates: &[&Node] = if kind_filtered.len() == raw_candidates.len() {
raw_candidates
} else {
return resolve_from_filtered_named(
uref,
&kind_filtered,
tag,
&self.import_index,
require_reachable,
&self.node_id_cache,
);
};
if candidates.len() == 1 {
if require_reachable
&& !is_plausibly_reachable(
uref,
candidates[0],
&self.import_index,
&self.node_id_cache,
)
{
return None;
}
let ref_lang = lang_from_path(&uref.file_path);
let candidate_lang = lang_from_path(&candidates[0].file_path);
let confidence = if ref_lang != "unknown"
&& candidate_lang != "unknown"
&& !same_language_family(ref_lang, candidate_lang)
{
0.5
} else {
0.9
};
return Some(ResolvedRef {
original: uref.clone(),
target_node_id: candidates[0].id.clone(),
confidence,
resolved_by: tag.to_string(),
});
}
let winners = Self::find_best_matches(uref, candidates, &self.import_index);
let [best] = winners.as_slice() else {
return None;
};
Some(ResolvedRef {
original: uref.clone(),
target_node_id: best.id.clone(),
confidence: 0.7,
resolved_by: format!("{tag}-scored"),
})
}
fn score_candidate(
uref: &UnresolvedRef,
node: &Node,
import_index: &HashMap<String, HashSet<String>>,
) -> i64 {
let ref_lang = lang_from_path(&uref.file_path);
let mut score: i64 = 0;
if node.file_path == uref.file_path {
score += 100;
let distance = node.start_line.abs_diff(uref.line);
let proximity = 20_i64.saturating_sub(i64::from(distance) / 10);
score += proximity.max(0);
} else {
score += path_proximity(&uref.file_path, &node.file_path);
}
let candidate_lang = lang_from_path(&node.file_path);
if ref_lang != "unknown" && candidate_lang != "unknown" {
if same_language_family(ref_lang, candidate_lang) {
score += 50;
} else {
score -= 80;
}
}
if is_c_family(ref_lang) && is_c_family(candidate_lang) && !is_header_path(&node.file_path)
{
score += 20;
}
if node.visibility == Visibility::Pub {
score += 10;
}
if uref.reference_kind == EdgeKind::Calls
&& matches!(
node.kind,
NodeKind::Function
| NodeKind::Method
| NodeKind::SingletonMethod
| NodeKind::StructMethod
| NodeKind::Constructor
| NodeKind::AbstractMethod
)
{
score += 25;
}
if let Some(imports) = import_index.get(&uref.file_path) {
if imports.contains(&node.name) {
score += 30;
}
}
score
}
fn explain_ambiguity(&self, uref: &UnresolvedRef) -> Option<AmbiguousCall> {
if uref.reference_kind != EdgeKind::Calls {
return None;
}
if has_typed_receiver_refs(&uref.file_path) && uref.reference_name.contains("::") {
return None;
}
let simple_name = simple_ref_name(&uref.reference_name);
let raw = self.name_cache.get(simple_name)?;
let candidates: Vec<&Node> = raw
.iter()
.copied()
.filter(|n| kind_compatible(uref, &n.kind))
.collect();
let winners = Self::find_best_matches(uref, &candidates, &self.import_index);
if winners.len() < 2 {
return None;
}
Some(AmbiguousCall {
from_node_id: uref.from_node_id.clone(),
reference_name: uref.reference_name.clone(),
file_path: uref.file_path.clone(),
line: uref.line,
column: uref.column,
candidate_node_ids: winners.into_iter().map(|n| n.id).collect(),
})
}
fn find_best_matches(
uref: &UnresolvedRef,
candidates: &[&Node],
import_index: &HashMap<String, HashSet<String>>,
) -> Vec<Node> {
if candidates.is_empty() {
return Vec::new();
}
let scored: Vec<(i64, &&Node)> = candidates
.iter()
.map(|node| (Self::score_candidate(uref, node, import_index), node))
.collect();
let Some(best_score) = scored.iter().map(|(score, _)| *score).max() else {
return Vec::new();
};
let mut winners: Vec<Node> = scored
.into_iter()
.filter(|(score, _)| *score == best_score)
.map(|(_, node)| (*node).clone())
.collect();
winners.sort_by(|a, b| a.id.cmp(&b.id));
winners
}
}
fn kind_compatible(uref: &UnresolvedRef, target_kind: &NodeKind) -> bool {
match uref.reference_kind {
EdgeKind::Implements if lang_from_path(&uref.file_path) == "ruby" => {
matches!(target_kind, NodeKind::Module)
}
EdgeKind::Implements if lang_from_path(&uref.file_path) == "vhdl" => {
matches!(target_kind, NodeKind::Module)
}
EdgeKind::Uses if lang_from_path(&uref.file_path) == "vhdl" => {
matches!(target_kind, NodeKind::Package)
}
EdgeKind::Implements | EdgeKind::Extends | EdgeKind::DerivesMacro => {
matches!(
target_kind,
NodeKind::Trait
| NodeKind::Interface
| NodeKind::InterfaceType
| NodeKind::Class
| NodeKind::InnerClass
| NodeKind::AbstractMethod
| NodeKind::SealedClass
| NodeKind::Annotation
| NodeKind::TypeAlias
)
}
EdgeKind::Instantiates => matches!(
target_kind,
NodeKind::Module | NodeKind::Interface | NodeKind::InterfaceType
),
EdgeKind::Calls => matches!(
target_kind,
NodeKind::Function
| NodeKind::Method
| NodeKind::SingletonMethod
| NodeKind::StructMethod
| NodeKind::Constructor
| NodeKind::AbstractMethod
| NodeKind::ArrowFunction
| NodeKind::Procedure
| NodeKind::Macro
),
EdgeKind::Annotates => false,
_ => true,
}
}
fn resolve_from_filtered<'a>(
uref: &UnresolvedRef,
kind_filtered: &[&Node],
import_index: &HashMap<String, HashSet<String>>,
node_by_id: &HashMap<&'a str, &'a Node>,
) -> Option<ResolvedRef> {
resolve_from_filtered_named(
uref,
kind_filtered,
"exact-match",
import_index,
false,
node_by_id,
)
}
fn bare_name_needs_evidence(lang: &str) -> bool {
matches!(lang, "python" | "javascript" | "typescript")
}
fn is_plausibly_reachable(
uref: &UnresolvedRef,
candidate: &Node,
import_index: &HashMap<String, HashSet<String>>,
node_by_id: &HashMap<&str, &Node>,
) -> bool {
if candidate.file_path == uref.file_path {
return true;
}
let dir_of = |path: &str| path.rfind('/').map(|i| path[..i].to_string());
if dir_of(&candidate.file_path) == dir_of(&uref.file_path) {
return true;
}
let Some(imports) = import_index.get(&uref.file_path) else {
return false;
};
if imports.contains(&candidate.file_path) {
return true;
}
let imported = |name: &str| {
imports
.iter()
.any(|entry| entry == name || entry.rsplit('.').next() == Some(name))
};
if imported(&candidate.name) {
return true;
}
if let Some(parent) = candidate
.parent_id
.as_deref()
.and_then(|id| node_by_id.get(id))
{
if imported(&parent.name) {
return true;
}
}
let module = candidate
.file_path
.rsplit('/')
.next()
.and_then(|file| file.split('.').next());
module.is_some_and(imported)
}
fn resolve_from_filtered_named(
uref: &UnresolvedRef,
kind_filtered: &[&Node],
resolved_by: &str,
import_index: &HashMap<String, HashSet<String>>,
require_reachable: bool,
node_by_id: &HashMap<&str, &Node>,
) -> Option<ResolvedRef> {
if kind_filtered.len() == 1 {
if require_reachable
&& !is_plausibly_reachable(uref, kind_filtered[0], import_index, node_by_id)
{
return None;
}
return Some(ResolvedRef {
original: uref.clone(),
target_node_id: kind_filtered[0].id.clone(),
confidence: 0.85,
resolved_by: resolved_by.to_string(),
});
}
let winners = ReferenceResolver::find_best_matches(uref, kind_filtered, import_index);
let [best] = winners.as_slice() else {
return None;
};
Some(ResolvedRef {
original: uref.clone(),
target_node_id: best.id.clone(),
confidence: 0.65,
resolved_by: format!("{resolved_by}-scored"),
})
}