Skip to main content

fallow_graph/graph/
effective_re_exports.rs

1//! Effective outward re-export routes for one canonical binding.
2
3use 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/// One module/name pair that effectively exposes a traced binding.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct EffectiveReExportRoute {
20    barrel_file: FileId,
21    exported_name: String,
22}
23
24impl EffectiveReExportRoute {
25    /// Module exposing the binding at this step.
26    #[must_use]
27    pub const fn barrel_file(&self) -> FileId {
28        self.barrel_file
29    }
30
31    /// Name exposed by this module.
32    #[must_use]
33    pub fn exported_name(&self) -> &str {
34        &self.exported_name
35    }
36}
37
38impl ModuleGraph {
39    /// Every effective outward route from one exported binding.
40    ///
41    /// Routes carry aliases across named re-exports, omit ambiguous and
42    /// shadowed paths, deduplicate convergent diamonds, and terminate on cycles.
43    #[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(&current_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, &current_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    // States borrow their names from the module re-export edges, so the search
165    // walks the topology without allocating one string per visited hop.
166    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        }
289    }
290
291    fn re_export(target: FileId, imported_name: &str, exported_name: &str) -> ResolvedReExport {
292        ResolvedReExport {
293            info: ReExportInfo {
294                source: "./source".to_string(),
295                imported_name: imported_name.to_string(),
296                exported_name: exported_name.to_string(),
297                is_type_only: false,
298                span: Span::default(),
299                statement_span: Span::default(),
300                source_span: Span::default(),
301            },
302            target: ResolveResult::InternalModule(target),
303        }
304    }
305
306    fn re_export_edge(
307        source_file: FileId,
308        imported_name: &str,
309        exported_name: &str,
310    ) -> ReExportEdge {
311        ReExportEdge {
312            source_file,
313            imported_name: imported_name.to_string(),
314            exported_name: exported_name.to_string(),
315            is_type_only: false,
316            span: Span::default(),
317        }
318    }
319
320    fn module(
321        file_id: FileId,
322        path: &str,
323        exports: Vec<ExportSymbol>,
324        re_exports: Vec<ReExportEdge>,
325    ) -> ModuleNode {
326        ModuleNode {
327            file_id,
328            path: PathBuf::from(path),
329            edge_range: 0..0,
330            exports,
331            re_exports,
332            flags: 0,
333        }
334    }
335
336    fn source_symbol() -> ExportSymbol {
337        ExportSymbol {
338            name: ExportName::Named("foo".to_string()),
339            is_type_only: false,
340            is_side_effect_used: false,
341            visibility: VisibilityTag::None,
342            expected_unused_reason: None,
343            span: Span::new(0, 3),
344            references: Vec::new(),
345            reference_paths: Vec::new(),
346            members: Vec::new(),
347        }
348    }
349
350    #[test]
351    fn declaration_route_retains_star_surface_and_origin_hops() {
352        let resolved = vec![
353            ResolvedModule {
354                file_id: FileId(0),
355                re_exports: vec![re_export(FileId(1), "*", "*")],
356                ..Default::default()
357            },
358            ResolvedModule {
359                file_id: FileId(1),
360                exports: vec![source_export()].into(),
361                ..Default::default()
362            },
363        ];
364        let mut modules = vec![
365            module(
366                FileId(0),
367                "/project/inner.ts",
368                Vec::new(),
369                vec![re_export_edge(FileId(1), "*", "*")],
370            ),
371            module(
372                FileId(1),
373                "/project/source.ts",
374                vec![source_symbol()],
375                Vec::new(),
376            ),
377        ];
378        let index = EffectiveExportIndex::build(&resolved);
379
380        let route =
381            effective_declaration_route(&modules, &index, FileId(0), "foo", ExportNamespace::Value)
382                .expect("the star surface must retain a route to its declaration");
383
384        assert_eq!(route.binding.origin_file(), FileId(1));
385        let mut paths = ReferencePathInterner::new(true);
386        let route = route.intern(&mut paths);
387        let path = route
388            .extend_path(None, &mut paths)
389            .expect("tracked routes return one interned path");
390        let finalized = paths.finalize(&mut modules);
391        let ReferencePathNode::Route {
392            graph,
393            start,
394            terminal,
395            start_mechanism,
396            ..
397        } = finalized.paths[path.index()]
398        else {
399            panic!("effective declaration routes use the compact route contract");
400        };
401        assert_eq!(
402            finalized
403                .routes
404                .canonical_hops(graph, start, terminal, start_mechanism),
405            vec![
406                (FileId(1), ModuleLoadMechanism::EsModule),
407                (FileId(0), ModuleLoadMechanism::EsModule),
408            ]
409        );
410    }
411
412    #[test]
413    fn declaration_route_retains_rename_star_and_origin_hops() {
414        let resolved = vec![
415            ResolvedModule {
416                file_id: FileId(0),
417                re_exports: vec![re_export(FileId(1), "foo", "alias")],
418                ..Default::default()
419            },
420            ResolvedModule {
421                file_id: FileId(1),
422                re_exports: vec![re_export(FileId(2), "*", "*")],
423                ..Default::default()
424            },
425            ResolvedModule {
426                file_id: FileId(2),
427                exports: vec![source_export()].into(),
428                ..Default::default()
429            },
430        ];
431        let mut modules = vec![
432            module(
433                FileId(0),
434                "/project/rename.ts",
435                Vec::new(),
436                vec![re_export_edge(FileId(1), "foo", "alias")],
437            ),
438            module(
439                FileId(1),
440                "/project/inner.ts",
441                Vec::new(),
442                vec![re_export_edge(FileId(2), "*", "*")],
443            ),
444            module(
445                FileId(2),
446                "/project/source.ts",
447                vec![source_symbol()],
448                Vec::new(),
449            ),
450        ];
451        let index = EffectiveExportIndex::build(&resolved);
452        let route = effective_declaration_route(
453            &modules,
454            &index,
455            FileId(0),
456            "alias",
457            ExportNamespace::Value,
458        )
459        .expect("the renamed star surface must retain its declaration route");
460
461        assert_eq!(route.binding.origin_file(), FileId(2));
462        let mut paths = ReferencePathInterner::new(true);
463        let route = route.intern(&mut paths);
464        let path = route
465            .extend_path(None, &mut paths)
466            .expect("tracked routes return one interned path");
467        let finalized = paths.finalize(&mut modules);
468        let ReferencePathNode::Route {
469            graph,
470            start,
471            terminal,
472            start_mechanism,
473            ..
474        } = finalized.paths[path.index()]
475        else {
476            panic!("effective declaration routes use the compact route contract");
477        };
478        assert_eq!(
479            finalized
480                .routes
481                .canonical_hops(graph, start, terminal, start_mechanism),
482            vec![
483                (FileId(2), ModuleLoadMechanism::EsModule),
484                (FileId(1), ModuleLoadMechanism::EsModule),
485                (FileId(0), ModuleLoadMechanism::EsModule),
486            ]
487        );
488    }
489}