1mod propagate;
4#[cfg(test)]
5mod tests;
6
7use std::path::PathBuf;
8
9use rustc_hash::{FxHashMap, FxHashSet};
10
11use fallow_types::discover::FileId;
12
13use crate::resolve::ResolvedModule;
14
15use super::ModuleGraph;
16
17use propagate::{
18 NamedReExportPropagation, StarReExportPropagation, propagate_named_re_export,
19 propagate_star_re_export,
20};
21
22#[derive(Debug, Clone)]
30pub struct GraphReExportCycle {
31 pub files: Vec<PathBuf>,
35 pub file_ids: Vec<FileId>,
40 pub is_self_loop: bool,
43}
44
45struct ReExportTuple {
51 barrel: FileId,
52 source: FileId,
53 imported_name: String,
54 exported_name: String,
55 is_type_only: bool,
60}
61
62struct ReExportContext<'a> {
63 entry_star_targets: &'a FxHashSet<FileId>,
64 edges_by_target: &'a FxHashMap<FileId, Vec<usize>>,
65 module_by_id: &'a FxHashMap<FileId, &'a ResolvedModule>,
66 existing_refs: &'a mut FxHashSet<FileId>,
67 synthetic_stubs: &'a mut FxHashSet<(FileId, String, bool)>,
68}
69
70impl ModuleGraph {
71 pub(super) fn resolve_re_export_chains(
80 &mut self,
81 module_by_id: &FxHashMap<FileId, &ResolvedModule>,
82 ) -> Vec<GraphReExportCycle> {
83 let re_export_info = self.collect_re_export_tuples();
84
85 if re_export_info.is_empty() {
86 return Vec::new();
87 }
88
89 let cycles = find_re_export_cycles(&self.modules, &re_export_info);
90
91 let entry_star_targets = self.collect_entry_star_targets();
92 let edges_by_target = self.build_edges_by_target();
93
94 self.run_re_export_fixpoint(
95 &re_export_info,
96 &entry_star_targets,
97 &edges_by_target,
98 module_by_id,
99 );
100
101 cycles
102 }
103
104 fn collect_re_export_tuples(&self) -> Vec<ReExportTuple> {
106 self.modules
107 .iter()
108 .flat_map(|m| {
109 m.re_exports.iter().map(move |re| ReExportTuple {
110 barrel: m.file_id,
111 source: re.source_file,
112 imported_name: re.imported_name.clone(),
113 exported_name: re.exported_name.clone(),
114 is_type_only: re.is_type_only,
115 })
116 })
117 .collect()
118 }
119
120 fn collect_entry_star_targets(&self) -> FxHashSet<FileId> {
123 let mut entry_star_targets: FxHashSet<FileId> = self
124 .modules
125 .iter()
126 .filter(|m| m.is_entry_point())
127 .flat_map(|m| {
128 m.re_exports
129 .iter()
130 .filter(|re| re.exported_name == "*")
131 .map(|re| re.source_file)
132 })
133 .collect();
134 let mut entry_star_stack: Vec<FileId> = entry_star_targets.iter().copied().collect();
135 while let Some(file_id) = entry_star_stack.pop() {
136 let idx = file_id.0 as usize;
137 if idx >= self.modules.len() {
138 continue;
139 }
140
141 for re in self.modules[idx]
142 .re_exports
143 .iter()
144 .filter(|re| re.exported_name == "*")
145 {
146 if entry_star_targets.insert(re.source_file) {
147 entry_star_stack.push(re.source_file);
148 }
149 }
150 }
151 entry_star_targets
152 }
153
154 fn build_edges_by_target(&self) -> FxHashMap<FileId, Vec<usize>> {
156 let mut edges_by_target: FxHashMap<FileId, Vec<usize>> = FxHashMap::default();
157 for (idx, edge) in self.edges.iter().enumerate() {
158 edges_by_target.entry(edge.target).or_default().push(idx);
159 }
160 edges_by_target
161 }
162
163 fn run_re_export_fixpoint(
165 &mut self,
166 re_export_info: &[ReExportTuple],
167 entry_star_targets: &FxHashSet<FileId>,
168 edges_by_target: &FxHashMap<FileId, Vec<usize>>,
169 module_by_id: &FxHashMap<FileId, &ResolvedModule>,
170 ) {
171 let safety_cap = re_export_info.len().saturating_add(1);
172 let mut changed = true;
173 let mut iteration: usize = 0;
174 let mut existing_refs: FxHashSet<FileId> = FxHashSet::default();
175 let mut synthetic_stubs: FxHashSet<(FileId, String, bool)> = FxHashSet::default();
176
177 while changed && iteration < safety_cap {
178 changed = false;
179 iteration += 1;
180
181 let mut context = ReExportContext {
182 entry_star_targets,
183 edges_by_target,
184 module_by_id,
185 existing_refs: &mut existing_refs,
186 synthetic_stubs: &mut synthetic_stubs,
187 };
188
189 for entry in re_export_info {
190 changed |= self.propagate_re_export_entry(entry, &mut context);
191 }
192 }
193
194 if iteration >= safety_cap && changed {
195 tracing::error!(
196 iterations = iteration,
197 safety_cap,
198 re_export_edges = re_export_info.len(),
199 "Re-export chain fixpoint exceeded safety cap; \
200 propagation may be non-monotonic. Please file a bug at \
201 https://github.com/fallow-rs/fallow/issues with the repro."
202 );
203 }
204 }
205
206 fn propagate_re_export_entry(
208 &mut self,
209 entry: &ReExportTuple,
210 context: &mut ReExportContext<'_>,
211 ) -> bool {
212 let barrel_idx = entry.barrel.0 as usize;
213 let source_idx = entry.source.0 as usize;
214
215 if barrel_idx >= self.modules.len() || source_idx >= self.modules.len() {
216 return false;
217 }
218
219 if entry.exported_name == "*" {
220 propagate_star_re_export(StarReExportPropagation {
221 modules: &mut self.modules,
222 edges: &self.edges,
223 edges_by_target: context.edges_by_target,
224 module_by_id: context.module_by_id,
225 barrel_id: entry.barrel,
226 barrel_idx,
227 source_id: entry.source,
228 source_idx,
229 entry_star_targets: context.entry_star_targets,
230 triggering_is_type_only: entry.is_type_only,
231 synthetic_stubs: context.synthetic_stubs,
232 })
233 } else {
234 propagate_named_re_export(NamedReExportPropagation {
235 modules: &mut self.modules,
236 barrel_id: entry.barrel,
237 barrel_idx,
238 source_idx,
239 imported_name: &entry.imported_name,
240 exported_name: &entry.exported_name,
241 existing_refs: context.existing_refs,
242 })
243 }
244 }
245}
246
247fn find_re_export_cycles(
257 modules: &[super::types::ModuleNode],
258 re_export_info: &[ReExportTuple],
259) -> Vec<GraphReExportCycle> {
260 let mut cycles: Vec<GraphReExportCycle> = Vec::new();
261
262 let (node_index, nodes) = build_re_export_node_index(re_export_info);
263 let n = nodes.len();
264 if n == 0 {
265 return cycles;
266 }
267
268 let adj = build_re_export_adjacency(re_export_info, &node_index, modules, &mut cycles);
269
270 let sccs = tarjan_scc(n, &adj);
271
272 for scc in &sccs {
273 if scc.len() < 2 {
274 continue;
275 }
276 cycles.push(build_multi_node_cycle(scc, &nodes, modules));
277 }
278
279 cycles
280}
281
282fn build_re_export_node_index(
284 re_export_info: &[ReExportTuple],
285) -> (FxHashMap<FileId, usize>, Vec<FileId>) {
286 let mut node_index: FxHashMap<FileId, usize> = FxHashMap::default();
287 let mut nodes: Vec<FileId> = Vec::new();
288 for entry in re_export_info {
289 for &id in &[entry.barrel, entry.source] {
290 node_index.entry(id).or_insert_with(|| {
291 let idx = nodes.len();
292 nodes.push(id);
293 idx
294 });
295 }
296 }
297 (node_index, nodes)
298}
299
300fn build_re_export_adjacency(
303 re_export_info: &[ReExportTuple],
304 node_index: &FxHashMap<FileId, usize>,
305 modules: &[super::types::ModuleNode],
306 cycles: &mut Vec<GraphReExportCycle>,
307) -> Vec<Vec<usize>> {
308 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); node_index.len()];
309 let mut seen_edge: FxHashSet<(usize, usize)> = FxHashSet::default();
310 let mut seen_self_loop: FxHashSet<FileId> = FxHashSet::default();
311 for entry in re_export_info {
312 let from = node_index[&entry.barrel];
313 let to = node_index[&entry.source];
314 if from == to {
315 if seen_self_loop.insert(entry.barrel) {
316 cycles.push(build_self_loop_cycle(entry.barrel, modules));
317 }
318 continue;
319 }
320 if seen_edge.insert((from, to)) {
321 adj[from].push(to);
322 }
323 }
324 adj
325}
326
327fn build_self_loop_cycle(
329 barrel: FileId,
330 modules: &[super::types::ModuleNode],
331) -> GraphReExportCycle {
332 let (path_buf, path_display) = module_path_and_display(barrel, modules);
333 tracing::warn!(
334 file = path_display.as_str(),
335 "Re-export self-loop detected: this file re-exports from \
336 itself. Chain propagation is structurally a no-op for \
337 these edges. Inspect the barrel for an accidental \
338 `export * from './<this-file>'` after a rename or move."
339 );
340 GraphReExportCycle {
341 files: vec![path_buf],
342 file_ids: vec![barrel],
343 is_self_loop: true,
344 }
345}
346
347fn build_multi_node_cycle(
349 scc: &[usize],
350 nodes: &[FileId],
351 modules: &[super::types::ModuleNode],
352) -> GraphReExportCycle {
353 let mut triples: Vec<(PathBuf, String, FileId)> = scc
354 .iter()
355 .map(|&idx| {
356 let file_id = nodes[idx];
357 let (path, display) = module_path_and_display(file_id, modules);
358 (path, display, file_id)
359 })
360 .collect();
361 triples.sort_by(|a, b| a.1.cmp(&b.1));
362 let members = triples
363 .iter()
364 .map(|(_, d, _)| d.as_str())
365 .collect::<Vec<_>>()
366 .join(" <-> ");
367 tracing::warn!(
368 cycle_size = scc.len(),
369 members = members.as_str(),
370 "Re-export cycle detected: chain propagation may be incomplete \
371 for symbols on this barrel loop. Break the cycle to restore \
372 full reachability analysis."
373 );
374 let (files, file_ids) = triples.into_iter().fold(
375 (Vec::new(), Vec::new()),
376 |(mut paths, mut ids), (p, _, id)| {
377 paths.push(p);
378 ids.push(id);
379 (paths, ids)
380 },
381 );
382 GraphReExportCycle {
383 files,
384 file_ids,
385 is_self_loop: false,
386 }
387}
388
389fn module_path_and_display(
392 file_id: FileId,
393 modules: &[super::types::ModuleNode],
394) -> (PathBuf, String) {
395 let i = file_id.0 as usize;
396 if i < modules.len() {
397 let p = modules[i].path.clone();
398 let d = p.display().to_string();
399 (p, d)
400 } else {
401 let placeholder = format!("<file id {i}>");
402 (PathBuf::from(&placeholder), placeholder)
403 }
404}
405
406struct TarjanFrame {
407 node: usize,
408 next_succ: usize,
409}
410
411struct TarjanState {
413 index_counter: u32,
414 indices: Vec<u32>,
415 lowlinks: Vec<u32>,
416 on_stack: fixedbitset::FixedBitSet,
417 stack: Vec<usize>,
418 sccs: Vec<Vec<usize>>,
419}
420
421impl TarjanState {
422 fn new(n: usize) -> Self {
423 Self {
424 index_counter: 0,
425 indices: vec![u32::MAX; n],
426 lowlinks: vec![0; n],
427 on_stack: fixedbitset::FixedBitSet::with_capacity(n),
428 stack: Vec::new(),
429 sccs: Vec::new(),
430 }
431 }
432
433 fn discover(&mut self, node: usize) {
435 self.indices[node] = self.index_counter;
436 self.lowlinks[node] = self.index_counter;
437 self.index_counter = self.index_counter.saturating_add(1);
438 self.stack.push(node);
439 self.on_stack.insert(node);
440 }
441
442 fn step_successor(&mut self, frame: &mut TarjanFrame, adj: &[Vec<usize>]) -> Option<usize> {
445 let v = frame.node;
446 let w = adj[v][frame.next_succ];
447 frame.next_succ = frame.next_succ.saturating_add(1);
448 if self.indices[w] == u32::MAX {
449 self.discover(w);
450 Some(w)
451 } else {
452 if self.on_stack.contains(w) {
453 self.lowlinks[v] = self.lowlinks[v].min(self.indices[w]);
454 }
455 None
456 }
457 }
458
459 fn finish_frame(&mut self, v: usize, parent: Option<usize>) {
462 if self.lowlinks[v] == self.indices[v] {
463 let mut scc = Vec::new();
464 while let Some(w) = self.stack.pop() {
465 self.on_stack.remove(w);
466 scc.push(w);
467 if w == v {
468 break;
469 }
470 }
471 self.sccs.push(scc);
472 }
473 if let Some(pv) = parent {
474 self.lowlinks[pv] = self.lowlinks[pv].min(self.lowlinks[v]);
475 }
476 }
477}
478
479fn tarjan_scc(n: usize, adj: &[Vec<usize>]) -> Vec<Vec<usize>> {
483 let mut state = TarjanState::new(n);
484
485 for start in 0..n {
486 if state.indices[start] != u32::MAX {
487 continue;
488 }
489 state.discover(start);
490 let mut dfs: Vec<TarjanFrame> = vec![TarjanFrame {
491 node: start,
492 next_succ: 0,
493 }];
494
495 while let Some(frame) = dfs.last_mut() {
496 let v = frame.node;
497 if frame.next_succ < adj[v].len() {
498 if let Some(child) = state.step_successor(frame, adj) {
499 dfs.push(TarjanFrame {
500 node: child,
501 next_succ: 0,
502 });
503 }
504 } else {
505 dfs.pop();
506 state.finish_frame(v, dfs.last().map(|parent| parent.node));
507 }
508 }
509 }
510
511 state.sccs
512}