use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use petgraph::visit::EdgeRef;
use crate::graph::CodeGraph;
use crate::graph::edge::EdgeKind;
use crate::parser::ParseResult;
use crate::parser::imports::ExportKind;
pub fn resolve_barrel_chains(
graph: &mut CodeGraph,
parse_results: &HashMap<PathBuf, ParseResult>,
verbose: bool,
) {
let barrel_edges: Vec<(PathBuf, String)> = parse_results
.iter()
.flat_map(|(file_path, result)| {
result
.exports
.iter()
.filter_map(|export| {
if export.kind == ExportKind::ReExportAll
&& let Some(source_specifier) = &export.source
{
return Some((file_path.clone(), source_specifier.clone()));
}
None
})
.collect::<Vec<_>>()
})
.collect();
for (barrel_path, source_specifier) in &barrel_edges {
let barrel_dir = match barrel_path.parent() {
Some(d) => d,
None => {
if verbose {
eprintln!(
"barrel: skipping {} — no parent directory",
barrel_path.display()
);
}
continue;
}
};
let resolved_source =
resolve_relative_specifier(barrel_dir, source_specifier, parse_results);
match resolved_source {
Some(source_path) => {
let barrel_idx = graph.file_index.get(barrel_path).copied();
let source_idx = graph.file_index.get(&source_path).copied();
match (barrel_idx, source_idx) {
(Some(b_idx), Some(s_idx)) => {
graph.add_barrel_reexport_all(b_idx, s_idx);
if verbose {
eprintln!(
"barrel: {} --[BarrelReExportAll]--> {}",
barrel_path.display(),
source_path.display()
);
}
}
(None, _) => {
if verbose {
eprintln!(
"barrel: skipping {} — barrel file not in graph",
barrel_path.display()
);
}
}
(_, None) => {
if verbose {
eprintln!(
"barrel: skipping {} re-export of '{}' — source file {} not in graph (external or not indexed)",
barrel_path.display(),
source_specifier,
source_path.display()
);
}
}
}
}
None => {
if verbose {
eprintln!(
"barrel: could not resolve '{}' from {} — skipping",
source_specifier,
barrel_path.display()
);
}
}
}
}
}
pub fn resolve_named_reexport_chains(
graph: &mut CodeGraph,
parse_results: &HashMap<PathBuf, ParseResult>,
verbose: bool,
) -> usize {
let mut barrel_reexports: HashMap<PathBuf, Vec<(Vec<String>, PathBuf)>> = HashMap::new();
for (file_path, result) in parse_results {
let barrel_dir = match file_path.parent() {
Some(d) => d,
None => continue,
};
for export in &result.exports {
if export.kind != ExportKind::ReExport {
continue;
}
let source_specifier = match &export.source {
Some(s) => s,
None => continue,
};
if export.names.is_empty() {
continue;
}
if let Some(source_path) =
resolve_relative_specifier(barrel_dir, source_specifier, parse_results)
{
barrel_reexports
.entry(file_path.clone())
.or_default()
.push((export.names.clone(), source_path));
}
}
}
if barrel_reexports.is_empty() {
return 0;
}
let idx_to_path: HashMap<petgraph::stable_graph::NodeIndex, PathBuf> = graph
.file_index
.iter()
.map(|(path, &idx)| (idx, path.clone()))
.collect();
let candidates: Vec<(PathBuf, PathBuf, String)> = graph
.graph
.edge_indices()
.filter_map(|edge_idx| {
match &graph.graph[edge_idx] {
EdgeKind::ResolvedImport { specifier } => {
let (src_node, tgt_node) = graph.graph.edge_endpoints(edge_idx)?;
let importer_path = idx_to_path.get(&src_node)?;
let barrel_path = idx_to_path.get(&tgt_node)?;
if !barrel_reexports.contains_key(barrel_path) {
return None;
}
if !specifier.starts_with('.') {
return None;
}
Some((
importer_path.clone(),
barrel_path.clone(),
specifier.clone(),
))
}
_ => None,
}
})
.collect();
let mut edges_to_add: Vec<(PathBuf, PathBuf, String)> = Vec::new();
for (importer_path, barrel_path, specifier) in &candidates {
let import_info = match parse_results.get(importer_path) {
Some(r) => r
.imports
.iter()
.find(|i| i.module_path == *specifier)
.cloned(),
None => continue,
};
let wanted_names: Vec<String> = match &import_info {
Some(info) => info
.specifiers
.iter()
.filter_map(|s| {
if s.is_default || s.is_namespace {
None
} else {
Some(s.alias.as_deref().unwrap_or(&s.name).to_owned())
}
})
.collect(),
None => {
continue;
}
};
if wanted_names.is_empty() {
continue;
}
let barrel_exports = match barrel_reexports.get(barrel_path) {
Some(e) => e,
None => continue,
};
for wanted_name in &wanted_names {
if let Some(defining_file) = chase_named_reexport(
wanted_name,
barrel_path,
barrel_exports,
&barrel_reexports,
verbose,
) {
if &defining_file != barrel_path {
edges_to_add.push((importer_path.clone(), defining_file, specifier.clone()));
}
}
}
}
let mut added = 0usize;
for (importer_path, defining_path, specifier) in edges_to_add {
let importer_idx = match graph.file_index.get(&importer_path).copied() {
Some(idx) => idx,
None => continue,
};
let defining_idx = match graph.file_index.get(&defining_path).copied() {
Some(idx) => idx,
None => continue,
};
let already_exists = graph.graph.edges(importer_idx).any(|e| {
e.target() == defining_idx && matches!(e.weight(), EdgeKind::ResolvedImport { .. })
});
if !already_exists {
graph.add_resolved_import(importer_idx, defining_idx, &specifier);
added += 1;
if verbose {
eprintln!(
"barrel(named): {} --[ResolvedImport]--> {} (chased through barrel)",
importer_path.display(),
defining_path.display()
);
}
}
}
added
}
fn chase_named_reexport(
name: &str,
current_barrel: &Path,
current_exports: &[(Vec<String>, PathBuf)],
all_barrel_reexports: &HashMap<PathBuf, Vec<(Vec<String>, PathBuf)>>,
verbose: bool,
) -> Option<PathBuf> {
let mut visited: HashSet<PathBuf> = HashSet::new();
visited.insert(current_barrel.to_path_buf());
chase_named_reexport_inner(
name,
current_exports,
all_barrel_reexports,
&mut visited,
verbose,
)
}
fn chase_named_reexport_inner(
name: &str,
current_exports: &[(Vec<String>, PathBuf)],
all_barrel_reexports: &HashMap<PathBuf, Vec<(Vec<String>, PathBuf)>>,
visited: &mut HashSet<PathBuf>,
verbose: bool,
) -> Option<PathBuf> {
for (exported_names, source_path) in current_exports {
if !exported_names.iter().any(|n| n == name) {
continue;
}
if visited.contains(source_path) {
if verbose {
eprintln!(
"barrel(named): cycle detected at {} — stopping chain for '{}'",
source_path.display(),
name
);
}
return None; }
visited.insert(source_path.clone());
match all_barrel_reexports.get(source_path) {
Some(next_exports) => {
let re_exported_again = next_exports
.iter()
.any(|(ns, _)| ns.iter().any(|n| n == name));
if re_exported_again {
return chase_named_reexport_inner(
name,
next_exports,
all_barrel_reexports,
visited,
verbose,
);
} else {
return Some(source_path.clone());
}
}
None => {
return Some(source_path.clone());
}
}
}
None
}
fn resolve_relative_specifier(
from_dir: &Path,
specifier: &str,
parse_results: &HashMap<PathBuf, ParseResult>,
) -> Option<PathBuf> {
if !specifier.starts_with('.') {
return None;
}
let base = from_dir.join(specifier);
if parse_results.contains_key(&base) {
return Some(base.clone());
}
let extensions = [".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs"];
for ext in &extensions {
let candidate = PathBuf::from(format!("{}{}", base.display(), ext));
if parse_results.contains_key(&candidate) {
return Some(candidate);
}
}
let index_files = ["index.ts", "index.tsx", "index.js", "index.jsx"];
for idx_file in &index_files {
let candidate = base.join(idx_file);
if parse_results.contains_key(&candidate) {
return Some(candidate);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use petgraph::visit::EdgeRef;
use crate::graph::CodeGraph;
use crate::graph::edge::EdgeKind;
use crate::parser::ParseResult;
use crate::parser::imports::{ExportInfo, ExportKind};
use crate::parser::imports::{ImportInfo, ImportKind, ImportSpecifier};
fn make_parse_result(exports: Vec<ExportInfo>) -> ParseResult {
ParseResult {
symbols: vec![],
imports: vec![],
exports,
relationships: vec![],
rust_uses: vec![],
}
}
fn make_parse_result_with_imports(
imports: Vec<ImportInfo>,
exports: Vec<ExportInfo>,
) -> ParseResult {
ParseResult {
symbols: vec![],
imports,
exports,
relationships: vec![],
rust_uses: vec![],
}
}
fn make_named_import(specifier: &str, names: &[&str]) -> ImportInfo {
ImportInfo {
kind: ImportKind::Esm,
module_path: specifier.to_owned(),
specifiers: names
.iter()
.map(|n| ImportSpecifier {
name: n.to_string(),
alias: None,
is_default: false,
is_namespace: false,
})
.collect(),
line: 0,
}
}
#[test]
fn test_barrel_reexport_all_adds_edge() {
let mut graph = CodeGraph::new();
let barrel_path = PathBuf::from("/project/src/index.ts");
let utils_path = PathBuf::from("/project/src/utils.ts");
let barrel_idx = graph.add_file(barrel_path.clone(), "typescript");
let utils_idx = graph.add_file(utils_path.clone(), "typescript");
let barrel_export = ExportInfo {
kind: ExportKind::ReExportAll,
names: vec![],
source: Some("./utils".to_owned()),
};
let mut parse_results: HashMap<PathBuf, ParseResult> = HashMap::new();
parse_results.insert(barrel_path.clone(), make_parse_result(vec![barrel_export]));
parse_results.insert(utils_path.clone(), make_parse_result(vec![]));
resolve_barrel_chains(&mut graph, &parse_results, false);
assert!(
graph.graph.contains_edge(barrel_idx, utils_idx),
"BarrelReExportAll edge should exist from barrel to utils"
);
let edge = graph
.graph
.edges(barrel_idx)
.find(|e| e.target() == utils_idx);
assert!(edge.is_some(), "edge should be found");
match edge.unwrap().weight() {
EdgeKind::BarrelReExportAll => {} other => panic!("expected BarrelReExportAll, got {:?}", other),
}
}
#[test]
fn test_barrel_no_reexport_all_no_edge() {
let mut graph = CodeGraph::new();
let barrel_path = PathBuf::from("/project/src/index.ts");
let utils_path = PathBuf::from("/project/src/utils.ts");
let _barrel_idx = graph.add_file(barrel_path.clone(), "typescript");
let _utils_idx = graph.add_file(utils_path.clone(), "typescript");
let named_reexport = ExportInfo {
kind: ExportKind::ReExport,
names: vec!["helper".to_owned()],
source: Some("./utils".to_owned()),
};
let mut parse_results: HashMap<PathBuf, ParseResult> = HashMap::new();
parse_results.insert(barrel_path.clone(), make_parse_result(vec![named_reexport]));
parse_results.insert(utils_path.clone(), make_parse_result(vec![]));
resolve_barrel_chains(&mut graph, &parse_results, false);
let barrel_idx = graph.file_index[&barrel_path];
let utils_idx = graph.file_index[&utils_path];
let barrel_edge = graph
.graph
.edges(barrel_idx)
.find(|e| e.target() == utils_idx && matches!(e.weight(), EdgeKind::BarrelReExportAll));
assert!(
barrel_edge.is_none(),
"no BarrelReExportAll edge should exist for named re-export"
);
}
#[test]
fn test_barrel_source_not_in_graph_skips_gracefully() {
let mut graph = CodeGraph::new();
let barrel_path = PathBuf::from("/project/src/index.ts");
let _barrel_idx = graph.add_file(barrel_path.clone(), "typescript");
let barrel_export = ExportInfo {
kind: ExportKind::ReExportAll,
names: vec![],
source: Some("./missing".to_owned()),
};
let mut parse_results: HashMap<PathBuf, ParseResult> = HashMap::new();
parse_results.insert(barrel_path.clone(), make_parse_result(vec![barrel_export]));
resolve_barrel_chains(&mut graph, &parse_results, false);
let barrel_idx = graph.file_index[&barrel_path];
let edge_count = graph.graph.edges(barrel_idx).count();
assert_eq!(
edge_count, 0,
"no edges should exist when source is missing"
);
}
#[test]
fn test_named_reexport_adds_direct_edge() {
let mut graph = CodeGraph::new();
let app_path = PathBuf::from("/project/app.ts");
let index_path = PathBuf::from("/project/services/index.ts");
let service_path = PathBuf::from("/project/services/FooService.ts");
let app_idx = graph.add_file(app_path.clone(), "typescript");
let _index_idx = graph.add_file(index_path.clone(), "typescript");
let service_idx = graph.add_file(service_path.clone(), "typescript");
graph.add_resolved_import(app_idx, _index_idx, "./services");
let barrel_export = ExportInfo {
kind: ExportKind::ReExport,
names: vec!["Foo".to_owned()],
source: Some("./FooService".to_owned()),
};
let mut parse_results: HashMap<PathBuf, ParseResult> = HashMap::new();
parse_results.insert(
app_path.clone(),
make_parse_result_with_imports(vec![make_named_import("./services", &["Foo"])], vec![]),
);
parse_results.insert(index_path.clone(), make_parse_result(vec![barrel_export]));
parse_results.insert(service_path.clone(), make_parse_result(vec![]));
let added = resolve_named_reexport_chains(&mut graph, &parse_results, false);
assert_eq!(added, 1, "should have added exactly 1 direct edge");
assert!(
graph.graph.contains_edge(app_idx, service_idx),
"direct ResolvedImport edge should exist from app.ts to FooService.ts"
);
let direct_edge = graph
.graph
.edges(app_idx)
.find(|e| e.target() == service_idx);
assert!(direct_edge.is_some(), "edge to defining file should exist");
assert!(
matches!(
direct_edge.unwrap().weight(),
EdgeKind::ResolvedImport { .. }
),
"edge should be ResolvedImport"
);
}
#[test]
fn test_named_reexport_multi_level_chain() {
let mut graph = CodeGraph::new();
let app_path = PathBuf::from("/project/app.ts");
let outer_path = PathBuf::from("/project/outer/index.ts");
let inner_path = PathBuf::from("/project/outer/inner/index.ts");
let defining_path = PathBuf::from("/project/outer/inner/defining.ts");
let app_idx = graph.add_file(app_path.clone(), "typescript");
let _outer_idx = graph.add_file(outer_path.clone(), "typescript");
let _inner_idx = graph.add_file(inner_path.clone(), "typescript");
let defining_idx = graph.add_file(defining_path.clone(), "typescript");
graph.add_resolved_import(app_idx, _outer_idx, "./outer");
let outer_export = ExportInfo {
kind: ExportKind::ReExport,
names: vec!["Foo".to_owned()],
source: Some("./inner".to_owned()),
};
let inner_export = ExportInfo {
kind: ExportKind::ReExport,
names: vec!["Foo".to_owned()],
source: Some("./defining".to_owned()),
};
let mut parse_results: HashMap<PathBuf, ParseResult> = HashMap::new();
parse_results.insert(
app_path.clone(),
make_parse_result_with_imports(vec![make_named_import("./outer", &["Foo"])], vec![]),
);
parse_results.insert(outer_path.clone(), make_parse_result(vec![outer_export]));
parse_results.insert(inner_path.clone(), make_parse_result(vec![inner_export]));
parse_results.insert(defining_path.clone(), make_parse_result(vec![]));
let added = resolve_named_reexport_chains(&mut graph, &parse_results, false);
assert_eq!(
added, 1,
"should have added exactly 1 edge for the multi-level chain"
);
assert!(
graph.graph.contains_edge(app_idx, defining_idx),
"direct ResolvedImport edge should exist from app.ts to defining.ts"
);
}
#[test]
fn test_named_reexport_cycle_detection() {
let mut graph = CodeGraph::new();
let app_path = PathBuf::from("/project/app.ts");
let a_path = PathBuf::from("/project/a/index.ts");
let b_path = PathBuf::from("/project/b/index.ts");
let app_idx = graph.add_file(app_path.clone(), "typescript");
let _a_idx = graph.add_file(a_path.clone(), "typescript");
let _b_idx = graph.add_file(b_path.clone(), "typescript");
graph.add_resolved_import(app_idx, _a_idx, "./a");
let a_export = ExportInfo {
kind: ExportKind::ReExport,
names: vec!["Foo".to_owned()],
source: Some("../b".to_owned()),
};
let b_export = ExportInfo {
kind: ExportKind::ReExport,
names: vec!["Foo".to_owned()],
source: Some("../a".to_owned()),
};
let mut parse_results: HashMap<PathBuf, ParseResult> = HashMap::new();
parse_results.insert(
app_path.clone(),
make_parse_result_with_imports(vec![make_named_import("./a", &["Foo"])], vec![]),
);
parse_results.insert(a_path.clone(), make_parse_result(vec![a_export]));
parse_results.insert(b_path.clone(), make_parse_result(vec![b_export]));
let added = resolve_named_reexport_chains(&mut graph, &parse_results, false);
assert_eq!(added, 0, "cycle should produce no new edges");
}
#[test]
fn test_named_reexport_no_edge_when_name_not_found() {
let mut graph = CodeGraph::new();
let app_path = PathBuf::from("/project/app.ts");
let index_path = PathBuf::from("/project/services/index.ts");
let service_path = PathBuf::from("/project/services/FooService.ts");
let app_idx = graph.add_file(app_path.clone(), "typescript");
let index_idx = graph.add_file(index_path.clone(), "typescript");
let service_idx = graph.add_file(service_path.clone(), "typescript");
graph.add_resolved_import(app_idx, index_idx, "./services");
let barrel_export = ExportInfo {
kind: ExportKind::ReExport,
names: vec!["Foo".to_owned()], source: Some("./FooService".to_owned()),
};
let mut parse_results: HashMap<PathBuf, ParseResult> = HashMap::new();
parse_results.insert(
app_path.clone(),
make_parse_result_with_imports(
vec![make_named_import("./services", &["Bar"])], vec![],
),
);
parse_results.insert(index_path.clone(), make_parse_result(vec![barrel_export]));
parse_results.insert(service_path.clone(), make_parse_result(vec![]));
let added = resolve_named_reexport_chains(&mut graph, &parse_results, false);
assert_eq!(
added, 0,
"no edge should be added when imported name is not in barrel re-exports"
);
let direct_edge = graph
.graph
.edges(app_idx)
.find(|e| e.target() == service_idx);
assert!(
direct_edge.is_none(),
"no edge to FooService.ts should exist when Bar is not re-exported"
);
}
}