use std::cell::RefCell;
use std::ops::Deref;
use std::path::Path;
use std::rc::Rc;
use ahash::AHashMap;
use crate::index::IndexDb;
use crate::intel::model::{ExportEdge, ImportEdge, ResolvedEdge};
use crate::intel::resolver::{ProbeSet, SpecifierResolver};
use crate::intel::stage_budget::BudgetedWriter;
use crate::store::Store;
pub struct FileFacts {
pub imports: Vec<ImportEdge>,
pub exports: Vec<ExportEdge>,
pub import_uses: Vec<ResolvedEdge>,
}
const COMMIT_BATCH: usize = 256;
const MAX_REEXPORT_HOPS: usize = 8;
struct FactsCatalog<'a> {
store: &'a Store,
resident: &'a AHashMap<String, FileFacts>,
loaded: RefCell<AHashMap<String, Option<Rc<FileFacts>>>>,
export_maps: RefCell<AHashMap<String, Rc<AHashMap<String, u32>>>>,
}
enum Facts<'a> {
Resident(&'a FileFacts),
Loaded(Rc<FileFacts>),
}
impl Deref for Facts<'_> {
type Target = FileFacts;
fn deref(&self) -> &FileFacts {
match self {
Self::Resident(facts) => facts,
Self::Loaded(facts) => facts,
}
}
}
impl<'a> FactsCatalog<'a> {
fn new(store: &'a Store, resident: &'a AHashMap<String, FileFacts>) -> Self {
Self {
store,
resident,
loaded: RefCell::default(),
export_maps: RefCell::default(),
}
}
fn facts(&self, key: &str) -> Option<Facts<'a>> {
if let Some(facts) = self.resident.get(key) {
return Some(Facts::Resident(facts));
}
self.loaded
.borrow_mut()
.entry(key.to_string())
.or_insert_with(|| self.load(key).map(Rc::new))
.clone()
.map(Facts::Loaded)
}
fn load(&self, key: &str) -> Option<FileFacts> {
let entry = self.store.lookup(key)?;
let refs = self.store.read_resolved_by_hex(&entry.hash_hex).ok()??;
if refs.imports.is_empty() && refs.exports.is_empty() {
return None;
}
Some(FileFacts {
imports: refs.imports,
exports: refs.exports,
import_uses: Vec::new(),
})
}
fn export_start(&self, key: &str, name: &str) -> Option<u32> {
let cached = self.export_maps.borrow().get(key).cloned();
let map = match cached {
Some(map) => map,
None => {
let facts = self.facts(key)?;
let map: Rc<AHashMap<String, u32>> =
Rc::new(facts.exports.iter().map(|e| (e.name.clone(), e.name_start)).collect());
self.export_maps.borrow_mut().insert(key.to_string(), Rc::clone(&map));
map
}
};
map.get(name).copied()
}
}
fn resolve_export_transitively(
root: &Path,
store: &Store,
catalog: &FactsCatalog<'_>,
resolver: &SpecifierResolver,
target_rel: crate::path::RelPath,
wanted: &str,
probes: &mut ProbeSet,
) -> Option<(crate::path::RelPath, u32)> {
let mut current_rel = target_rel;
let mut wanted = wanted.to_string();
let mut visited: ahash::AHashSet<(String, String)> = ahash::AHashSet::new();
for _ in 0..MAX_REEXPORT_HOPS {
let current_key = current_rel.as_str()?.to_string();
if !visited.insert((current_key.clone(), wanted.clone())) {
return None;
}
if let Some(name_start) = catalog.export_start(¤t_key, &wanted) {
return Some((current_rel, name_start));
}
let facts = catalog.facts(¤t_key)?;
let reexport = facts
.imports
.iter()
.find(|import| !import.is_type && import.local == wanted)?;
let next_rel = resolver.resolve(root, ¤t_key, reexport, probes)?;
store.lookup(&next_rel)?;
wanted = reexport.imported.clone().unwrap_or(wanted);
current_rel = next_rel;
}
None
}
pub fn stitch_cross_file_edges(
root: &Path,
store: &Store,
index_db: &IndexDb,
importers: &AHashMap<String, FileFacts>,
) -> bool {
if importers.is_empty() {
return true;
}
let catalog = FactsCatalog::new(store, importers);
let mut resolvers: AHashMap<String, Option<SpecifierResolver>> = AHashMap::new();
let mut batch = BudgetedWriter::new(index_db, COMMIT_BATCH, "cross-file stitch");
let mut edges = 0usize;
let mut importer_keys: Vec<&String> = importers.keys().collect();
importer_keys.sort_unstable();
for importer_key in importer_keys {
let importer_facts = &importers[importer_key];
if importer_facts.imports.iter().all(|import| import.is_type) {
continue;
}
let Some(language) = store.lookup(importer_key.as_str()).map(|e| e.language.clone()) else {
continue;
};
let resolver = resolvers
.entry(language)
.or_insert_with_key(|lang| SpecifierResolver::for_language(lang));
let Some(resolver) = resolver.as_ref() else {
continue;
};
resolver.begin_importer();
let importer_rel = crate::path::RelPath::from(importer_key.as_str());
let mut probes = ProbeSet::default();
let mut targets: Vec<(u32, crate::path::RelPath, u32)> = Vec::new();
for import in &importer_facts.imports {
if import.is_type {
continue;
}
let Some(target_rel) = resolver.resolve(root, importer_key, import, &mut probes) else {
tracing::debug!(
importer = %importer_rel,
specifier = %import.specifier,
"cross-file stitch: specifier did not resolve — skipping"
);
continue;
};
if store.lookup(&target_rel).is_none() {
continue;
}
let Some(wanted) = import.imported.as_deref().or_else(|| resolver.default_export_name()) else {
continue;
};
let Some((def_rel, name_start)) =
resolve_export_transitively(root, store, &catalog, resolver, target_rel, wanted, &mut probes)
else {
continue;
};
targets.push((import.local_start, def_rel.clone(), name_start));
let mut use_starts: Vec<u32> = vec![import.local_start];
for edge in &importer_facts.import_uses {
if edge.def_start == import.local_start && !use_starts.contains(&edge.use_start) {
use_starts.push(edge.use_start);
}
}
for use_start in use_starts {
let staged = batch
.writer()
.upsert_cross_file_edge(&def_rel, name_start, &importer_rel, use_start);
match staged {
Ok(()) => {
edges += 1;
batch.item_staged();
}
Err(error) => tracing::warn!(
importer = %importer_rel,
target = %def_rel,
%error,
"cross-file stitch: failed to stage edge — skipping"
),
}
}
}
if !probes.is_empty() || !targets.is_empty() {
batch
.writer()
.upsert_import_probes(&importer_rel, &targets, &probes.hashes());
batch.item_staged();
}
}
let committed = batch.finish();
if committed {
tracing::debug!(edges, "cross-file stitch: staged cross-file resolved edges");
}
committed
}