use std::collections::{HashMap, HashSet};
use crate::extract::TsFragment;
use crate::graph::{Def, Edge};
use super::{resolve_specifier, TsAliasScopes, BARREL_HOPS};
pub(super) struct ResolvedReexport {
spec: String,
line: usize,
star: bool,
names: Vec<(String, String)>,
to: Option<String>,
}
pub(super) struct Exports<'a> {
pub(super) by_file: HashMap<&'a str, HashMap<String, usize>>,
pub(super) reexports_by_file: HashMap<&'a str, Vec<ResolvedReexport>>,
}
pub(super) fn resolve_reexports<'a>(
fragments: &'a [(String, TsFragment)],
file_set: &HashSet<&str>,
alias: &TsAliasScopes,
) -> HashMap<&'a str, Vec<ResolvedReexport>> {
let mut reexports_by_file: HashMap<&str, Vec<ResolvedReexport>> = HashMap::new();
for (file, frag) in fragments {
let rows = frag
.reexports
.iter()
.map(|rx| ResolvedReexport {
spec: rx.spec.clone(),
line: rx.line,
star: rx.star,
names: rx
.names
.iter()
.map(|n| (n.exported.clone(), n.imported.clone()))
.collect(),
to: resolve_specifier(file, &rx.spec, file_set, alias),
})
.collect();
reexports_by_file.insert(file.as_str(), rows);
}
reexports_by_file
}
impl Exports<'_> {
pub(super) fn lookup(&self, file: &str, name: &str) -> Option<usize> {
let mut visited: HashMap<(String, String), usize> = HashMap::new();
lookup_export(file, name, 0, self, &mut visited)
}
}
fn lookup_export(
file: &str,
name: &str,
hops: usize,
exports: &Exports,
visited: &mut HashMap<(String, String), usize>,
) -> Option<usize> {
let key = (file.to_string(), name.to_string());
if visited.get(&key).is_some_and(|&seen| seen <= hops) {
return None;
}
visited.insert(key, hops);
if let Some(own) = exports.by_file.get(file).and_then(|m| m.get(name)) {
return Some(*own);
}
if hops >= BARREL_HOPS {
return None;
}
for rx in exports
.reexports_by_file
.get(file)
.map(Vec::as_slice)
.unwrap_or(&[])
{
let Some(to) = &rx.to else { continue };
if rx.star {
if let Some(hit) = lookup_export(to, name, hops + 1, exports, visited) {
return Some(hit);
}
continue;
}
for (exported, imported) in &rx.names {
if exported != name {
continue;
}
if let Some(hit) = lookup_export(to, imported, hops + 1, exports, visited) {
return Some(hit);
}
}
}
None
}
pub(super) struct FileImports {
pub(super) edges: Vec<Edge>,
pub(super) external: usize,
pub(super) bindings: HashMap<String, (String, String)>,
}
pub(super) fn file_imports(
file: &str,
frag: &TsFragment,
file_set: &HashSet<&str>,
alias: &TsAliasScopes,
defs: &[Def],
exports: &Exports,
) -> FileImports {
let mut out = FileImports {
edges: Vec::new(),
external: 0,
bindings: HashMap::new(),
};
for imp in &frag.imports {
let to = resolve_specifier(file, &imp.spec, file_set, alias);
let Some(to) = to else {
out.external += 1;
continue;
};
out.edges.push(Edge::Import {
from_file: file.to_string(),
from_line: imp.line,
target: imp.spec.clone(),
to_file: to.clone(),
via: None,
bound_defs: Vec::new(),
});
for b in &imp.bindings {
out.bindings
.entry(b.local.clone())
.or_insert((to.clone(), b.imported.clone()));
}
let statement = out.edges.len() - 1;
let mut via_edge: HashMap<String, usize> = HashMap::new();
for b in &imp.bindings {
if b.imported == "*" {
continue;
}
let Some(target) = exports.lookup(&to, &b.imported) else {
continue;
};
let target_file = defs[target].file.clone();
let slot = if target_file == to {
statement
} else if let Some(&slot) = via_edge.get(&target_file) {
slot
} else {
via_edge.insert(target_file.clone(), out.edges.len());
out.edges.push(Edge::Import {
from_file: file.to_string(),
from_line: imp.line,
target: imp.spec.clone(),
to_file: target_file,
via: Some(to.clone()),
bound_defs: Vec::new(),
});
out.edges.len() - 1
};
bind(&mut out.edges[slot], &defs[target].id);
}
}
for rx in exports
.reexports_by_file
.get(file)
.map(Vec::as_slice)
.unwrap_or(&[])
{
match &rx.to {
None => out.external += 1,
Some(to) => {
let mut edge = Edge::Import {
from_file: file.to_string(),
from_line: rx.line,
target: rx.spec.clone(),
to_file: to.clone(),
via: None,
bound_defs: Vec::new(),
};
if !rx.star {
for (_, imported) in &rx.names {
if let Some(target) = exports.lookup(to, imported) {
if defs[target].file == *to {
bind(&mut edge, &defs[target].id);
}
}
}
}
out.edges.push(edge);
}
}
}
out
}
fn bind(edge: &mut Edge, id: &str) {
if let Edge::Import { bound_defs, .. } = edge {
if !bound_defs.iter().any(|d| d == id) {
bound_defs.push(id.to_string());
}
}
}
#[cfg(test)]
mod tests;