use std::collections::HashMap;
use sinter_core::{
Edge, Embed, Evidence, LocalBinding, Node, NodeId, Reference, Relation, SymbolKind, TraitImpl,
};
use sinter_extract::{LanguageSpec, ModuleRoot, spec_for_path};
pub struct Binding {
pub edge: Edge,
pub reference: usize,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct ResolutionStats {
pub scope: usize,
pub import: usize,
pub scip: usize,
pub unresolved_internal: usize,
pub unresolved_external: usize,
pub scip_agree: usize,
pub scip_disagree: usize,
pub scip_external: usize,
}
impl ResolutionStats {
pub fn resolved(&self) -> usize {
self.scope + self.import + self.scip + self.scip_external
}
pub fn unresolved(&self) -> usize {
self.unresolved_internal + self.unresolved_external
}
pub fn unresolved_rate(&self) -> f64 {
let total = self.resolved() + self.unresolved();
if total == 0 {
0.0
} else {
self.unresolved() as f64 / total as f64
}
}
pub fn internal_unresolved_rate(&self) -> f64 {
let total = self.scope + self.import + self.scip + self.unresolved_internal;
if total == 0 {
0.0
} else {
self.unresolved_internal as f64 / total as f64
}
}
}
enum Res {
Bound(Binding),
Internal,
External,
}
pub fn qualified_of(id: &str) -> &str {
match id.split_once('#') {
Some((_, rest)) => rest.rsplit_once('@').map_or(rest, |(q, _)| q),
None => id,
}
}
fn is_type_kind(kind: SymbolKind) -> bool {
matches!(
kind,
SymbolKind::Struct
| SymbolKind::Enum
| SymbolKind::Interface
| SymbolKind::Trait
| SymbolKind::TypeAlias
)
}
fn is_member_scope(kind: SymbolKind) -> bool {
is_type_kind(kind) || kind == SymbolKind::Class
}
fn is_callable(kind: SymbolKind) -> bool {
matches!(
kind,
SymbolKind::Function | SymbolKind::Method | SymbolKind::Macro | SymbolKind::Class
)
}
struct ModuleFiles<'a> {
key: Vec<String>,
files: Vec<&'a str>,
}
struct LocalRange<'a> {
start: u64,
scope_end: u64,
type_name: Option<&'a str>,
}
struct Import {
segments: Vec<String>,
binding: String,
glob: bool,
}
struct FileDef<'a> {
node: &'a Node,
prefix: String,
functionish: bool,
}
struct Index<'a> {
by_file_name: HashMap<(&'a str, &'a str), Vec<FileDef<'a>>>,
by_file_qualified: HashMap<(&'a str, &'a str), &'a Node>,
file_nodes: HashMap<&'a str, &'a Node>,
defs_by_file: HashMap<&'a str, Vec<&'a Node>>,
by_name: HashMap<&'a str, Vec<(Vec<String>, &'a Node)>>,
by_module_tail: HashMap<String, Vec<(Vec<String>, &'a Node)>>,
files_of_module: HashMap<String, Vec<ModuleFiles<'a>>>,
module_defs: HashMap<Vec<String>, HashMap<&'a str, Vec<&'a Node>>>,
imports: HashMap<&'a str, Vec<Import>>,
locals: HashMap<(&'a str, &'a str), Vec<LocalRange<'a>>>,
embeds: HashMap<&'a str, Vec<&'a str>>,
roots: Vec<ModuleRoot>,
}
fn key_of(spec: &LanguageSpec, roots: &[ModuleRoot], file: &str) -> Vec<String> {
let Some((manifest, root)) = spec.manifest.zip(root_of(spec, roots, file)) else {
return (spec.module_path)(file);
};
let rel = if root.dir.is_empty() {
file
} else {
&file[root.dir.len() + 1..]
};
let mut key = (spec.module_path)(rel);
match key.first() {
Some(head) if manifest.self_names.contains(&head.as_str()) => {
key[0] = root.name.clone();
}
_ => key.insert(0, root.name.clone()),
}
key
}
fn root_of<'r>(spec: &LanguageSpec, roots: &'r [ModuleRoot], file: &str) -> Option<&'r ModuleRoot> {
roots
.iter()
.filter(|r| r.language == spec.name)
.filter(|r| r.dir.is_empty() || file.starts_with(&format!("{}/", r.dir)))
.max_by_key(|r| r.dir.len())
}
fn expand(
spec: &LanguageSpec,
roots: &[ModuleRoot],
file: &str,
mut segments: Vec<String>,
) -> Vec<String> {
if let Some(manifest) = spec.manifest
&& let Some(head) = segments.first()
&& manifest.self_names.contains(&head.as_str())
&& let Some(root) = root_of(spec, roots, file)
{
segments[0] = root.name.clone();
}
segments
}
fn module_of(node: &Node, roots: &[ModuleRoot]) -> Vec<String> {
let mut module = spec_for_path(&node.file)
.map(|s| key_of(s, roots, &node.file))
.unwrap_or_default();
let qualified = qualified_of(node.id.as_str());
if let Some((prefix, _)) = qualified.rsplit_once("::") {
module.extend(prefix.split("::").map(str::to_string));
}
module
}
fn build_index<'a>(
nodes: &'a [Node],
all_imports: &'a [Reference],
locals: &'a [LocalBinding],
embeds: &'a [Embed],
roots: &[ModuleRoot],
) -> Index<'a> {
let mut index = Index {
by_file_name: HashMap::new(),
by_file_qualified: HashMap::new(),
file_nodes: HashMap::new(),
defs_by_file: HashMap::new(),
by_name: HashMap::new(),
by_module_tail: HashMap::new(),
files_of_module: HashMap::new(),
module_defs: HashMap::new(),
imports: HashMap::new(),
locals: HashMap::new(),
embeds: HashMap::new(),
roots: roots.to_vec(),
};
let mut kind_of: HashMap<(&str, &str), SymbolKind> = HashMap::new();
for node in nodes {
kind_of.insert(
(node.file.as_str(), qualified_of(node.id.as_str())),
node.kind,
);
}
for node in nodes {
let Some(spec) = spec_for_path(&node.file) else {
continue;
};
let file_module = key_of(spec, roots, &node.file);
if node.kind == SymbolKind::File {
index.file_nodes.insert(node.file.as_str(), node);
if let Some(tail) = file_module.last() {
index
.by_module_tail
.entry(tail.clone())
.or_default()
.push((file_module.clone(), node));
}
if let Some(tail) = file_module.last() {
let entries = index.files_of_module.entry(tail.clone()).or_default();
match entries.iter_mut().find(|m| m.key == file_module) {
Some(m) => m.files.push(&node.file),
None => entries.push(ModuleFiles {
key: file_module.clone(),
files: vec![&node.file],
}),
}
}
continue;
}
let qualified = qualified_of(node.id.as_str());
let prefix = qualified.rsplit_once("::").map_or("", |(p, _)| p);
let functionish =
prefix
.split("::")
.filter(|s| !s.is_empty())
.try_fold(String::new(), |acc, seg| {
let q = if acc.is_empty() {
seg.to_string()
} else {
format!("{acc}::{seg}")
};
let kind = kind_of.get(&(node.file.as_str(), q.as_str()));
match kind {
Some(k) if is_callable(*k) && *k != SymbolKind::Class => Some(q),
None => None, Some(_) => None,
}
});
index
.by_file_qualified
.insert((node.file.as_str(), qualified), node);
index
.defs_by_file
.entry(node.file.as_str())
.or_default()
.push(node);
index
.by_file_name
.entry((node.file.as_str(), node.name.as_str()))
.or_default()
.push(FileDef {
node,
prefix: prefix.to_string(),
functionish: prefix.is_empty() || functionish.is_some(),
});
let mut module = file_module.clone();
if !prefix.is_empty() {
module.extend(prefix.split("::").map(str::to_string));
}
index
.by_name
.entry(node.name.as_str())
.or_default()
.push((module, node));
if prefix.is_empty() {
index
.module_defs
.entry(file_module)
.or_default()
.entry(node.name.as_str())
.or_default()
.push(node);
}
}
for r in all_imports {
let Some(spec) = spec_for_path(&r.file) else {
continue;
};
let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
let raw = strip_glob(&r.name);
let segments = expand(spec, roots, &r.file, (spec.absolutize)(raw, &r.file));
let binding = match (&r.alias, glob) {
(Some(alias), false) => alias.clone(),
_ => segments.last().cloned().unwrap_or_default(),
};
index
.imports
.entry(r.file.as_str())
.or_default()
.push(Import {
segments,
binding,
glob,
});
}
for l in locals {
index
.locals
.entry((l.file.as_str(), l.name.as_str()))
.or_default()
.push(LocalRange {
start: l.span.start,
scope_end: l.scope_end,
type_name: l.type_name.as_deref(),
});
}
for e in embeds {
index
.embeds
.entry(e.owner.as_str())
.or_default()
.push(&e.type_name);
}
index
}
fn strip_glob(name: &str) -> &str {
name.strip_suffix('*')
.map(|s| s.trim_end_matches(['.', ':', '/']))
.unwrap_or(name)
}
impl<'a> Index<'a> {
fn local_at(&self, file: &str, name: &str, at: u64) -> Option<Option<&'a str>> {
self.locals
.get(&(file, name))
.into_iter()
.flatten()
.filter(|l| l.start <= at && at < l.scope_end)
.map(|l| l.type_name)
.next_back()
}
fn type_def(&self, file: &str, module: &[String], name: &str) -> Option<&'a Node> {
let same_file: Vec<&Node> = self
.by_file_name
.get(&(file, name))
.into_iter()
.flatten()
.filter(|d| is_member_scope(d.node.kind))
.map(|d| d.node)
.collect();
if let [node] = same_file.as_slice() {
return Some(node);
}
let in_module: Vec<&Node> = self
.module_defs
.get(module)
.and_then(|m| m.get(name))
.into_iter()
.flatten()
.filter(|n| is_member_scope(n.kind))
.copied()
.collect();
match in_module.as_slice() {
[node] => Some(node),
_ => None,
}
}
fn member_of(&self, ty: &'a Node, name: &str, depth: usize) -> Option<&'a Node> {
if depth == 0 {
return None;
}
let mut module = module_of(ty, &self.roots);
module.extend(
qualified_of(ty.id.as_str())
.rsplit("::")
.next()
.map(str::to_string),
);
let direct: Vec<&Node> = self
.by_name
.get(name)
.into_iter()
.flatten()
.filter(|(m, _)| *m == module)
.map(|(_, n)| *n)
.collect();
if let [node] = direct.as_slice() {
return Some(node);
}
let in_type_file: Vec<&Node> = direct
.iter()
.filter(|n| n.file == ty.file)
.copied()
.collect();
if let [node] = in_type_file.as_slice() {
return Some(node);
}
let spec = spec_for_path(&ty.file)?;
let file_module = key_of(spec, &self.roots, &ty.file);
for embedded in self.embeds.get(ty.id.as_str()).into_iter().flatten() {
if let Some(embedded_ty) = self.type_def(&ty.file, &file_module, embedded)
&& let Some(node) = self.member_of(embedded_ty, name, depth - 1)
{
return Some(node);
}
}
None
}
fn anchored(&self, segments: &[String]) -> bool {
let module_hit = |segs: &[String]| {
segs.last().is_some_and(|tail| {
self.files_of_module
.get(tail.as_str())
.into_iter()
.flatten()
.any(|m| suffix_len(&m.key, segs).is_some())
|| self
.by_module_tail
.get(tail.as_str())
.into_iter()
.flatten()
.any(|(key, _)| suffix_len(key, segs).is_some())
})
};
if module_hit(segments) {
return true;
}
match segments.split_last() {
Some((_, module)) if !module.is_empty() => module_hit(module),
_ => false,
}
}
fn import_file(&self, segments: &[String]) -> Option<&'a Node> {
unique_best(
self.by_module_tail
.get(segments.last()?.as_str())
.into_iter()
.flatten()
.filter_map(|(key, node)| {
let len = suffix_len(key, segments).or_else(|| suffix_len(segments, key))?;
Some((len, *node))
}),
)
}
fn resolve_path(&self, segments: &[String], depth: usize) -> Option<&'a Node> {
self.resolve_path_defs(segments, depth).or_else(|| {
let files = self
.by_module_tail
.get(segments.last()?.as_str())
.into_iter()
.flatten()
.filter_map(|(key, node)| Some((suffix_len(key, segments)?, *node)));
unique_best(files)
})
}
fn resolve_path_defs(&self, segments: &[String], depth: usize) -> Option<&'a Node> {
if segments.is_empty() || depth == 0 {
return None;
}
if let Some((name, module)) = segments.split_last() {
let defs = self
.by_name
.get(name.as_str())
.into_iter()
.flatten()
.filter_map(|(key, node)| Some((suffix_len(key, module)?, *node)));
if let Some(node) = unique_best(defs) {
return Some(node);
}
if !module.is_empty() {
let mut chained: Vec<&Node> = Vec::new();
let tail = module.last().map(String::as_str).unwrap_or("");
for m in self.files_of_module.get(tail).into_iter().flatten() {
if suffix_len(&m.key, module).is_none() {
continue;
}
for file in &m.files {
for import in self.imports.get(*file).into_iter().flatten() {
if import.binding == *name && !import.glob {
chained.extend(self.resolve_path(&import.segments, depth - 1));
} else if import.glob {
let mut deeper = import.segments.clone();
deeper.push(name.clone());
chained.extend(self.resolve_path(&deeper, depth - 1));
}
}
}
}
chained.sort_by_key(|n| n.id.as_str().to_string());
chained.dedup_by_key(|n| n.id.as_str().to_string());
if let [node] = chained.as_slice() {
return Some(node);
}
}
}
None
}
}
fn namespace_pick(candidates: Vec<&Node>, relation: Relation) -> Option<&Node> {
match candidates.as_slice() {
[node] => Some(node),
[] => None,
_ => {
let preferred: Vec<&Node> = candidates
.iter()
.filter(|n| match relation {
Relation::Calls => is_callable(n.kind),
Relation::Uses => is_type_kind(n.kind),
_ => true,
})
.copied()
.collect();
match preferred.as_slice() {
[node] => Some(node),
_ => None,
}
}
}
}
pub fn resolve(
nodes: &[Node],
references: &[Reference],
locals: &[LocalBinding],
all_imports: &[Reference],
embeds: &[Embed],
roots: &[ModuleRoot],
) -> (Vec<Binding>, ResolutionStats, Vec<usize>) {
let t = std::time::Instant::now();
let index = build_index(nodes, all_imports, locals, embeds, roots);
if std::env::var_os("SINTER_TIMING").is_some() {
eprintln!("index build: {:?}", t.elapsed());
}
use rayon::prelude::*;
let results: Vec<Res> = references
.par_iter()
.enumerate()
.map(|(i, r)| {
let Some(spec) = spec_for_path(&r.file) else {
return Res::External;
};
let src = r
.enclosing
.clone()
.unwrap_or_else(|| NodeId::new(r.file.clone()));
let file_module = key_of(spec, &index.roots, &r.file);
let imports = index.imports.get(r.file.as_str());
let (target, evidence, internal) = resolve_one(&index, spec, r, &file_module, imports);
match target {
Some(node) if node.id != src => {
let relation = if r.relation == Relation::Calls && is_type_kind(node.kind) {
Relation::Uses
} else {
r.relation
};
Res::Bound(Binding {
edge: Edge {
src,
dst: node.id.clone(),
relation,
evidence,
confidence: evidence.confidence(),
},
reference: i,
})
}
_ if internal => Res::Internal,
_ => Res::External,
}
})
.collect();
let mut bindings = Vec::new();
let mut stats = ResolutionStats::default();
let mut internal_indices = Vec::new();
for (i, result) in results.into_iter().enumerate() {
match result {
Res::Bound(binding) => {
match binding.edge.evidence {
Evidence::Scope => stats.scope += 1,
_ => stats.import += 1,
}
bindings.push(binding);
}
Res::Internal => {
stats.unresolved_internal += 1;
internal_indices.push(i);
}
Res::External => stats.unresolved_external += 1,
}
}
(bindings, stats, internal_indices)
}
fn resolve_one<'a>(
index: &Index<'a>,
spec: &sinter_extract::LanguageSpec,
r: &Reference,
file_module: &[String],
imports: Option<&Vec<Import>>,
) -> (Option<&'a Node>, Evidence, bool) {
if r.relation == Relation::Imports {
let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
let raw = strip_glob(&r.name);
if let Some(node) = index
.file_nodes
.get(raw.trim().trim_matches(['<', '>', '"']))
{
return (Some(node), Evidence::Import, true);
}
let segments = expand(spec, &index.roots, &r.file, (spec.absolutize)(raw, &r.file));
let target = if glob {
index.import_file(&segments)
} else {
index.resolve_path(&segments, 4)
};
let internal = target.is_some() || index.anchored(&segments);
return (target, Evidence::Import, internal);
}
if let Some(path) = &r.path {
if spec.file_refs {
return resolve_file_ref(index, spec, r, path);
}
let segments = expand(
spec,
&index.roots,
&r.file,
(spec.absolutize)(path, &r.file),
);
let prefix = segments
.len()
.checked_sub(2)
.and_then(|p| segments.get(p))
.cloned();
let Some(prefix) = prefix else {
return (None, Evidence::Import, false);
};
if spec.receivers.contains(&prefix.as_str())
&& let Some(enclosing) = &r.enclosing
&& let Some((type_prefix, _)) = qualified_of(enclosing.as_str()).rsplit_once("::")
&& let Some(ty) = index.by_file_qualified.get(&(r.file.as_str(), type_prefix))
{
return (index.member_of(ty, &r.name, 4), Evidence::Scope, true);
}
match index.local_at(&r.file, &prefix, r.span.start) {
Some(Some(type_name)) => {
let ty = index.type_def(&r.file, file_module, type_name);
let target = ty.and_then(|ty| index.member_of(ty, &r.name, 4));
return (target, Evidence::Scope, ty.is_some());
}
Some(None) => return (None, Evidence::Scope, false), None => {}
}
if let Some(ty) = index.type_def(&r.file, file_module, &prefix)
&& let Some(node) = index.member_of(ty, &r.name, 4)
{
return (Some(node), Evidence::Scope, true);
}
if let Some(node) = index.resolve_path_defs(&segments, 4) {
return (Some(node), Evidence::Import, true);
}
if let Some((leaf, type_path)) = segments.split_last()
&& type_path.len() >= 2
&& let Some(ty) = index.resolve_path_defs(type_path, 4)
&& let Some(node) = index.member_of(ty, leaf, 4)
{
return (Some(node), Evidence::Import, true);
}
let matching: Vec<&Import> = imports
.into_iter()
.flatten()
.filter(|imp| !imp.glob && imp.binding == prefix)
.collect();
let candidates: Vec<&Node> = matching
.iter()
.filter_map(|imp| {
let mut full = imp.segments.clone();
full.push(r.name.clone());
index.resolve_path(&full, 4)
})
.collect();
let internal = candidates.len() > 1
|| index.anchored(&segments)
|| matching.iter().any(|imp| index.anchored(&imp.segments));
return match candidates.as_slice() {
[node] => (Some(node), Evidence::Import, true),
_ => (None, Evidence::Import, internal),
};
}
if index.local_at(&r.file, &r.name, r.span.start).is_some() {
return (None, Evidence::Scope, false); }
let enclosing_q = r
.enclosing
.as_ref()
.map(|e| qualified_of(e.as_str()))
.unwrap_or("");
let visible: Vec<&Node> = index
.by_file_name
.get(&(r.file.as_str(), r.name.as_str()))
.into_iter()
.flatten()
.filter(|d| {
d.prefix.is_empty()
|| (d.functionish
&& (enclosing_q == d.prefix
|| enclosing_q.starts_with(&format!("{}::", d.prefix))))
})
.map(|d| d.node)
.collect();
if !visible.is_empty() {
return (namespace_pick(visible, r.relation), Evidence::Scope, true);
}
if let Some(defs) = index
.module_defs
.get(file_module)
.and_then(|m| m.get(r.name.as_str()))
{
return (
namespace_pick(defs.clone(), r.relation),
Evidence::Scope,
true,
);
}
let named: Vec<&Node> = imports
.into_iter()
.flatten()
.filter(|imp| !imp.glob && imp.binding == r.name)
.filter_map(|imp| index.resolve_path(&imp.segments, 4))
.collect();
let (target, internal) = match named.as_slice() {
[node] => (Some(*node), true),
[] => {
let globbed: Vec<&Node> = imports
.into_iter()
.flatten()
.filter(|imp| imp.glob)
.filter_map(|imp| {
let mut full = imp.segments.clone();
full.push(r.name.clone());
index.resolve_path(&full, 4).or_else(|| {
let file = index.import_file(&imp.segments)?;
index
.by_file_name
.get(&(file.file.as_str(), r.name.as_str()))
.into_iter()
.flatten()
.find(|d| d.prefix.is_empty())
.map(|d| d.node)
})
})
.collect();
let name_imports_anchored = imports
.into_iter()
.flatten()
.filter(|imp| !imp.glob && imp.binding == r.name)
.any(|imp| index.anchored(&imp.segments));
match globbed.as_slice() {
[node] => (Some(*node), true),
[] => (None, name_imports_anchored),
_ => (None, true), }
}
_ => (None, true), };
(target, Evidence::Import, internal)
}
fn resolve_file_ref<'a>(
index: &Index<'a>,
spec: &LanguageSpec,
r: &Reference,
path: &str,
) -> (Option<&'a Node>, Evidence, bool) {
let (head, frag) = match path.split_once('#') {
Some((h, f)) => (h, Some(f)),
None => (path, None),
};
let file = if head.is_empty() {
index.file_nodes.get(r.file.as_str()).copied()
} else {
let joined = (spec.absolutize)(head, &r.file).join("/");
index.file_nodes.get(joined.as_str()).copied().or_else(|| {
spec.extensions.iter().find_map(|ext| {
index
.file_nodes
.get(format!("{joined}.{ext}").as_str())
.copied()
})
})
};
match (file, frag) {
(Some(file), None) => (Some(file), Evidence::Import, true),
(Some(file), Some(frag)) => {
let matching: Vec<&Node> = index
.defs_by_file
.get(file.file.as_str())
.into_iter()
.flatten()
.filter(|n| slugify(&n.name) == frag)
.copied()
.collect();
match matching.as_slice() {
[node] => (Some(node), Evidence::Import, true),
_ => (None, Evidence::Import, true),
}
}
(None, _) => (None, Evidence::Import, false),
}
}
fn slugify(name: &str) -> String {
name.chars()
.filter_map(|c| match c {
' ' => Some('-'),
'-' | '_' => Some(c),
c if c.is_alphanumeric() => Some(c.to_ascii_lowercase()),
_ => None,
})
.collect()
}
pub fn dynamic_edges(
nodes: &[Node],
trait_impls: &[TraitImpl],
all_imports: &[Reference],
roots: &[ModuleRoot],
) -> Vec<Edge> {
if trait_impls.is_empty() {
return Vec::new();
}
let index = build_index(nodes, all_imports, &[], &[], roots);
let mut by_file: HashMap<&str, Vec<&Node>> = HashMap::new();
for n in nodes {
if is_callable(n.kind) {
by_file.entry(n.file.as_str()).or_default().push(n);
}
}
let is_trait = |n: &Node| {
matches!(
n.kind,
SymbolKind::Trait | SymbolKind::Interface | SymbolKind::Class
)
};
let mut edges = Vec::new();
for ti in trait_impls {
let Some(spec) = spec_for_path(&ti.file) else {
continue;
};
let file_module = key_of(spec, roots, &ti.file);
let trait_node = index
.type_def(&ti.file, &file_module, &ti.trait_name)
.filter(|n| is_trait(n))
.or_else(|| {
let named: Vec<&Node> = index
.imports
.get(ti.file.as_str())
.into_iter()
.flatten()
.filter(|imp| !imp.glob && imp.binding == ti.trait_name)
.filter_map(|imp| index.resolve_path_defs(&imp.segments, 4))
.filter(|n| is_trait(n))
.collect();
match named.as_slice() {
[node] => Some(node),
_ => None,
}
});
let Some(trait_node) = trait_node else {
continue; };
for method in by_file.get(ti.file.as_str()).into_iter().flatten() {
if !(ti.span.start <= method.span.start && method.span.end <= ti.span.end) {
continue;
}
if let Some(trait_method) = index.member_of(trait_node, &method.name, 1)
&& trait_method.id != method.id
{
edges.push(Edge {
src: trait_method.id.clone(),
dst: method.id.clone(),
relation: Relation::Calls,
evidence: Evidence::Dynamic,
confidence: Evidence::Dynamic.confidence(),
});
}
}
}
edges.sort();
edges.dedup();
edges
}
pub fn resolve_boundary(
foreign_nodes: &[Node],
references: &[Reference],
owner_imports: &[Reference],
) -> Vec<Binding> {
let index = build_index(foreign_nodes, owner_imports, &[], &[], &[]);
let mut bindings = Vec::new();
for (i, r) in references.iter().enumerate() {
let Some(spec) = spec_for_path(&r.file) else {
continue;
};
let src = r
.enclosing
.clone()
.unwrap_or_else(|| NodeId::new(r.file.clone()));
let imports = index.imports.get(r.file.as_str());
let target = if r.relation == Relation::Imports {
let glob = matches!(r.alias.as_deref(), Some("*") | Some("."));
let segments = (spec.absolutize)(strip_glob(&r.name), &r.file);
if glob {
index.import_file(&segments)
} else {
index.resolve_path(&segments, 4)
}
} else if let Some(path) = &r.path {
let segments = (spec.absolutize)(path, &r.file);
let direct = index.resolve_path(&segments, 4);
direct.or_else(|| {
let prefix = segments
.len()
.checked_sub(2)
.and_then(|p| segments.get(p))?;
let candidates: Vec<&Node> = imports
.into_iter()
.flatten()
.filter(|imp| !imp.glob && imp.binding == *prefix)
.filter_map(|imp| {
let mut full = imp.segments.clone();
full.push(r.name.clone());
index.resolve_path(&full, 4)
})
.collect();
match candidates.as_slice() {
[node] => Some(node),
_ => None,
}
})
} else {
let named: Vec<&Node> = imports
.into_iter()
.flatten()
.filter(|imp| !imp.glob && imp.binding == r.name)
.filter_map(|imp| index.resolve_path(&imp.segments, 4))
.collect();
match named.as_slice() {
[node] => Some(*node),
_ => None,
}
};
if let Some(node) = target
&& node.id != src
{
let relation = if r.relation == Relation::Calls && is_type_kind(node.kind) {
Relation::Uses
} else {
r.relation
};
bindings.push(Binding {
edge: Edge {
src,
dst: node.id.clone(),
relation,
evidence: Evidence::Import,
confidence: Evidence::Import.confidence(),
},
reference: i,
});
}
}
bindings
}
fn suffix_len(key: &[String], path: &[String]) -> Option<usize> {
(!key.is_empty() && path.len() >= key.len() && path[path.len() - key.len()..] == key[..])
.then_some(key.len())
}
fn unique_best<'a>(candidates: impl Iterator<Item = (usize, &'a Node)>) -> Option<&'a Node> {
let mut best: Option<(usize, Vec<&Node>)> = None;
for (len, node) in candidates {
match &mut best {
Some((best_len, nodes)) if len == *best_len => nodes.push(node),
Some((best_len, nodes)) if len > *best_len => {
*best_len = len;
nodes.clear();
nodes.push(node);
}
None => best = Some((len, vec![node])),
_ => {}
}
}
match best {
Some((_, nodes)) if nodes.len() == 1 => Some(nodes[0]),
_ => None,
}
}