1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! Phase 4: Re-export chain resolution — propagate references through barrel files.
mod propagate;
#[cfg(test)]
mod tests;
use rustc_hash::{FxHashMap, FxHashSet};
use fallow_types::discover::FileId;
use super::ModuleGraph;
use propagate::{propagate_named_re_export, propagate_star_re_export};
impl ModuleGraph {
/// Resolve re-export chains: when module A re-exports from B,
/// any reference to A's re-exported symbol should also count as a reference
/// to B's original export (and transitively through the chain).
pub(super) fn resolve_re_export_chains(&mut self) {
// Collect re-export info: (barrel_file_id, source_file_id, imported_name, exported_name)
let re_export_info: Vec<(FileId, FileId, String, String)> = self
.modules
.iter()
.flat_map(|m| {
m.re_exports.iter().map(move |re| {
(
m.file_id,
re.source_file,
re.imported_name.clone(),
re.exported_name.clone(),
)
})
})
.collect();
if re_export_info.is_empty() {
return;
}
// Precompute barrels that are transitively star-re-exported from entry points.
// These get entry-point-like treatment: all source exports are marked used.
// Entry points often expose public APIs through multiple `export *`
// barrels, so direct targets alone are not enough.
// Computing this once avoids O(modules) per call inside the hot loop.
let mut entry_star_targets: FxHashSet<FileId> = self
.modules
.iter()
.filter(|m| m.is_entry_point())
.flat_map(|m| {
m.re_exports
.iter()
.filter(|re| re.exported_name == "*")
.map(|re| re.source_file)
})
.collect();
let mut entry_star_stack: Vec<FileId> = entry_star_targets.iter().copied().collect();
while let Some(file_id) = entry_star_stack.pop() {
let idx = file_id.0 as usize;
if idx >= self.modules.len() {
continue;
}
for re in self.modules[idx]
.re_exports
.iter()
.filter(|re| re.exported_name == "*")
{
if entry_star_targets.insert(re.source_file) {
entry_star_stack.push(re.source_file);
}
}
}
// Pre-build reverse edge index: target FileId → edge indices.
// This avoids O(all_edges) scans per star re-export in the hot loop.
// For barrel-heavy monorepos (Vue/Nuxt), star re-exports dominate the
// iteration cost — without this index, each call to propagate_star_re_export
// linearly scans all edges to find those targeting the barrel.
let mut edges_by_target: FxHashMap<FileId, Vec<usize>> = FxHashMap::default();
for (idx, edge) in self.edges.iter().enumerate() {
edges_by_target.entry(edge.target).or_default().push(idx);
}
// For each re-export, if the barrel's exported symbol has references,
// propagate those references to the source module's original export.
// We iterate until no new references are added (handles chains).
let mut changed = true;
let max_iterations = 20; // prevent infinite loops on cycles
let mut iteration = 0;
// Reuse a single HashSet across iterations to avoid repeated allocations.
// In barrel-heavy monorepos, this loop can run up to max_iterations × re_export_info.len()
// × target_exports.len() times — reusing with .clear() avoids O(n) allocations.
let mut existing_refs: FxHashSet<FileId> = FxHashSet::default();
while changed && iteration < max_iterations {
changed = false;
iteration += 1;
for &(barrel_id, source_id, ref imported_name, ref exported_name) in &re_export_info {
let barrel_idx = barrel_id.0 as usize;
let source_idx = source_id.0 as usize;
if barrel_idx >= self.modules.len() || source_idx >= self.modules.len() {
continue;
}
if exported_name == "*" {
changed |= propagate_star_re_export(
&mut self.modules,
&self.edges,
&edges_by_target,
barrel_id,
barrel_idx,
source_idx,
&entry_star_targets,
);
} else {
changed |= propagate_named_re_export(
&mut self.modules,
barrel_id,
barrel_idx,
source_idx,
imported_name,
exported_name,
&mut existing_refs,
);
}
}
}
if iteration >= max_iterations {
tracing::warn!(
iterations = max_iterations,
"Re-export chain resolution hit iteration limit, some chains may be incomplete"
);
}
}
}