use std::path::Path;
use rayon::prelude::*;
use crate::index::IndexDb;
use crate::intel::model::FileResolvedRefs;
use crate::lang;
use crate::path::RelPath;
use crate::scanner_lanes::contain_panic;
use crate::store::Store;
const INDEX_COMMIT_BATCH: usize = 256;
type FileSnapshot = (String, String, String);
pub(crate) fn resolve_pass(root: &Path, store: &Store, precise: bool) {
let Some(index_db) = store.index_db.as_ref() else {
return;
};
let files: Vec<FileSnapshot> = store
.index
.files
.iter()
.map(|(rel, entry)| {
(
rel.to_str_lossy().into_owned(),
entry.hash_hex.clone(),
entry.language.clone(),
)
})
.collect();
let facts = compute_facts(root, store, &files, precise);
stage_facts(index_db, &facts);
#[cfg(any(feature = "code-intel-js", feature = "code-intel-stack"))]
{
let facts_map = harvest_cross_file_facts(facts);
crate::intel::xfile::stitch_cross_file_edges(root, store, index_db, &facts_map);
}
#[cfg(not(any(feature = "code-intel-js", feature = "code-intel-stack")))]
let _ = facts;
}
pub(crate) fn resolve_pass_incremental(root: &Path, store: &Store, changed: &[String], precise: bool) {
let Some(index_db) = store.index_db.as_ref() else {
return;
};
let changed_snapshot: Vec<FileSnapshot> = changed
.iter()
.filter_map(|rel| {
let entry = store.lookup(rel.as_str())?;
Some((rel.clone(), entry.hash_hex.clone(), entry.language.clone()))
})
.collect();
let changed_facts = compute_facts(root, store, &changed_snapshot, precise);
stage_facts(index_db, &changed_facts);
#[cfg(any(feature = "code-intel-js", feature = "code-intel-stack"))]
xfile_incremental::restitch_affected(root, store, index_db, changed, precise);
#[cfg(not(any(feature = "code-intel-js", feature = "code-intel-stack")))]
let _ = changed_facts;
}
fn compute_facts(root: &Path, store: &Store, files: &[FileSnapshot], precise: bool) -> Vec<(String, FileResolvedRefs)> {
files
.par_iter()
.filter_map(|(rel_str, hash_hex, language)| {
let refs = match store.read_resolved_by_hex(hash_hex) {
Ok(Some(cached)) => cached,
_ => {
let lang = lang::intern(language)?;
let abs = root.join(rel_str);
let bytes = std::fs::read(&abs).ok()?;
let computed = match contain_panic(|| crate::intel::resolve::resolve_file(lang, &abs, &bytes, precise))
{
Ok(computed) => computed,
Err(reason) => {
tracing::warn!(
path = rel_str,
lang,
reason,
"resolve pass: resolver panicked on this file — skipping it; its navigation stays name-only"
);
return None;
}
};
if !computed.is_empty() {
let _ = store.write_resolved_hex(hash_hex, &computed);
}
computed
}
};
Some((rel_str.clone(), refs))
})
.collect()
}
fn stage_facts(index_db: &IndexDb, facts: &[(String, FileResolvedRefs)]) {
let mut writer = index_db.writer();
let mut staged = 0usize;
for (rel_str, refs) in facts {
let rel = RelPath::from(rel_str.as_str());
let staged_res = if refs.is_empty() {
writer.remove_resolved_file(&rel)
} else {
writer.upsert_resolved_file(&rel, refs)
};
if let Err(error) = staged_res {
tracing::warn!(path = %rel, %error, "resolve pass: failed to stage resolved edges — skipping file");
}
staged += 1;
if staged >= INDEX_COMMIT_BATCH {
if let Err(error) = writer.commit() {
tracing::warn!(%error, "resolve pass: index commit failed — resolved navigation may be stale");
}
writer = index_db.writer();
staged = 0;
}
}
if let Err(error) = writer.commit() {
tracing::warn!(%error, "resolve pass: index commit failed — resolved navigation may be stale");
}
}
#[cfg(any(feature = "code-intel-js", feature = "code-intel-stack"))]
fn import_bound_edges(refs: &FileResolvedRefs) -> Vec<crate::intel::model::ResolvedEdge> {
if refs.imports.is_empty() || refs.intra.is_empty() {
return Vec::new();
}
let import_starts: ahash::AHashSet<u32> = refs.imports.iter().map(|i| i.local_start).collect();
refs.intra
.iter()
.filter(|e| import_starts.contains(&e.def_start))
.cloned()
.collect()
}
#[cfg(any(feature = "code-intel-js", feature = "code-intel-stack"))]
fn harvest_cross_file_facts(
facts: Vec<(String, FileResolvedRefs)>,
) -> ahash::AHashMap<String, crate::intel::xfile::FileFacts> {
facts
.into_iter()
.filter(|(_, refs)| !refs.imports.is_empty() || !refs.exports.is_empty())
.map(|(rel, refs)| {
let import_uses = import_bound_edges(&refs);
(
rel,
crate::intel::xfile::FileFacts {
imports: refs.imports,
exports: refs.exports,
import_uses,
},
)
})
.collect()
}
#[cfg(any(feature = "code-intel-js", feature = "code-intel-stack"))]
mod xfile_incremental {
use std::path::Path;
use ahash::{AHashMap, AHashSet};
use rayon::prelude::*;
use super::{FileSnapshot, compute_facts, stage_facts};
use crate::index::IndexDb;
use crate::intel::resolver::SpecifierResolver;
use crate::intel::xfile::{FileFacts, stitch_cross_file_edges};
use crate::store::Store;
fn has_resolver(language: &str) -> bool {
SpecifierResolver::supports(language)
}
struct FileEntry {
language: String,
facts: FileFacts,
}
type ResolverCache = AHashMap<String, Option<SpecifierResolver>>;
fn build_resolvers(entries: &AHashMap<String, FileEntry>) -> ResolverCache {
let mut cache = ResolverCache::new();
for entry in entries.values() {
if !cache.contains_key(&entry.language) {
cache.insert(entry.language.clone(), SpecifierResolver::for_language(&entry.language));
}
}
cache
}
fn resolve_targets(
root: &Path,
importer: &str,
entry: &FileEntry,
resolvers: &ResolverCache,
out: &mut Vec<String>,
) {
let Some(Some(resolver)) = resolvers.get(&entry.language) else {
return;
};
for import in &entry.facts.imports {
if import.is_type {
continue;
}
if let Some(target) = resolver.resolve(root, importer, import)
&& let Some(key) = target.as_str()
{
out.push(key.to_string());
}
}
}
pub(super) fn restitch_affected(root: &Path, store: &Store, index_db: &IndexDb, changed: &[String], precise: bool) {
let changed_set: AHashSet<&str> = changed
.iter()
.filter(|rel| store.lookup(rel.as_str()).is_some_and(|e| has_resolver(&e.language)))
.map(String::as_str)
.collect();
if changed_set.is_empty() {
return;
}
let candidate_files: Vec<(String, String, String)> = store
.index
.files
.iter()
.filter(|(_, e)| has_resolver(&e.language))
.map(|(rel, e)| (rel.to_str_lossy().into_owned(), e.hash_hex.clone(), e.language.clone()))
.collect();
let entries: AHashMap<String, FileEntry> = candidate_files
.par_iter()
.filter_map(|(rel, hash, language)| {
let refs = store.read_resolved_by_hex(hash).ok()??;
if refs.imports.is_empty() && refs.exports.is_empty() {
return None;
}
let import_uses = super::import_bound_edges(&refs);
Some((
rel.clone(),
FileEntry {
language: language.clone(),
facts: FileFacts {
imports: refs.imports,
exports: refs.exports,
import_uses,
},
},
))
})
.collect::<Vec<_>>()
.into_iter()
.collect();
let resolvers = build_resolvers(&entries);
let importers_of_changed: Vec<String> = entries
.par_iter()
.filter_map(|(importer, entry)| {
if entry.facts.imports.is_empty() {
return None;
}
let mut targets = Vec::new();
resolve_targets(root, importer, entry, &resolvers, &mut targets);
targets
.iter()
.any(|t| changed_set.contains(t.as_str()))
.then(|| importer.clone())
})
.collect();
let mut affected: AHashSet<String> = changed_set.iter().map(|s| (*s).to_string()).collect();
affected.extend(importers_of_changed);
let unchanged_affected: Vec<FileSnapshot> = affected
.iter()
.filter(|k| !changed_set.contains(k.as_str()))
.filter_map(|k| {
let entry = store.lookup(k.as_str())?;
Some((k.clone(), entry.hash_hex.clone(), entry.language.clone()))
})
.collect();
let ua_facts = compute_facts(root, store, &unchanged_affected, precise);
stage_facts(index_db, &ua_facts);
let mut stitch_facts: AHashMap<String, FileFacts> = AHashMap::with_capacity(affected.len());
for key in &affected {
if let Some(entry) = entries.get(key) {
stitch_facts.insert(
key.clone(),
FileFacts {
imports: entry.facts.imports.clone(),
exports: entry.facts.exports.clone(),
import_uses: entry.facts.import_uses.clone(),
},
);
}
}
let mut provider_targets: Vec<String> = Vec::new();
for key in &affected {
if let Some(entry) = entries.get(key) {
resolve_targets(root, key, entry, &resolvers, &mut provider_targets);
}
}
for target in provider_targets {
if stitch_facts.contains_key(&target) {
continue;
}
if let Some(entry) = entries.get(&target) {
stitch_facts.insert(
target,
FileFacts {
imports: Vec::new(),
exports: entry.facts.exports.clone(),
import_uses: Vec::new(),
},
);
}
}
stitch_cross_file_edges(root, store, index_db, &stitch_facts);
}
}