Skip to main content

fallow_graph/graph/
types.rs

1//! Shared graph types: module nodes, re-export edges, export symbols, and references.
2
3use std::cmp::Ordering;
4use std::num::NonZeroU32;
5use std::ops::Range;
6use std::path::PathBuf;
7
8use fallow_types::discover::FileId;
9use fallow_types::extract::{ExportName, ModuleLoadMechanism, VisibilityTag};
10use rustc_hash::FxHashMap;
11
12/// A single module in the graph.
13///
14/// Boolean flags are packed into a `u8` to keep the struct at 96 bytes
15/// (down from 104 with 5 separate `bool` fields), improving cache line
16/// utilization in hot graph traversal loops.
17#[derive(Debug, serde::Serialize, serde::Deserialize)]
18pub struct ModuleNode {
19    /// Unique identifier for this module.
20    pub file_id: FileId,
21    /// Absolute path to the module file.
22    pub path: PathBuf,
23    /// Range into the flat `edges` array.
24    pub edge_range: Range<usize>,
25    /// Exports declared by this module.
26    pub exports: Vec<ExportSymbol>,
27    /// Re-exports from this module (export { x } from './y', export * from './z').
28    pub re_exports: Vec<ReExportEdge>,
29    /// Packed boolean flags (entry point, reachability, CJS).
30    pub(crate) flags: u8,
31}
32
33const FLAG_ENTRY_POINT: u8 = 1 << 0;
34const FLAG_REACHABLE: u8 = 1 << 1;
35const FLAG_RUNTIME_REACHABLE: u8 = 1 << 2;
36const FLAG_TEST_REACHABLE: u8 = 1 << 3;
37const FLAG_CJS_EXPORTS: u8 = 1 << 4;
38
39impl ModuleNode {
40    /// Whether this module is an entry point.
41    #[inline]
42    pub const fn is_entry_point(&self) -> bool {
43        self.flags & FLAG_ENTRY_POINT != 0
44    }
45
46    /// Whether this module is reachable from any entry point.
47    #[inline]
48    pub const fn is_reachable(&self) -> bool {
49        self.flags & FLAG_REACHABLE != 0
50    }
51
52    /// Whether this module is reachable from a runtime/application root.
53    #[inline]
54    pub const fn is_runtime_reachable(&self) -> bool {
55        self.flags & FLAG_RUNTIME_REACHABLE != 0
56    }
57
58    /// Whether this module is reachable from a test root.
59    #[inline]
60    pub const fn is_test_reachable(&self) -> bool {
61        self.flags & FLAG_TEST_REACHABLE != 0
62    }
63
64    /// Whether this module has CJS exports (module.exports / exports.*).
65    #[inline]
66    pub const fn has_cjs_exports(&self) -> bool {
67        self.flags & FLAG_CJS_EXPORTS != 0
68    }
69
70    /// Set whether this module is an entry point.
71    #[inline]
72    pub fn set_entry_point(&mut self, v: bool) {
73        if v {
74            self.flags |= FLAG_ENTRY_POINT;
75        } else {
76            self.flags &= !FLAG_ENTRY_POINT;
77        }
78    }
79
80    /// Set whether this module is reachable from any entry point.
81    #[inline]
82    pub fn set_reachable(&mut self, v: bool) {
83        if v {
84            self.flags |= FLAG_REACHABLE;
85        } else {
86            self.flags &= !FLAG_REACHABLE;
87        }
88    }
89
90    /// Set whether this module is reachable from a runtime/application root.
91    #[inline]
92    pub(crate) fn set_runtime_reachable(&mut self, v: bool) {
93        if v {
94            self.flags |= FLAG_RUNTIME_REACHABLE;
95        } else {
96            self.flags &= !FLAG_RUNTIME_REACHABLE;
97        }
98    }
99
100    /// Set whether this module is reachable from a test root.
101    #[inline]
102    pub(crate) fn set_test_reachable(&mut self, v: bool) {
103        if v {
104            self.flags |= FLAG_TEST_REACHABLE;
105        } else {
106            self.flags &= !FLAG_TEST_REACHABLE;
107        }
108    }
109
110    /// Set whether this module has CJS exports.
111    #[inline]
112    pub fn set_cjs_exports(&mut self, v: bool) {
113        if v {
114            self.flags |= FLAG_CJS_EXPORTS;
115        } else {
116            self.flags &= !FLAG_CJS_EXPORTS;
117        }
118    }
119
120    /// Build flags byte from individual booleans (used by graph construction).
121    #[inline]
122    pub(crate) fn flags_from(
123        is_entry_point: bool,
124        is_runtime_reachable: bool,
125        has_cjs_exports: bool,
126    ) -> u8 {
127        let mut f = 0u8;
128        if is_entry_point {
129            f |= FLAG_ENTRY_POINT;
130        }
131        if is_runtime_reachable {
132            f |= FLAG_RUNTIME_REACHABLE;
133        }
134        if has_cjs_exports {
135            f |= FLAG_CJS_EXPORTS;
136        }
137        f
138    }
139}
140
141/// A re-export edge, tracking which exports are forwarded from which module.
142#[derive(Debug, serde::Serialize, serde::Deserialize)]
143pub struct ReExportEdge {
144    /// The module being re-exported from.
145    pub source_file: FileId,
146    /// The name imported from the source (or "*" for star re-exports).
147    pub imported_name: String,
148    /// The name exported from this module.
149    pub exported_name: String,
150    /// Whether this is a type-only re-export.
151    pub is_type_only: bool,
152    /// Source span of the re-export declaration on this module, used for
153    /// line-number reporting. `(0, 0)` for re-exports synthesized inside the
154    /// graph layer (e.g., `export *` chain propagation, namespace narrowing).
155    #[serde(with = "crate::cache::span_serde")]
156    pub span: oxc_span::Span,
157}
158
159/// An export with reference tracking.
160#[derive(Debug, serde::Serialize, serde::Deserialize)]
161pub struct ExportSymbol {
162    /// The exported name (named or default).
163    pub name: ExportName,
164    /// Whether this is a type-only export.
165    pub is_type_only: bool,
166    /// Whether this export is registered through a runtime side effect at module
167    /// load time (e.g. a Lit `@customElement('tag')` decorator or a
168    /// `customElements.define('tag', ClassRef)` call). The unused-export
169    /// detector treats this as an effective reference.
170    pub is_side_effect_used: bool,
171    /// Visibility tag from JSDoc/TSDoc comment (`@public`, `@internal`, `@alpha`, `@beta`).
172    /// Exports with any visibility tag are never reported as unused.
173    pub visibility: VisibilityTag,
174    /// Human-authored reason on `@expected-unused -- <reason>`, when present.
175    pub expected_unused_reason: Option<String>,
176    /// Source span of the export declaration.
177    #[serde(with = "crate::cache::span_serde")]
178    pub span: oxc_span::Span,
179    /// Which files reference this export.
180    pub references: Vec<SymbolReference>,
181    /// Interned provenance paths parallel to `references`, keyed by reference
182    /// index (issue #2083).
183    ///
184    /// Only populated when the test-reachability plan requires reference
185    /// provenance (a replacement mock exists). It stays empty for every other
186    /// project so the reference list itself remains 16 bytes per entry and the
187    /// side table allocates nothing. Entries can be `None` even when populated:
188    /// legacy reachability stores no path, profiled reachability always does.
189    #[serde(default)]
190    pub reference_paths: Vec<Option<ReferencePathId>>,
191    /// Members of this export (enum members, class members).
192    ///
193    /// `MemberInfo` is a shared `fallow-types` struct whose serde shape is
194    /// serialize-only (its `span` uses `serialize_with` with no matching
195    /// deserializer), so it cannot round-trip through a plain derive. The cache
196    /// routes it through a dedicated lossless mirror in `crate::cache`.
197    #[serde(with = "crate::cache::member_serde")]
198    pub members: Vec<fallow_types::extract::MemberInfo>,
199}
200
201/// A reference to an export from another file.
202#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
203pub struct SymbolReference {
204    /// The file that references this export.
205    pub from_file: FileId,
206    /// How the export is referenced.
207    pub kind: ReferenceKind,
208    /// Byte span of the import statement in the referencing file.
209    /// Used by the LSP to locate references for Code Lens navigation.
210    #[serde(with = "crate::cache::span_serde")]
211    pub import_span: oxc_span::Span,
212}
213
214/// Compact identifier for an interned reference path.
215///
216/// Opaque outside the graph crate; it only appears in the public API as the
217/// element type of [`ExportSymbol::reference_paths`].
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
219pub struct ReferencePathId(NonZeroU32);
220
221/// A symbol reference paired with its optional provenance path while build
222/// passes route it between exports, before it lands in a reference list plus
223/// its provenance side table.
224#[derive(Clone, Copy)]
225pub(crate) struct RoutedReference {
226    pub(crate) reference: SymbolReference,
227    pub(crate) path: Option<ReferencePathId>,
228}
229
230impl ExportSymbol {
231    /// Provenance path recorded for the reference at `index`, when tracked.
232    pub(crate) fn reference_path(&self, index: usize) -> Option<ReferencePathId> {
233        self.reference_paths.get(index).copied().flatten()
234    }
235
236    /// Whether a reference from `from_file` with this exact provenance path is
237    /// already attached.
238    pub(crate) fn has_reference_from(
239        &self,
240        from_file: FileId,
241        path: Option<ReferencePathId>,
242    ) -> bool {
243        self.references
244            .iter()
245            .enumerate()
246            .any(|(index, reference)| {
247                reference.from_file == from_file && self.reference_path(index) == path
248            })
249    }
250
251    /// Attach `reference`, recording `path` in the provenance side table.
252    ///
253    /// The side table stays untouched until the first tracked path arrives, so
254    /// projects without replacement mocks never allocate it.
255    pub(crate) fn push_reference(
256        &mut self,
257        reference: SymbolReference,
258        path: Option<ReferencePathId>,
259    ) {
260        if path.is_some() || !self.reference_paths.is_empty() {
261            self.reference_paths.resize(self.references.len(), None);
262            self.reference_paths.push(path);
263        }
264        self.references.push(reference);
265    }
266
267    /// Iterate references together with their recorded provenance paths.
268    pub(crate) fn routed_references(&self) -> impl Iterator<Item = RoutedReference> + '_ {
269        self.references
270            .iter()
271            .enumerate()
272            .map(|(index, reference)| RoutedReference {
273                reference: *reference,
274                path: self.reference_path(index),
275            })
276    }
277}
278
279/// One conjunctive step in an interned export-reference route.
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
281pub(crate) enum ReferencePathNode {
282    /// One ordinary module-load hop.
283    Hop {
284        /// Previous conjunctive step, or `None` for a direct load.
285        parent: Option<ReferencePathId>,
286        /// Module loaded by this hop.
287        target: FileId,
288        /// Runtime mechanism used by this hop.
289        mechanism: ModuleLoadMechanism,
290    },
291    /// One existential traversal through a compact namespace transition graph.
292    Route {
293        /// Previous conjunctive step, used when a namespace route is followed
294        /// by another namespace segment.
295        parent: Option<ReferencePathId>,
296        /// Canonical transition graph containing `start` and `terminal`.
297        graph: ReferenceRouteGraphId,
298        /// Local graph node where traversal begins.
299        start: ReferenceRouteNodeId,
300        /// Local graph node that must be reachable.
301        terminal: ReferenceRouteNodeId,
302        /// Mechanism used by the consumer to load `start`. `None` means the
303        /// reference source already owns the start module (entry points and
304        /// concatenated route segments).
305        start_mechanism: Option<ModuleLoadMechanism>,
306    },
307}
308
309impl ReferencePathNode {
310    pub(crate) const fn parent(self) -> Option<ReferencePathId> {
311        match self {
312            Self::Hop { parent, .. } | Self::Route { parent, .. } => parent,
313        }
314    }
315
316    fn remap_parent(&mut self, remap: &[ReferencePathId]) {
317        match self {
318            Self::Hop { parent, .. } | Self::Route { parent, .. } => {
319                *parent = parent.map(|path| remap[path.index()]);
320            }
321        }
322    }
323}
324
325/// Build-time identifier for one compact namespace transition graph.
326#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
327pub(crate) struct ReferenceRouteGraphId(pub(crate) u32);
328
329/// Node identifier local to one namespace transition graph.
330#[derive(
331    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
332)]
333pub(crate) struct ReferenceRouteNodeId(pub(crate) u32);
334
335/// One canonical node in a build-time namespace transition graph.
336#[derive(Debug, Clone, PartialEq, Eq, Hash)]
337pub(crate) struct ReferenceRouteNodeSpec {
338    target: FileId,
339    mechanism: ModuleLoadMechanism,
340    successors: Vec<ReferenceRouteNodeId>,
341}
342
343impl ReferenceRouteNodeSpec {
344    pub(crate) fn new(
345        target: FileId,
346        mechanism: ModuleLoadMechanism,
347        mut successors: Vec<ReferenceRouteNodeId>,
348    ) -> Self {
349        successors.sort_unstable_by_key(|successor| successor.0);
350        successors.dedup();
351        Self {
352            target,
353            mechanism,
354            successors,
355        }
356    }
357}
358
359/// Canonical build-time representation of one namespace transition graph.
360#[derive(Debug, Clone, PartialEq, Eq, Hash)]
361pub(crate) struct ReferenceRouteGraphSpec {
362    nodes: Vec<ReferenceRouteNodeSpec>,
363}
364
365impl ReferenceRouteGraphSpec {
366    pub(crate) fn new(nodes: Vec<ReferenceRouteNodeSpec>) -> Self {
367        debug_assert!(nodes.iter().all(|node| {
368            node.successors
369                .iter()
370                .all(|successor| successor.0 < nodes.len() as u32)
371        }));
372        Self { nodes }
373    }
374}
375
376/// Persisted range for one canonical namespace transition graph.
377#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
378pub(crate) struct ReferenceRouteGraph {
379    pub(crate) nodes: Range<u32>,
380}
381
382/// One persisted namespace transition node.
383#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
384pub(crate) struct ReferenceRouteNode {
385    pub(crate) target: FileId,
386    pub(crate) mechanism: ModuleLoadMechanism,
387    pub(crate) successors: Range<u32>,
388}
389
390/// Cache-friendly persisted namespace transition graphs.
391#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
392pub(crate) struct ReferenceRoutes {
393    pub(crate) graphs: Vec<ReferenceRouteGraph>,
394    pub(crate) nodes: Vec<ReferenceRouteNode>,
395    pub(crate) edges: Vec<ReferenceRouteNodeId>,
396}
397
398impl ReferenceRoutes {
399    #[cfg(test)]
400    pub(crate) fn canonical_hops(
401        &self,
402        graph_id: ReferenceRouteGraphId,
403        start: ReferenceRouteNodeId,
404        terminal: ReferenceRouteNodeId,
405        start_mechanism: Option<ModuleLoadMechanism>,
406    ) -> Vec<(FileId, ModuleLoadMechanism)> {
407        let Some(graph) = self.graphs.get(graph_id.0 as usize) else {
408            return Vec::new();
409        };
410        let node_count = graph.nodes.end.saturating_sub(graph.nodes.start) as usize;
411        let start_index = start.0 as usize;
412        let terminal_index = terminal.0 as usize;
413        if start_index >= node_count || terminal_index >= node_count {
414            return Vec::new();
415        }
416
417        let mut predecessor = vec![None; node_count];
418        let mut visited = vec![false; node_count];
419        let mut queue = std::collections::VecDeque::from([start_index]);
420        visited[start_index] = true;
421        while let Some(local_index) = queue.pop_front() {
422            if local_index == terminal_index {
423                break;
424            }
425            let Some(node) = self.nodes.get(graph.nodes.start as usize + local_index) else {
426                return Vec::new();
427            };
428            let Some(successors) = self
429                .edges
430                .get(node.successors.start as usize..node.successors.end as usize)
431            else {
432                return Vec::new();
433            };
434            for successor in successors {
435                let successor_index = successor.0 as usize;
436                if successor_index >= node_count || visited[successor_index] {
437                    continue;
438                }
439                visited[successor_index] = true;
440                predecessor[successor_index] = Some(local_index);
441                queue.push_back(successor_index);
442            }
443        }
444        if !visited[terminal_index] {
445            return Vec::new();
446        }
447
448        let mut hops = Vec::new();
449        let mut current = terminal_index;
450        loop {
451            let node = &self.nodes[graph.nodes.start as usize + current];
452            if current != start_index {
453                hops.push((node.target, node.mechanism));
454            } else {
455                if let Some(mechanism) = start_mechanism {
456                    hops.push((node.target, mechanism));
457                }
458                break;
459            }
460            let Some(parent) = predecessor[current] else {
461                return Vec::new();
462            };
463            current = parent;
464        }
465        hops
466    }
467}
468
469/// Finalized linear paths plus compact namespace transition graphs.
470#[derive(Debug, PartialEq, Eq)]
471pub(crate) struct FinalizedReferencePaths {
472    pub(crate) paths: Vec<ReferencePathNode>,
473    pub(crate) routes: ReferenceRoutes,
474}
475
476/// Build-time interner for shared linked reference paths.
477pub(crate) struct ReferencePathInterner {
478    track_provenance: bool,
479    nodes: Vec<ReferencePathNode>,
480    metadata: Vec<ReferencePathMetadata>,
481    ids: FxHashMap<ReferencePathNode, ReferencePathId>,
482    route_graphs: Vec<ReferenceRouteGraphSpec>,
483    route_graph_ids: FxHashMap<ReferenceRouteGraphSpec, ReferenceRouteGraphId>,
484}
485
486#[derive(Clone, Copy)]
487struct ReferencePathMetadata {
488    depth: usize,
489    hop_target_bounds: Option<(FileId, FileId)>,
490}
491
492impl Default for ReferencePathInterner {
493    fn default() -> Self {
494        Self::new(true)
495    }
496}
497
498impl ReferencePathInterner {
499    pub(crate) fn new(track_provenance: bool) -> Self {
500        Self {
501            track_provenance,
502            nodes: Vec::new(),
503            metadata: Vec::new(),
504            ids: FxHashMap::default(),
505            route_graphs: Vec::new(),
506            route_graph_ids: FxHashMap::default(),
507        }
508    }
509
510    pub(crate) const fn tracks_provenance(&self) -> bool {
511        self.track_provenance
512    }
513
514    /// Intern a direct consumer-to-target path.
515    pub(crate) fn direct(
516        &mut self,
517        target: FileId,
518        mechanism: ModuleLoadMechanism,
519    ) -> Option<ReferencePathId> {
520        self.track_provenance.then(|| {
521            self.intern(ReferencePathNode::Hop {
522                parent: None,
523                target,
524                mechanism,
525            })
526        })
527    }
528
529    /// Append one typed hop to an existing path.
530    pub(crate) fn extend(
531        &mut self,
532        parent: Option<ReferencePathId>,
533        target: FileId,
534        mechanism: ModuleLoadMechanism,
535    ) -> Option<ReferencePathId> {
536        if !self.track_provenance {
537            debug_assert!(parent.is_none());
538            return None;
539        }
540        let Some(parent) = parent else {
541            debug_assert!(false, "tracked reference paths require an interned parent");
542            return None;
543        };
544        let may_contain_target = self
545            .metadata
546            .get(parent.index())
547            .and_then(|metadata| metadata.hop_target_bounds)
548            .is_some_and(|(minimum, maximum)| target.0 >= minimum.0 && target.0 <= maximum.0);
549        if may_contain_target && self.contains_target(parent, target) {
550            return Some(parent);
551        }
552        Some(self.intern(ReferencePathNode::Hop {
553            parent: Some(parent),
554            target,
555            mechanism,
556        }))
557    }
558
559    /// Intern one compact namespace transition graph.
560    pub(crate) fn intern_route_graph(
561        &mut self,
562        graph: ReferenceRouteGraphSpec,
563    ) -> ReferenceRouteGraphId {
564        debug_assert!(self.track_provenance);
565        if let Some(id) = self.route_graph_ids.get(&graph) {
566            return *id;
567        }
568        let id = ReferenceRouteGraphId(self.route_graphs.len() as u32);
569        self.route_graphs.push(graph.clone());
570        self.route_graph_ids.insert(graph, id);
571        id
572    }
573
574    /// Intern one existential traversal through a compact route graph.
575    pub(crate) fn route(
576        &mut self,
577        parent: Option<ReferencePathId>,
578        graph: ReferenceRouteGraphId,
579        start: ReferenceRouteNodeId,
580        terminal: ReferenceRouteNodeId,
581        start_mechanism: Option<ModuleLoadMechanism>,
582    ) -> Option<ReferencePathId> {
583        if !self.track_provenance {
584            return None;
585        }
586        Some(self.intern(ReferencePathNode::Route {
587            parent,
588            graph,
589            start,
590            terminal,
591            start_mechanism,
592        }))
593    }
594
595    fn contains_target(&self, mut path: ReferencePathId, target: FileId) -> bool {
596        loop {
597            let Some(node) = self.nodes.get(path.index()) else {
598                return false;
599            };
600            if let ReferencePathNode::Hop {
601                target: hop_target, ..
602            } = node
603                && *hop_target == target
604            {
605                return true;
606            }
607            let Some(parent) = node.parent() else {
608                return false;
609            };
610            path = parent;
611        }
612    }
613
614    fn intern(&mut self, node: ReferencePathNode) -> ReferencePathId {
615        if let Some(path) = self.ids.get(&node) {
616            return *path;
617        }
618        let path = ReferencePathId::from_index(self.nodes.len());
619        let parent_metadata = node
620            .parent()
621            .and_then(|parent| self.metadata.get(parent.index()).copied());
622        let depth = parent_metadata.map_or(0, |metadata| metadata.depth + 1);
623        let hop_target_bounds = match node {
624            ReferencePathNode::Hop { target, .. } => Some(
625                parent_metadata
626                    .and_then(|metadata| metadata.hop_target_bounds)
627                    .map_or((target, target), |(minimum, maximum)| {
628                        (
629                            FileId(minimum.0.min(target.0)),
630                            FileId(maximum.0.max(target.0)),
631                        )
632                    }),
633            ),
634            ReferencePathNode::Route { .. } => {
635                parent_metadata.and_then(|metadata| metadata.hop_target_bounds)
636            }
637        };
638        self.nodes.push(node);
639        self.metadata.push(ReferencePathMetadata {
640            depth,
641            hop_target_bounds,
642        });
643        self.ids.insert(node, path);
644        path
645    }
646
647    /// Finalize cache-friendly storage and assign canonical IDs.
648    ///
649    /// Paths are ordered depth-by-depth so every parent already has its final
650    /// ID before its children are sorted. This keeps serialized graphs stable
651    /// when equivalent imports or re-exports are discovered in another order.
652    pub(crate) fn finalize(self, modules: &mut [ModuleNode]) -> FinalizedReferencePaths {
653        if self.nodes.is_empty() && self.route_graphs.is_empty() {
654            return FinalizedReferencePaths {
655                paths: Vec::new(),
656                routes: ReferenceRoutes::default(),
657            };
658        }
659
660        let (routes, route_remap) = finalize_route_graphs(&self.route_graphs);
661        let max_depth = self
662            .metadata
663            .iter()
664            .map(|metadata| metadata.depth)
665            .max()
666            .unwrap_or(0);
667
668        let mut paths_by_depth = vec![Vec::new(); max_depth.saturating_add(1)];
669        for (old_index, metadata) in self.metadata.iter().enumerate() {
670            paths_by_depth[metadata.depth].push(old_index);
671        }
672
673        let mut remap = vec![ReferencePathId::from_index(0); self.nodes.len()];
674        let mut finalized = Vec::with_capacity(self.nodes.len());
675        for mut paths in paths_by_depth {
676            paths.sort_unstable_by(|&left, &right| {
677                compare_path_nodes(self.nodes[left], self.nodes[right], &remap, &route_remap)
678            });
679            for old_index in paths {
680                let mut node = self.nodes[old_index];
681                node.remap_parent(&remap);
682                if let ReferencePathNode::Route { graph, .. } = &mut node {
683                    *graph = route_remap[graph.0 as usize];
684                }
685                let canonical = ReferencePathId::from_index(finalized.len());
686                remap[old_index] = canonical;
687                finalized.push(node);
688            }
689        }
690
691        for path in modules
692            .iter_mut()
693            .flat_map(|module| &mut module.exports)
694            .flat_map(|export| &mut export.reference_paths)
695        {
696            if let Some(existing) = *path {
697                *path = Some(remap[existing.index()]);
698            }
699        }
700
701        FinalizedReferencePaths {
702            paths: finalized,
703            routes,
704        }
705    }
706}
707
708fn compare_path_nodes(
709    left: ReferencePathNode,
710    right: ReferencePathNode,
711    path_remap: &[ReferencePathId],
712    route_remap: &[ReferenceRouteGraphId],
713) -> Ordering {
714    let left_parent = left.parent().map(|parent| path_remap[parent.index()].0);
715    let right_parent = right.parent().map(|parent| path_remap[parent.index()].0);
716    left_parent
717        .cmp(&right_parent)
718        .then_with(|| match (left, right) {
719            (
720                ReferencePathNode::Hop {
721                    target: left_target,
722                    mechanism: left_mechanism,
723                    ..
724                },
725                ReferencePathNode::Hop {
726                    target: right_target,
727                    mechanism: right_mechanism,
728                    ..
729                },
730            ) => {
731                (left_target.0, left_mechanism as u8).cmp(&(right_target.0, right_mechanism as u8))
732            }
733            (ReferencePathNode::Hop { .. }, ReferencePathNode::Route { .. }) => Ordering::Less,
734            (ReferencePathNode::Route { .. }, ReferencePathNode::Hop { .. }) => Ordering::Greater,
735            (
736                ReferencePathNode::Route {
737                    graph: left_graph,
738                    start: left_start,
739                    terminal: left_terminal,
740                    start_mechanism: left_mechanism,
741                    ..
742                },
743                ReferencePathNode::Route {
744                    graph: right_graph,
745                    start: right_start,
746                    terminal: right_terminal,
747                    start_mechanism: right_mechanism,
748                    ..
749                },
750            ) => (
751                route_remap[left_graph.0 as usize].0,
752                left_start.0,
753                left_terminal.0,
754                left_mechanism.map(|mechanism| mechanism as u8),
755            )
756                .cmp(&(
757                    route_remap[right_graph.0 as usize].0,
758                    right_start.0,
759                    right_terminal.0,
760                    right_mechanism.map(|mechanism| mechanism as u8),
761                )),
762        })
763}
764
765fn compare_route_graph_specs(
766    left: &ReferenceRouteGraphSpec,
767    right: &ReferenceRouteGraphSpec,
768) -> Ordering {
769    left.nodes.len().cmp(&right.nodes.len()).then_with(|| {
770        left.nodes
771            .iter()
772            .zip(&right.nodes)
773            .find_map(|(left_node, right_node)| {
774                let ordering = (
775                    left_node.target.0,
776                    left_node.mechanism as u8,
777                    &left_node.successors,
778                )
779                    .cmp(&(
780                        right_node.target.0,
781                        right_node.mechanism as u8,
782                        &right_node.successors,
783                    ));
784                (ordering != Ordering::Equal).then_some(ordering)
785            })
786            .unwrap_or(Ordering::Equal)
787    })
788}
789
790fn finalize_route_graphs(
791    graphs: &[ReferenceRouteGraphSpec],
792) -> (ReferenceRoutes, Vec<ReferenceRouteGraphId>) {
793    let mut order: Vec<usize> = (0..graphs.len()).collect();
794    order
795        .sort_unstable_by(|&left, &right| compare_route_graph_specs(&graphs[left], &graphs[right]));
796
797    let mut remap = vec![ReferenceRouteGraphId(0); graphs.len()];
798    let mut finalized = ReferenceRoutes::default();
799    for old_index in order {
800        let graph_id = ReferenceRouteGraphId(finalized.graphs.len() as u32);
801        remap[old_index] = graph_id;
802        let node_start = finalized.nodes.len() as u32;
803        for node in &graphs[old_index].nodes {
804            let edge_start = finalized.edges.len() as u32;
805            finalized.edges.extend_from_slice(&node.successors);
806            finalized.nodes.push(ReferenceRouteNode {
807                target: node.target,
808                mechanism: node.mechanism,
809                successors: edge_start..finalized.edges.len() as u32,
810            });
811        }
812        finalized.graphs.push(ReferenceRouteGraph {
813            nodes: node_start..finalized.nodes.len() as u32,
814        });
815    }
816    (finalized, remap)
817}
818
819impl ReferencePathId {
820    fn from_index(index: usize) -> Self {
821        let Some(encoded) = u32::try_from(index)
822            .ok()
823            .and_then(|index| index.checked_add(1))
824            .and_then(NonZeroU32::new)
825        else {
826            panic!("a process cannot allocate more than u32::MAX reference path nodes");
827        };
828        Self(encoded)
829    }
830
831    pub(crate) const fn index(self) -> usize {
832        (self.0.get() - 1) as usize
833    }
834}
835
836/// How an export is referenced.
837#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
838pub enum ReferenceKind {
839    /// A named import (`import { foo }`).
840    NamedImport,
841    /// A default import (`import Foo`).
842    DefaultImport,
843    /// A namespace import (`import * as ns`).
844    NamespaceImport,
845    /// A re-export (`export { foo } from './bar'`).
846    ReExport,
847    /// A dynamic import (`import('./foo')`).
848    DynamicImport,
849    /// A side-effect import (`import './styles'`).
850    SideEffectImport,
851}
852
853#[cfg(target_pointer_width = "64")]
854const _: () = assert!(std::mem::size_of::<ExportSymbol>() == 136);
855#[cfg(target_pointer_width = "64")]
856const _: () = assert!(std::mem::size_of::<SymbolReference>() == 16);
857#[cfg(target_pointer_width = "64")]
858const _: () = assert!(std::mem::size_of::<ReExportEdge>() == 64);
859#[cfg(all(target_pointer_width = "64", unix))]
860const _: () = assert!(std::mem::size_of::<ModuleNode>() == 96);
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865
866    #[test]
867    fn reference_kind_equality() {
868        assert_eq!(ReferenceKind::NamedImport, ReferenceKind::NamedImport);
869        assert_ne!(ReferenceKind::NamedImport, ReferenceKind::DefaultImport);
870    }
871
872    #[test]
873    fn reference_kind_all_variants_are_distinct() {
874        let all = [
875            ReferenceKind::NamedImport,
876            ReferenceKind::DefaultImport,
877            ReferenceKind::NamespaceImport,
878            ReferenceKind::ReExport,
879            ReferenceKind::DynamicImport,
880            ReferenceKind::SideEffectImport,
881        ];
882        for (i, a) in all.iter().enumerate() {
883            for (j, b) in all.iter().enumerate() {
884                if i == j {
885                    assert_eq!(a, b);
886                } else {
887                    assert_ne!(a, b);
888                }
889            }
890        }
891    }
892
893    #[test]
894    fn reference_kind_copy() {
895        let original = ReferenceKind::NamespaceImport;
896        let copied = original;
897        assert_eq!(original, copied);
898    }
899
900    #[test]
901    fn reference_kind_debug_format() {
902        let kind = ReferenceKind::DynamicImport;
903        let debug_str = format!("{kind:?}");
904        assert_eq!(debug_str, "DynamicImport");
905    }
906
907    fn module_with_reference_paths(paths: &[Option<ReferencePathId>]) -> ModuleNode {
908        ModuleNode {
909            file_id: FileId(0),
910            path: PathBuf::from("/project/source.ts"),
911            edge_range: 0..0,
912            exports: vec![ExportSymbol {
913                name: ExportName::Named("value".to_string()),
914                is_type_only: false,
915                is_side_effect_used: false,
916                visibility: VisibilityTag::None,
917                expected_unused_reason: None,
918                span: oxc_span::Span::default(),
919                references: paths
920                    .iter()
921                    .map(|_| SymbolReference {
922                        from_file: FileId(0),
923                        kind: ReferenceKind::NamedImport,
924                        import_span: oxc_span::Span::default(),
925                    })
926                    .collect(),
927                reference_paths: paths.to_vec(),
928                members: Vec::new(),
929            }],
930            re_exports: Vec::new(),
931            flags: 0,
932        }
933    }
934
935    #[test]
936    fn reference_path_metadata_tracks_exact_depth_and_hop_bounds() {
937        let mut interner = ReferencePathInterner::default();
938        let root = interner
939            .direct(FileId(10), ModuleLoadMechanism::EsModule)
940            .expect("tracked interner must return a path");
941        let lower = interner
942            .extend(Some(root), FileId(5), ModuleLoadMechanism::EsModule)
943            .expect("tracked interner must extend a path");
944        let upper = interner
945            .extend(Some(lower), FileId(20), ModuleLoadMechanism::EsModule)
946            .expect("tracked interner must extend a path");
947
948        assert_eq!(interner.metadata[root.index()].depth, 0);
949        assert_eq!(
950            interner.metadata[lower.index()].hop_target_bounds,
951            Some((FileId(5), FileId(10)))
952        );
953        assert_eq!(interner.metadata[upper.index()].depth, 2);
954        assert_eq!(
955            interner.metadata[upper.index()].hop_target_bounds,
956            Some((FileId(5), FileId(20)))
957        );
958
959        let repeated = interner.extend(Some(upper), FileId(10), ModuleLoadMechanism::EsModule);
960        assert_eq!(repeated, Some(upper));
961    }
962
963    #[test]
964    fn finalized_reference_paths_are_independent_of_interning_order() {
965        let mut first = ReferencePathInterner::default();
966        let first_parent = first.direct(FileId(1), ModuleLoadMechanism::EsModule);
967        let first_direct = first.direct(FileId(2), ModuleLoadMechanism::CommonJsRequire);
968        let first_chain = first.extend(first_parent, FileId(3), ModuleLoadMechanism::EsModule);
969        let mut first_modules = vec![module_with_reference_paths(&[first_direct, first_chain])];
970        let first_nodes = first.finalize(&mut first_modules);
971
972        let mut second = ReferencePathInterner::default();
973        let second_direct = second.direct(FileId(2), ModuleLoadMechanism::CommonJsRequire);
974        let second_parent = second.direct(FileId(1), ModuleLoadMechanism::EsModule);
975        let second_chain = second.extend(second_parent, FileId(3), ModuleLoadMechanism::EsModule);
976        let mut second_modules = vec![module_with_reference_paths(&[second_direct, second_chain])];
977        let second_nodes = second.finalize(&mut second_modules);
978
979        assert_eq!(first_nodes, second_nodes);
980        assert_eq!(
981            first_modules[0].exports[0].reference_paths,
982            second_modules[0].exports[0].reference_paths
983        );
984    }
985
986    fn two_hop_route(first: FileId, second: FileId) -> ReferenceRouteGraphSpec {
987        ReferenceRouteGraphSpec::new(vec![
988            ReferenceRouteNodeSpec::new(
989                first,
990                ModuleLoadMechanism::EsModule,
991                vec![ReferenceRouteNodeId(1)],
992            ),
993            ReferenceRouteNodeSpec::new(second, ModuleLoadMechanism::EsModule, Vec::new()),
994        ])
995    }
996
997    #[test]
998    fn finalized_reference_routes_are_independent_of_interning_order() {
999        let route_a = two_hop_route(FileId(1), FileId(2));
1000        let route_b = two_hop_route(FileId(3), FileId(4));
1001
1002        let mut first = ReferencePathInterner::default();
1003        let first_a = first.intern_route_graph(route_a.clone());
1004        let first_b = first.intern_route_graph(route_b.clone());
1005        let first_b_path = first.route(
1006            None,
1007            first_b,
1008            ReferenceRouteNodeId(0),
1009            ReferenceRouteNodeId(1),
1010            Some(ModuleLoadMechanism::CommonJsRequire),
1011        );
1012        let first_a_path = first.route(
1013            None,
1014            first_a,
1015            ReferenceRouteNodeId(0),
1016            ReferenceRouteNodeId(1),
1017            Some(ModuleLoadMechanism::EsModule),
1018        );
1019        let mut first_modules = vec![module_with_reference_paths(&[first_b_path, first_a_path])];
1020        let first_paths = first.finalize(&mut first_modules);
1021
1022        let mut second = ReferencePathInterner::default();
1023        let second_b = second.intern_route_graph(route_b);
1024        let second_a = second.intern_route_graph(route_a);
1025        let second_b_path = second.route(
1026            None,
1027            second_b,
1028            ReferenceRouteNodeId(0),
1029            ReferenceRouteNodeId(1),
1030            Some(ModuleLoadMechanism::CommonJsRequire),
1031        );
1032        let second_a_path = second.route(
1033            None,
1034            second_a,
1035            ReferenceRouteNodeId(0),
1036            ReferenceRouteNodeId(1),
1037            Some(ModuleLoadMechanism::EsModule),
1038        );
1039        let mut second_modules = vec![module_with_reference_paths(&[second_b_path, second_a_path])];
1040        let second_paths = second.finalize(&mut second_modules);
1041
1042        assert_eq!(first_paths, second_paths);
1043        assert_eq!(
1044            first_modules[0].exports[0].reference_paths,
1045            second_modules[0].exports[0].reference_paths
1046        );
1047    }
1048
1049    #[test]
1050    fn symbol_reference_construction() {
1051        let reference = SymbolReference {
1052            from_file: FileId(42),
1053            kind: ReferenceKind::NamedImport,
1054            import_span: oxc_span::Span::new(10, 30),
1055        };
1056        assert_eq!(reference.from_file, FileId(42));
1057        assert_eq!(reference.kind, ReferenceKind::NamedImport);
1058        assert_eq!(reference.import_span.start, 10);
1059        assert_eq!(reference.import_span.end, 30);
1060    }
1061
1062    #[test]
1063    fn symbol_reference_copy_preserves_all_fields() {
1064        let reference = SymbolReference {
1065            from_file: FileId(7),
1066            kind: ReferenceKind::ReExport,
1067            import_span: oxc_span::Span::new(5, 25),
1068        };
1069        let copied = reference;
1070        assert_eq!(copied.from_file, reference.from_file);
1071        assert_eq!(copied.kind, reference.kind);
1072        assert_eq!(copied.import_span.start, reference.import_span.start);
1073        assert_eq!(copied.import_span.end, reference.import_span.end);
1074    }
1075
1076    #[test]
1077    fn re_export_edge_construction() {
1078        let edge = ReExportEdge {
1079            source_file: FileId(3),
1080            imported_name: "*".to_string(),
1081            exported_name: "*".to_string(),
1082            is_type_only: false,
1083            span: oxc_span::Span::default(),
1084        };
1085        assert_eq!(edge.source_file, FileId(3));
1086        assert_eq!(edge.imported_name, "*");
1087        assert_eq!(edge.exported_name, "*");
1088        assert!(!edge.is_type_only);
1089    }
1090
1091    #[test]
1092    fn re_export_edge_type_only() {
1093        let edge = ReExportEdge {
1094            source_file: FileId(1),
1095            imported_name: "MyType".to_string(),
1096            exported_name: "MyType".to_string(),
1097            is_type_only: true,
1098            span: oxc_span::Span::default(),
1099        };
1100        assert!(edge.is_type_only);
1101    }
1102
1103    #[test]
1104    fn re_export_edge_renamed() {
1105        let edge = ReExportEdge {
1106            source_file: FileId(2),
1107            imported_name: "internal".to_string(),
1108            exported_name: "public".to_string(),
1109            is_type_only: false,
1110            span: oxc_span::Span::default(),
1111        };
1112        assert_ne!(edge.imported_name, edge.exported_name);
1113        assert_eq!(edge.imported_name, "internal");
1114        assert_eq!(edge.exported_name, "public");
1115    }
1116
1117    #[test]
1118    fn export_symbol_named() {
1119        let sym = ExportSymbol {
1120            name: ExportName::Named("myFunction".to_string()),
1121            is_type_only: false,
1122            is_side_effect_used: false,
1123            visibility: VisibilityTag::None,
1124            expected_unused_reason: None,
1125            span: oxc_span::Span::new(0, 50),
1126            references: vec![],
1127            reference_paths: Vec::new(),
1128            members: vec![],
1129        };
1130        assert!(matches!(sym.name, ExportName::Named(ref n) if n == "myFunction"));
1131        assert!(!sym.is_type_only);
1132        assert_eq!(sym.visibility, VisibilityTag::None);
1133    }
1134
1135    #[test]
1136    fn export_symbol_default() {
1137        let sym = ExportSymbol {
1138            name: ExportName::Default,
1139            is_type_only: false,
1140            is_side_effect_used: false,
1141            visibility: VisibilityTag::None,
1142            expected_unused_reason: None,
1143            span: oxc_span::Span::new(0, 20),
1144            references: vec![],
1145            reference_paths: Vec::new(),
1146            members: vec![],
1147        };
1148        assert!(matches!(sym.name, ExportName::Default));
1149    }
1150
1151    #[test]
1152    fn export_symbol_public_tag() {
1153        let sym = ExportSymbol {
1154            name: ExportName::Named("api".to_string()),
1155            is_type_only: false,
1156            is_side_effect_used: false,
1157            visibility: VisibilityTag::Public,
1158            expected_unused_reason: None,
1159            span: oxc_span::Span::new(0, 10),
1160            references: vec![],
1161            reference_paths: Vec::new(),
1162            members: vec![],
1163        };
1164        assert_eq!(sym.visibility, VisibilityTag::Public);
1165    }
1166
1167    #[test]
1168    fn export_symbol_type_only() {
1169        let sym = ExportSymbol {
1170            name: ExportName::Named("MyInterface".to_string()),
1171            is_type_only: true,
1172            is_side_effect_used: false,
1173            visibility: VisibilityTag::None,
1174            expected_unused_reason: None,
1175            span: oxc_span::Span::new(0, 30),
1176            references: vec![],
1177            reference_paths: Vec::new(),
1178            members: vec![],
1179        };
1180        assert!(sym.is_type_only);
1181    }
1182
1183    #[test]
1184    fn export_symbol_with_references() {
1185        let sym = ExportSymbol {
1186            name: ExportName::Named("helper".to_string()),
1187            is_type_only: false,
1188            is_side_effect_used: false,
1189            visibility: VisibilityTag::None,
1190            expected_unused_reason: None,
1191            span: oxc_span::Span::new(0, 20),
1192            references: vec![
1193                SymbolReference {
1194                    from_file: FileId(1),
1195                    kind: ReferenceKind::NamedImport,
1196                    import_span: oxc_span::Span::new(0, 10),
1197                },
1198                SymbolReference {
1199                    from_file: FileId(2),
1200                    kind: ReferenceKind::ReExport,
1201                    import_span: oxc_span::Span::new(5, 15),
1202                },
1203            ],
1204            reference_paths: vec![
1205                Some(ReferencePathId::from_index(0)),
1206                Some(ReferencePathId::from_index(1)),
1207            ],
1208            members: vec![],
1209        };
1210        assert_eq!(sym.references.len(), 2);
1211        assert_eq!(sym.references[0].from_file, FileId(1));
1212        assert_eq!(sym.references[1].kind, ReferenceKind::ReExport);
1213    }
1214
1215    #[test]
1216    fn push_reference_without_paths_never_allocates_the_side_table() {
1217        let mut export = ExportSymbol {
1218            name: ExportName::Named("value".to_string()),
1219            is_type_only: false,
1220            is_side_effect_used: false,
1221            visibility: VisibilityTag::None,
1222            expected_unused_reason: None,
1223            span: oxc_span::Span::default(),
1224            references: Vec::new(),
1225            reference_paths: Vec::new(),
1226            members: Vec::new(),
1227        };
1228        for id in 0..3 {
1229            export.push_reference(
1230                SymbolReference {
1231                    from_file: FileId(id),
1232                    kind: ReferenceKind::NamedImport,
1233                    import_span: oxc_span::Span::default(),
1234                },
1235                None,
1236            );
1237        }
1238        assert_eq!(export.references.len(), 3);
1239        assert!(export.reference_paths.is_empty());
1240        assert_eq!(export.reference_paths.capacity(), 0);
1241        assert_eq!(export.reference_path(1), None);
1242        assert!(export.has_reference_from(FileId(1), None));
1243        assert!(!export.has_reference_from(FileId(9), None));
1244    }
1245
1246    #[test]
1247    fn push_reference_backfills_the_side_table_on_the_first_tracked_path() {
1248        let mut export = ExportSymbol {
1249            name: ExportName::Named("value".to_string()),
1250            is_type_only: false,
1251            is_side_effect_used: false,
1252            visibility: VisibilityTag::None,
1253            expected_unused_reason: None,
1254            span: oxc_span::Span::default(),
1255            references: Vec::new(),
1256            reference_paths: Vec::new(),
1257            members: Vec::new(),
1258        };
1259        let reference = SymbolReference {
1260            from_file: FileId(0),
1261            kind: ReferenceKind::NamedImport,
1262            import_span: oxc_span::Span::default(),
1263        };
1264        export.push_reference(reference, None);
1265        let tracked = ReferencePathId::from_index(4);
1266        export.push_reference(reference, Some(tracked));
1267        export.push_reference(reference, None);
1268
1269        assert_eq!(export.reference_paths, vec![None, Some(tracked), None]);
1270        assert_eq!(export.reference_path(0), None);
1271        assert_eq!(export.reference_path(1), Some(tracked));
1272        assert!(export.has_reference_from(FileId(0), Some(tracked)));
1273        assert!(!export.has_reference_from(FileId(0), Some(ReferencePathId::from_index(7))));
1274    }
1275
1276    #[test]
1277    fn module_node_construction() {
1278        let mut node = ModuleNode {
1279            file_id: FileId(0),
1280            path: PathBuf::from("/project/src/index.ts"),
1281            edge_range: 0..5,
1282            exports: vec![],
1283            re_exports: vec![],
1284            flags: ModuleNode::flags_from(true, true, false),
1285        };
1286        node.set_reachable(true);
1287        assert_eq!(node.file_id, FileId(0));
1288        assert!(node.is_entry_point());
1289        assert!(node.is_reachable());
1290        assert!(node.is_runtime_reachable());
1291        assert!(!node.is_test_reachable());
1292        assert!(!node.has_cjs_exports());
1293        assert_eq!(node.edge_range, 0..5);
1294    }
1295
1296    #[test]
1297    fn module_node_non_entry_unreachable() {
1298        let node = ModuleNode {
1299            file_id: FileId(5),
1300            path: PathBuf::from("/project/src/orphan.ts"),
1301            edge_range: 0..0,
1302            exports: vec![],
1303            re_exports: vec![],
1304            flags: ModuleNode::flags_from(false, false, false),
1305        };
1306        assert!(!node.is_entry_point());
1307        assert!(!node.is_reachable());
1308        assert!(!node.is_runtime_reachable());
1309        assert!(!node.is_test_reachable());
1310        assert!(node.edge_range.is_empty());
1311    }
1312
1313    #[test]
1314    fn module_node_cjs_exports() {
1315        let mut node = ModuleNode {
1316            file_id: FileId(2),
1317            path: PathBuf::from("/project/lib/legacy.js"),
1318            edge_range: 3..7,
1319            exports: vec![],
1320            re_exports: vec![],
1321            flags: ModuleNode::flags_from(false, true, true),
1322        };
1323        node.set_reachable(true);
1324        assert!(node.has_cjs_exports());
1325        assert!(node.is_runtime_reachable());
1326        assert_eq!(node.edge_range.len(), 4);
1327    }
1328
1329    #[test]
1330    fn module_node_with_exports_and_re_exports() {
1331        let node = ModuleNode {
1332            file_id: FileId(1),
1333            path: PathBuf::from("/project/src/barrel.ts"),
1334            edge_range: 0..3,
1335            exports: vec![ExportSymbol {
1336                name: ExportName::Named("localFn".to_string()),
1337                is_type_only: false,
1338                is_side_effect_used: false,
1339                visibility: VisibilityTag::None,
1340                expected_unused_reason: None,
1341                span: oxc_span::Span::new(0, 20),
1342                references: vec![],
1343                reference_paths: Vec::new(),
1344                members: vec![],
1345            }],
1346            re_exports: vec![ReExportEdge {
1347                source_file: FileId(2),
1348                imported_name: "*".to_string(),
1349                exported_name: "*".to_string(),
1350                is_type_only: false,
1351                span: oxc_span::Span::default(),
1352            }],
1353            flags: ModuleNode::flags_from(false, true, false),
1354        };
1355        assert_eq!(node.exports.len(), 1);
1356        assert_eq!(node.re_exports.len(), 1);
1357        assert_eq!(node.re_exports[0].source_file, FileId(2));
1358    }
1359}