use super::imports::ImportScopes;
use crate::Result;
use crate::language::{Language, ReferenceFact};
use std::collections::{BTreeSet, HashMap};
use weavatrix_graph::{Confidence, Edge, EdgeKind, EvidenceKind, GraphBuilder, NodeId, Provenance};
pub(super) struct PendingReference {
pub source: NodeId,
pub source_path: String,
pub language: Language,
pub extractor: &'static str,
pub reference: ReferenceFact,
}
pub(super) fn resolve(
graph: &mut GraphBuilder,
symbols: &HashMap<Language, HashMap<String, Vec<NodeId>>>,
per_file: &HashMap<String, HashMap<String, Vec<NodeId>>>,
visible_imports: &ImportScopes,
references: Vec<PendingReference>,
) -> Result<()> {
for item in references {
let name = item.reference.name.as_str();
let Some(resolution) = resolve_name(&item, name, symbols, per_file, visible_imports) else {
continue;
};
let provenance = Provenance::new(item.extractor, EvidenceKind::Resolved, Confidence::High)?
.with_span(item.reference.span)
.with_detail(resolution.detail);
graph.add_edge(Edge::new(
item.source,
resolution.target,
item.reference.kind,
provenance,
))?;
}
Ok(())
}
struct Resolution {
target: NodeId,
detail: &'static str,
}
fn resolve_name(
item: &PendingReference,
name: &str,
symbols: &HashMap<Language, HashMap<String, Vec<NodeId>>>,
per_file: &HashMap<String, HashMap<String, Vec<NodeId>>>,
visible_imports: &ImportScopes,
) -> Option<Resolution> {
if item.reference.kind == EdgeKind::Calls
&& item.reference.qualified
&& matches!(item.language, Language::JavaScript | Language::TypeScript)
{
return None;
}
if let Some(target) = unique_in_file(
&item.source_path,
name,
per_file,
item.reference.kind == EdgeKind::Calls,
) && (target != item.source || item.reference.kind == EdgeKind::Calls)
{
return Some(Resolution {
target,
detail: "resolved in the referencing file's own scope",
});
}
let bindings = visible_imports.bindings(&item.source_path, name);
let direct_bindings = binding_targets(
bindings,
false,
item,
per_file,
item.reference.kind == EdgeKind::Calls,
);
if direct_bindings.len() == 1 {
return Some(Resolution {
target: direct_bindings.into_iter().next()?,
detail: "resolved through an exact imported-name binding",
});
}
if direct_bindings.is_empty() {
let forwarded_bindings = binding_targets(
bindings,
true,
item,
per_file,
item.reference.kind == EdgeKind::Calls,
);
if forwarded_bindings.len() == 1 {
return Some(Resolution {
target: forwarded_bindings.into_iter().next()?,
detail: "resolved through an exact imported-name binding",
});
}
}
let mut from_imports = visible_imports
.files(&item.source_path)
.into_iter()
.flatten()
.filter_map(|path| {
unique_in_file(path, name, per_file, item.reference.kind == EdgeKind::Calls)
})
.filter(|target| *target != item.source);
if let Some(target) = from_imports.next()
&& from_imports.next().is_none()
{
return Some(Resolution {
target,
detail: "resolved through an import of the defining module",
});
}
if item.reference.kind == EdgeKind::Calls
&& matches!(item.language, Language::JavaScript | Language::TypeScript)
{
return None;
}
let defined = symbols.get(&item.language)?.get(name)?;
let mut repository_wide = defined
.iter()
.filter(|target| **target != item.source || item.reference.kind == EdgeKind::Calls);
let only = repository_wide.next()?;
repository_wide.next().is_none().then(|| Resolution {
target: only.clone(),
detail: "unique repository symbol match",
})
}
fn binding_targets(
bindings: Option<&BTreeSet<super::imports::ImportedBinding>>,
forwarded: bool,
item: &PendingReference,
per_file: &HashMap<String, HashMap<String, Vec<NodeId>>>,
prefer_callable: bool,
) -> BTreeSet<NodeId> {
bindings
.into_iter()
.flatten()
.filter(|binding| binding.forwarded == forwarded)
.filter_map(|binding| {
unique_in_file(&binding.path, &binding.imported, per_file, prefer_callable)
})
.filter(|target| *target != item.source)
.collect()
}
fn unique_in_file(
path: &str,
name: &str,
per_file: &HashMap<String, HashMap<String, Vec<NodeId>>>,
prefer_callable: bool,
) -> Option<NodeId> {
let defined = per_file.get(path)?.get(name)?;
if defined.len() == 1 {
return Some(defined[0].clone());
}
if !prefer_callable {
return None;
}
let mut callable = defined.iter().filter(|target| {
let id = target.as_str();
["#function:", "#method:", "#class:"]
.iter()
.any(|kind| id.contains(kind))
});
let only = callable.next()?;
callable.next().is_none().then(|| only.clone())
}