1use std::collections::VecDeque;
4
5use fallow_types::discover::FileId;
6use rustc_hash::{FxHashMap, FxHashSet};
7
8use fallow_types::extract::ModuleLoadMechanism;
9
10use super::effective_exports::{EffectiveExportBinding, EffectiveExportIndex};
11use super::types::{
12 ModuleNode, ReferencePathId, ReferencePathInterner, ReferenceRouteGraphId,
13 ReferenceRouteGraphSpec, ReferenceRouteNodeId, ReferenceRouteNodeSpec,
14};
15use super::{EffectiveExportResolution, ExportNamespace, ModuleGraph};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct EffectiveReExportRoute {
20 barrel_file: FileId,
21 exported_name: String,
22}
23
24impl EffectiveReExportRoute {
25 #[must_use]
27 pub const fn barrel_file(&self) -> FileId {
28 self.barrel_file
29 }
30
31 #[must_use]
33 pub fn exported_name(&self) -> &str {
34 &self.exported_name
35 }
36}
37
38impl ModuleGraph {
39 #[must_use]
44 pub fn effective_re_export_routes(
45 &self,
46 source_file: FileId,
47 source_name: &str,
48 namespace: ExportNamespace,
49 ) -> Vec<EffectiveReExportRoute> {
50 let EffectiveExportResolution::Unique(source_binding) =
51 self.resolve_export(source_file, source_name, namespace)
52 else {
53 return Vec::new();
54 };
55
56 let mut re_exports_by_source: FxHashMap<FileId, Vec<(FileId, usize)>> =
57 FxHashMap::default();
58 for module in &self.modules {
59 for (index, re_export) in module.re_exports.iter().enumerate() {
60 re_exports_by_source
61 .entry(re_export.source_file)
62 .or_default()
63 .push((module.file_id, index));
64 }
65 }
66
67 let initial = (source_file, source_name.to_string());
68 let mut visited = FxHashSet::from_iter([initial.clone()]);
69 let mut queue = VecDeque::from([initial]);
70 let mut routes = Vec::new();
71
72 while let Some((current_file, current_name)) = queue.pop_front() {
73 let Some(re_exports) = re_exports_by_source.get(¤t_file) else {
74 continue;
75 };
76 for &(barrel_file, re_export_index) in re_exports {
77 let re_export = &self.modules[barrel_file.0 as usize].re_exports[re_export_index];
78 let Some(exported_name) =
79 effective_destination_name(re_export, ¤t_name, namespace)
80 else {
81 continue;
82 };
83 if self.resolve_export(barrel_file, exported_name, namespace)
84 != EffectiveExportResolution::Unique(source_binding)
85 {
86 continue;
87 }
88
89 let destination = (barrel_file, exported_name.to_string());
90 if !visited.insert(destination.clone()) {
91 continue;
92 }
93 routes.push(EffectiveReExportRoute {
94 barrel_file,
95 exported_name: destination.1.clone(),
96 });
97 queue.push_back(destination);
98 }
99 }
100
101 routes
102 }
103}
104
105pub(in crate::graph) struct EffectiveDeclarationRoute {
106 pub(in crate::graph) binding: EffectiveExportBinding,
107 graph: ReferenceRouteGraphSpec,
108 start: ReferenceRouteNodeId,
109 terminal: ReferenceRouteNodeId,
110}
111
112impl EffectiveDeclarationRoute {
113 pub(in crate::graph) fn intern(
114 self,
115 reference_paths: &mut ReferencePathInterner,
116 ) -> InternedEffectiveDeclarationRoute {
117 let graph = reference_paths
118 .tracks_provenance()
119 .then(|| reference_paths.intern_route_graph(self.graph));
120 InternedEffectiveDeclarationRoute {
121 graph,
122 start: self.start,
123 terminal: self.terminal,
124 }
125 }
126}
127
128#[derive(Clone, Copy)]
129pub(in crate::graph) struct InternedEffectiveDeclarationRoute {
130 graph: Option<ReferenceRouteGraphId>,
131 start: ReferenceRouteNodeId,
132 terminal: ReferenceRouteNodeId,
133}
134
135impl InternedEffectiveDeclarationRoute {
136 pub(in crate::graph) fn extend_path(
137 &self,
138 parent: Option<ReferencePathId>,
139 reference_paths: &mut ReferencePathInterner,
140 ) -> Option<ReferencePathId> {
141 let graph = self.graph?;
142 reference_paths.route(
143 parent,
144 graph,
145 self.start,
146 self.terminal,
147 Some(ModuleLoadMechanism::EsModule),
148 )
149 }
150}
151
152pub(in crate::graph) fn effective_declaration_route(
153 modules: &[ModuleNode],
154 index: &EffectiveExportIndex,
155 file: FileId,
156 name: &str,
157 namespace: ExportNamespace,
158) -> Option<EffectiveDeclarationRoute> {
159 let EffectiveExportResolution::Unique(binding) = index.resolve(file, name, namespace) else {
160 return None;
161 };
162 binding.origin_slot()?;
163
164 let initial: (FileId, &str) = (file, name);
167 let mut states = vec![initial];
168 let mut state_ids: FxHashMap<(FileId, &str), usize> = FxHashMap::from_iter([(initial, 0)]);
169 let mut successors: Vec<Vec<usize>> = vec![Vec::new()];
170 let mut frontier = VecDeque::from([0_usize]);
171 let mut terminal = None;
172
173 while let Some(state_id) = frontier.pop_front() {
174 let (current_file, current_name) = states[state_id];
175 if current_file == binding.origin_file() {
176 terminal = Some(state_id);
177 continue;
178 }
179 let module = modules.get(current_file.0 as usize)?;
180 for edge in &module.re_exports {
181 let Some(source_name) = effective_source_name(edge, current_name, namespace) else {
182 continue;
183 };
184 if index.resolve(edge.source_file, source_name, namespace)
185 != EffectiveExportResolution::Unique(binding)
186 {
187 continue;
188 }
189 let state = (edge.source_file, source_name);
190 let next_id = if let Some(next_id) = state_ids.get(&state) {
191 *next_id
192 } else {
193 let next_id = states.len();
194 states.push(state);
195 state_ids.insert(state, next_id);
196 successors.push(Vec::new());
197 frontier.push_back(next_id);
198 next_id
199 };
200 successors[state_id].push(next_id);
201 }
202 }
203
204 let terminal = terminal?;
205 for next in &mut successors {
206 next.sort_unstable();
207 next.dedup();
208 }
209 let nodes = states
210 .iter()
211 .zip(successors)
212 .map(|((target, _), successors)| {
213 ReferenceRouteNodeSpec::new(
214 *target,
215 ModuleLoadMechanism::EsModule,
216 successors
217 .into_iter()
218 .map(|id| ReferenceRouteNodeId(id as u32))
219 .collect(),
220 )
221 })
222 .collect();
223 Some(EffectiveDeclarationRoute {
224 binding,
225 graph: ReferenceRouteGraphSpec::new(nodes),
226 start: ReferenceRouteNodeId(0),
227 terminal: ReferenceRouteNodeId(terminal as u32),
228 })
229}
230
231fn effective_destination_name<'a>(
232 re_export: &'a super::ReExportEdge,
233 source_name: &'a str,
234 namespace: ExportNamespace,
235) -> Option<&'a str> {
236 if namespace == ExportNamespace::Value && re_export.is_type_only {
237 return None;
238 }
239 if re_export.exported_name == "*" {
240 return (source_name != "default").then_some(source_name);
241 }
242 if re_export.imported_name == "*" || re_export.imported_name != source_name {
243 return None;
244 }
245 Some(&re_export.exported_name)
246}
247
248fn effective_source_name<'a>(
249 re_export: &'a super::ReExportEdge,
250 exported_name: &'a str,
251 namespace: ExportNamespace,
252) -> Option<&'a str> {
253 if namespace == ExportNamespace::Value && re_export.is_type_only {
254 return None;
255 }
256 if re_export.exported_name == "*" {
257 return (exported_name != "default").then_some(exported_name);
258 }
259 if re_export.exported_name != exported_name || re_export.imported_name == "*" {
260 return None;
261 }
262 Some(&re_export.imported_name)
263}
264
265#[cfg(test)]
266mod tests {
267 use std::path::PathBuf;
268
269 use fallow_types::extract::{ExportInfo, ExportName, ReExportInfo, VisibilityTag};
270 use oxc_span::Span;
271
272 use super::*;
273 use crate::graph::ReExportEdge;
274 use crate::graph::types::{ExportSymbol, ReferencePathNode};
275 use crate::resolve::{ResolveResult, ResolvedModule, ResolvedReExport};
276
277 fn source_export() -> ExportInfo {
278 ExportInfo {
279 name: ExportName::Named("foo".to_string()),
280 local_name: Some("foo".to_string()),
281 is_type_only: false,
282 is_side_effect_used: false,
283 visibility: VisibilityTag::None,
284 expected_unused_reason: None,
285 span: Span::new(0, 3),
286 members: Vec::new(),
287 super_class: None,
288 deprecated: false,
289 deprecated_reason: None,
290 }
291 }
292
293 fn re_export(target: FileId, imported_name: &str, exported_name: &str) -> ResolvedReExport {
294 ResolvedReExport {
295 info: ReExportInfo {
296 source: "./source".to_string(),
297 imported_name: imported_name.to_string(),
298 exported_name: exported_name.to_string(),
299 is_type_only: false,
300 span: Span::default(),
301 statement_span: Span::default(),
302 source_span: Span::default(),
303 },
304 target: ResolveResult::InternalModule(target),
305 }
306 }
307
308 fn re_export_edge(
309 source_file: FileId,
310 imported_name: &str,
311 exported_name: &str,
312 ) -> ReExportEdge {
313 ReExportEdge {
314 source_file,
315 imported_name: imported_name.to_string(),
316 exported_name: exported_name.to_string(),
317 is_type_only: false,
318 span: Span::default(),
319 }
320 }
321
322 fn module(
323 file_id: FileId,
324 path: &str,
325 exports: Vec<ExportSymbol>,
326 re_exports: Vec<ReExportEdge>,
327 ) -> ModuleNode {
328 ModuleNode {
329 file_id,
330 path: PathBuf::from(path),
331 edge_range: 0..0,
332 exports,
333 re_exports,
334 flags: 0,
335 }
336 }
337
338 fn source_symbol() -> ExportSymbol {
339 ExportSymbol {
340 name: ExportName::Named("foo".to_string()),
341 is_type_only: false,
342 is_side_effect_used: false,
343 visibility: VisibilityTag::None,
344 expected_unused_reason: None,
345 span: Span::new(0, 3),
346 references: Vec::new(),
347 reference_paths: Vec::new(),
348 members: Vec::new(),
349 deprecated: false,
350 deprecated_reason: None,
351 }
352 }
353
354 #[test]
355 fn declaration_route_retains_star_surface_and_origin_hops() {
356 let resolved = vec![
357 ResolvedModule {
358 file_id: FileId(0),
359 re_exports: vec![re_export(FileId(1), "*", "*")],
360 ..Default::default()
361 },
362 ResolvedModule {
363 file_id: FileId(1),
364 exports: vec![source_export()].into(),
365 ..Default::default()
366 },
367 ];
368 let mut modules = vec![
369 module(
370 FileId(0),
371 "/project/inner.ts",
372 Vec::new(),
373 vec![re_export_edge(FileId(1), "*", "*")],
374 ),
375 module(
376 FileId(1),
377 "/project/source.ts",
378 vec![source_symbol()],
379 Vec::new(),
380 ),
381 ];
382 let index = EffectiveExportIndex::build(&resolved);
383
384 let route =
385 effective_declaration_route(&modules, &index, FileId(0), "foo", ExportNamespace::Value)
386 .expect("the star surface must retain a route to its declaration");
387
388 assert_eq!(route.binding.origin_file(), FileId(1));
389 let mut paths = ReferencePathInterner::new(true);
390 let route = route.intern(&mut paths);
391 let path = route
392 .extend_path(None, &mut paths)
393 .expect("tracked routes return one interned path");
394 let finalized = paths.finalize(&mut modules);
395 let ReferencePathNode::Route {
396 graph,
397 start,
398 terminal,
399 start_mechanism,
400 ..
401 } = finalized.paths[path.index()]
402 else {
403 panic!("effective declaration routes use the compact route contract");
404 };
405 assert_eq!(
406 finalized
407 .routes
408 .canonical_hops(graph, start, terminal, start_mechanism),
409 vec![
410 (FileId(1), ModuleLoadMechanism::EsModule),
411 (FileId(0), ModuleLoadMechanism::EsModule),
412 ]
413 );
414 }
415
416 #[test]
417 fn declaration_route_retains_rename_star_and_origin_hops() {
418 let resolved = vec![
419 ResolvedModule {
420 file_id: FileId(0),
421 re_exports: vec![re_export(FileId(1), "foo", "alias")],
422 ..Default::default()
423 },
424 ResolvedModule {
425 file_id: FileId(1),
426 re_exports: vec![re_export(FileId(2), "*", "*")],
427 ..Default::default()
428 },
429 ResolvedModule {
430 file_id: FileId(2),
431 exports: vec![source_export()].into(),
432 ..Default::default()
433 },
434 ];
435 let mut modules = vec![
436 module(
437 FileId(0),
438 "/project/rename.ts",
439 Vec::new(),
440 vec![re_export_edge(FileId(1), "foo", "alias")],
441 ),
442 module(
443 FileId(1),
444 "/project/inner.ts",
445 Vec::new(),
446 vec![re_export_edge(FileId(2), "*", "*")],
447 ),
448 module(
449 FileId(2),
450 "/project/source.ts",
451 vec![source_symbol()],
452 Vec::new(),
453 ),
454 ];
455 let index = EffectiveExportIndex::build(&resolved);
456 let route = effective_declaration_route(
457 &modules,
458 &index,
459 FileId(0),
460 "alias",
461 ExportNamespace::Value,
462 )
463 .expect("the renamed star surface must retain its declaration route");
464
465 assert_eq!(route.binding.origin_file(), FileId(2));
466 let mut paths = ReferencePathInterner::new(true);
467 let route = route.intern(&mut paths);
468 let path = route
469 .extend_path(None, &mut paths)
470 .expect("tracked routes return one interned path");
471 let finalized = paths.finalize(&mut modules);
472 let ReferencePathNode::Route {
473 graph,
474 start,
475 terminal,
476 start_mechanism,
477 ..
478 } = finalized.paths[path.index()]
479 else {
480 panic!("effective declaration routes use the compact route contract");
481 };
482 assert_eq!(
483 finalized
484 .routes
485 .canonical_hops(graph, start, terminal, start_mechanism),
486 vec![
487 (FileId(2), ModuleLoadMechanism::EsModule),
488 (FileId(1), ModuleLoadMechanism::EsModule),
489 (FileId(0), ModuleLoadMechanism::EsModule),
490 ]
491 );
492 }
493}