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            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}