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