use crate::calls::graph::{CallEdge, CallTarget, Confidence, Qn};
use crate::calls::pass::{file_rel, raw_to_edge, FilePass, RawEdge};
use crate::deps::manifest::detect_aliases;
use crate::deps::resolver::{build_suffix_index, resolve as resolve_spec, Lang, ResolveCtx};
use crate::deps::traverse;
use crate::deps::DepGraph;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
pub struct Resolved {
pub forward: HashMap<Qn, Vec<CallEdge>>,
pub symbol_table: HashMap<String, Vec<Qn>>,
}
pub fn build_symbol_table(passes: &[FilePass]) -> HashMap<String, Vec<Qn>> {
let mut symbol_table: HashMap<String, Vec<Qn>> = HashMap::new();
for fp in passes {
for qn in &fp.defined {
symbol_table
.entry(qn.name().to_string())
.or_default()
.push(qn.clone());
}
}
for v in symbol_table.values_mut() {
v.sort_by(|a, b| a.0.cmp(&b.0));
v.dedup();
}
symbol_table
}
pub fn run(root: &Path, deps: &DepGraph, passes: Vec<FilePass>) -> Resolved {
let symbol_table = build_symbol_table(&passes);
run_with_table(root, deps, passes, symbol_table)
}
pub fn run_with_table(
root: &Path,
deps: &DepGraph,
passes: Vec<FilePass>,
symbol_table: HashMap<String, Vec<Qn>>,
) -> Resolved {
let aliases = detect_aliases(root);
let suffix_idx = build_suffix_index(root);
let mut forward: HashMap<Qn, Vec<CallEdge>> = HashMap::new();
let mut ambiguous_buffer: Vec<(RawEdge, PathBuf, Vec<Qn>)> = Vec::new();
for fp in passes {
let file_qns: HashSet<String> = fp.defined.iter().map(|q| q.name().to_string()).collect();
let local_qn_by_name: HashMap<String, Qn> = fp
.defined
.iter()
.map(|q| (q.name().to_string(), q.clone()))
.collect();
let import_lookup: HashMap<String, String> = fp
.imports
.iter()
.map(|b| (b.local.clone(), b.module.clone()))
.collect();
let lang = Lang::from_path(&fp.file);
for raw in fp.raw_edges {
if file_qns.contains(&raw.bare_name) {
let target = local_qn_by_name
.get(&raw.bare_name)
.cloned()
.map(CallTarget::Resolved)
.unwrap_or(CallTarget::Bare(raw.bare_name.clone()));
let edge = raw_to_edge(
raw.clone(),
target,
Confidence::Exact,
rel_path(root, &fp.file),
Vec::new(),
);
forward.entry(edge.source.clone()).or_default().push(edge);
continue;
}
if let Some(spec) = import_lookup.get(&raw.bare_name) {
if let Some(target) = resolve_via_imports(
spec,
&raw.bare_name,
&fp.file,
lang,
&aliases,
&suffix_idx,
root,
&symbol_table,
) {
let edge = raw_to_edge(
raw.clone(),
CallTarget::Resolved(target),
Confidence::Exact,
rel_path(root, &fp.file),
Vec::new(),
);
forward.entry(edge.source.clone()).or_default().push(edge);
continue;
}
}
let has_receiver = raw.receiver.is_some()
&& !matches!(raw.receiver.as_deref(), Some("self") | Some("Self") | Some("crate") | Some("super"));
match symbol_table.get(&raw.bare_name) {
Some(cands) if cands.len() == 1 && !has_receiver => {
let edge = raw_to_edge(
raw.clone(),
CallTarget::Resolved(cands[0].clone()),
Confidence::Exact,
rel_path(root, &fp.file),
Vec::new(),
);
forward.entry(edge.source.clone()).or_default().push(edge);
}
Some(cands) if !cands.is_empty() => {
ambiguous_buffer.push((raw.clone(), fp.file.clone(), cands.clone()));
}
_ => {
let edge = raw_to_edge(
raw.clone(),
CallTarget::Bare(raw.bare_name.clone()),
Confidence::Ambiguous,
rel_path(root, &fp.file),
Vec::new(),
);
forward.entry(edge.source.clone()).or_default().push(edge);
}
}
}
for qn in fp.defined {
forward.entry(qn).or_default();
}
}
for (raw, src_file, cands) in ambiguous_buffer {
let closure = forward_closure_files(deps, &src_file);
let filtered: Vec<Qn> = cands
.iter()
.filter(|qn| {
let cand_file = root.join(qn.file().replace('/', std::path::MAIN_SEPARATOR_STR));
closure.contains(&cand_file)
})
.cloned()
.collect();
let (target, confidence, candidates) = if filtered.len() == 1 {
(CallTarget::Resolved(filtered[0].clone()), Confidence::Inferred, Vec::new())
} else if filtered.is_empty() {
(CallTarget::Bare(raw.bare_name.clone()), Confidence::Ambiguous, cands)
} else {
(CallTarget::Bare(raw.bare_name.clone()), Confidence::Ambiguous, filtered)
};
let edge = raw_to_edge(raw, target, confidence, rel_path(root, &src_file), candidates);
forward.entry(edge.source.clone()).or_default().push(edge);
}
Resolved {
forward,
symbol_table,
}
}
fn resolve_via_imports(
spec: &str,
bare_name: &str,
from_file: &Path,
lang: Option<Lang>,
aliases: &crate::deps::manifest::ProjectAliases,
idx: &crate::deps::resolver::SuffixIndex,
root: &Path,
symbol_table: &HashMap<String, Vec<Qn>>,
) -> Option<Qn> {
let lang = lang?;
let ctx = ResolveCtx {
from_file,
lang,
alias_prefix: aliases.go_module.as_deref(),
path_aliases: &aliases.ts_path_aliases,
php_psr4: &aliases.php_psr4,
};
let target_file = resolve_spec(spec, &ctx, idx)?;
let rel = file_rel(root, &target_file);
if let Some(cands) = symbol_table.get(bare_name) {
for cand in cands {
if cand.file() == rel {
return Some(cand.clone());
}
}
}
Some(Qn::new(format!("{}::{}", rel, bare_name)))
}
fn forward_closure_files(deps: &DepGraph, from: &Path) -> HashSet<PathBuf> {
let hits = traverse::forward(deps, from, 8);
let mut out: HashSet<PathBuf> = hits.into_iter().map(|h| h.file).collect();
out.insert(from.to_path_buf());
out
}
fn rel_path(root: &Path, file: &Path) -> PathBuf {
file.strip_prefix(root).map(|p| p.to_path_buf()).unwrap_or_else(|_| file.to_path_buf())
}