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                if let Some(mechanism) = start_mechanism {
514                    hops.push((node.target, mechanism));
515                }
516                break;
517            }
518            hops.push((node.target, node.mechanism));
519            let Some(parent) = predecessor[current] else {
520                return Vec::new();
521            };
522            current = parent;
523        }
524        hops
525    }
526}
527
528/// Finalized linear paths plus compact namespace transition graphs.
529#[derive(Debug, PartialEq, Eq)]
530pub(crate) struct FinalizedReferencePaths {
531    pub(crate) paths: Vec<ReferencePathNode>,
532    pub(crate) routes: ReferenceRoutes,
533}
534
535/// Build-time interner for shared linked reference paths.
536pub(crate) struct ReferencePathInterner {
537    track_provenance: bool,
538    nodes: Vec<ReferencePathNode>,
539    metadata: Vec<ReferencePathMetadata>,
540    ids: FxHashMap<ReferencePathNode, ReferencePathId>,
541    route_graphs: Vec<ReferenceRouteGraphSpec>,
542    route_graph_ids: FxHashMap<ReferenceRouteGraphSpec, ReferenceRouteGraphId>,
543}
544
545#[derive(Clone, Copy)]
546struct ReferencePathMetadata {
547    depth: usize,
548    hop_target_bounds: Option<(FileId, FileId)>,
549}
550
551impl Default for ReferencePathInterner {
552    fn default() -> Self {
553        Self::new(true)
554    }
555}
556
557impl ReferencePathInterner {
558    pub(crate) fn new(track_provenance: bool) -> Self {
559        Self {
560            track_provenance,
561            nodes: Vec::new(),
562            metadata: Vec::new(),
563            ids: FxHashMap::default(),
564            route_graphs: Vec::new(),
565            route_graph_ids: FxHashMap::default(),
566        }
567    }
568
569    pub(crate) const fn tracks_provenance(&self) -> bool {
570        self.track_provenance
571    }
572
573    /// Intern a direct consumer-to-target path.
574    pub(crate) fn direct(
575        &mut self,
576        target: FileId,
577        mechanism: ModuleLoadMechanism,
578    ) -> Option<ReferencePathId> {
579        self.track_provenance.then(|| {
580            self.intern(ReferencePathNode::Hop {
581                parent: None,
582                target,
583                mechanism,
584            })
585        })
586    }
587
588    /// Append one typed hop to an existing path.
589    pub(crate) fn extend(
590        &mut self,
591        parent: Option<ReferencePathId>,
592        target: FileId,
593        mechanism: ModuleLoadMechanism,
594    ) -> Option<ReferencePathId> {
595        if !self.track_provenance {
596            debug_assert!(parent.is_none());
597            return None;
598        }
599        let Some(parent) = parent else {
600            debug_assert!(false, "tracked reference paths require an interned parent");
601            return None;
602        };
603        let may_contain_target = self
604            .metadata
605            .get(parent.index())
606            .and_then(|metadata| metadata.hop_target_bounds)
607            .is_some_and(|(minimum, maximum)| target.0 >= minimum.0 && target.0 <= maximum.0);
608        if may_contain_target && self.contains_target(parent, target) {
609            return Some(parent);
610        }
611        Some(self.intern(ReferencePathNode::Hop {
612            parent: Some(parent),
613            target,
614            mechanism,
615        }))
616    }
617
618    /// Intern one compact namespace transition graph.
619    pub(crate) fn intern_route_graph(
620        &mut self,
621        graph: ReferenceRouteGraphSpec,
622    ) -> ReferenceRouteGraphId {
623        debug_assert!(self.track_provenance);
624        if let Some(id) = self.route_graph_ids.get(&graph) {
625            return *id;
626        }
627        let id = ReferenceRouteGraphId(self.route_graphs.len() as u32);
628        self.route_graphs.push(graph.clone());
629        self.route_graph_ids.insert(graph, id);
630        id
631    }
632
633    /// Intern one existential traversal through a compact route graph.
634    pub(crate) fn route(
635        &mut self,
636        parent: Option<ReferencePathId>,
637        graph: ReferenceRouteGraphId,
638        start: ReferenceRouteNodeId,
639        terminal: ReferenceRouteNodeId,
640        start_mechanism: Option<ModuleLoadMechanism>,
641    ) -> Option<ReferencePathId> {
642        if !self.track_provenance {
643            return None;
644        }
645        Some(self.intern(ReferencePathNode::Route {
646            parent,
647            graph,
648            start,
649            terminal,
650            start_mechanism,
651        }))
652    }
653
654    fn contains_target(&self, mut path: ReferencePathId, target: FileId) -> bool {
655        loop {
656            let Some(node) = self.nodes.get(path.index()) else {
657                return false;
658            };
659            if let ReferencePathNode::Hop {
660                target: hop_target, ..
661            } = node
662                && *hop_target == target
663            {
664                return true;
665            }
666            let Some(parent) = node.parent() else {
667                return false;
668            };
669            path = parent;
670        }
671    }
672
673    fn intern(&mut self, node: ReferencePathNode) -> ReferencePathId {
674        if let Some(path) = self.ids.get(&node) {
675            return *path;
676        }
677        let path = ReferencePathId::from_index(self.nodes.len());
678        let parent_metadata = node
679            .parent()
680            .and_then(|parent| self.metadata.get(parent.index()).copied());
681        let depth = parent_metadata.map_or(0, |metadata| metadata.depth + 1);
682        let hop_target_bounds = match node {
683            ReferencePathNode::Hop { target, .. } => Some(
684                parent_metadata
685                    .and_then(|metadata| metadata.hop_target_bounds)
686                    .map_or((target, target), |(minimum, maximum)| {
687                        (
688                            FileId(minimum.0.min(target.0)),
689                            FileId(maximum.0.max(target.0)),
690                        )
691                    }),
692            ),
693            ReferencePathNode::Route { .. } => {
694                parent_metadata.and_then(|metadata| metadata.hop_target_bounds)
695            }
696        };
697        self.nodes.push(node);
698        self.metadata.push(ReferencePathMetadata {
699            depth,
700            hop_target_bounds,
701        });
702        self.ids.insert(node, path);
703        path
704    }
705
706    /// Finalize cache-friendly storage and assign canonical IDs.
707    ///
708    /// Paths are ordered depth-by-depth so every parent already has its final
709    /// ID before its children are sorted. This keeps serialized graphs stable
710    /// when equivalent imports or re-exports are discovered in another order.
711    pub(crate) fn finalize(self, modules: &mut [ModuleNode]) -> FinalizedReferencePaths {
712        if self.nodes.is_empty() && self.route_graphs.is_empty() {
713            return FinalizedReferencePaths {
714                paths: Vec::new(),
715                routes: ReferenceRoutes::default(),
716            };
717        }
718
719        let (routes, route_remap) = finalize_route_graphs(&self.route_graphs);
720        let max_depth = self
721            .metadata
722            .iter()
723            .map(|metadata| metadata.depth)
724            .max()
725            .unwrap_or(0);
726
727        let mut paths_by_depth = vec![Vec::new(); max_depth.saturating_add(1)];
728        for (old_index, metadata) in self.metadata.iter().enumerate() {
729            paths_by_depth[metadata.depth].push(old_index);
730        }
731
732        let mut remap = vec![ReferencePathId::from_index(0); self.nodes.len()];
733        let mut finalized = Vec::with_capacity(self.nodes.len());
734        for mut paths in paths_by_depth {
735            paths.sort_unstable_by(|&left, &right| {
736                compare_path_nodes(self.nodes[left], self.nodes[right], &remap, &route_remap)
737            });
738            for old_index in paths {
739                let mut node = self.nodes[old_index];
740                node.remap_parent(&remap);
741                if let ReferencePathNode::Route { graph, .. } = &mut node {
742                    *graph = route_remap[graph.0 as usize];
743                }
744                let canonical = ReferencePathId::from_index(finalized.len());
745                remap[old_index] = canonical;
746                finalized.push(node);
747            }
748        }
749
750        for path in modules
751            .iter_mut()
752            .flat_map(|module| &mut module.exports)
753            .flat_map(|export| &mut export.reference_paths)
754        {
755            if let Some(existing) = *path {
756                *path = Some(remap[existing.index()]);
757            }
758        }
759
760        FinalizedReferencePaths {
761            paths: finalized,
762            routes,
763        }
764    }
765}
766
767fn compare_path_nodes(
768    left: ReferencePathNode,
769    right: ReferencePathNode,
770    path_remap: &[ReferencePathId],
771    route_remap: &[ReferenceRouteGraphId],
772) -> Ordering {
773    let left_parent = left.parent().map(|parent| path_remap[parent.index()].0);
774    let right_parent = right.parent().map(|parent| path_remap[parent.index()].0);
775    left_parent
776        .cmp(&right_parent)
777        .then_with(|| match (left, right) {
778            (
779                ReferencePathNode::Hop {
780                    target: left_target,
781                    mechanism: left_mechanism,
782                    ..
783                },
784                ReferencePathNode::Hop {
785                    target: right_target,
786                    mechanism: right_mechanism,
787                    ..
788                },
789            ) => {
790                (left_target.0, left_mechanism as u8).cmp(&(right_target.0, right_mechanism as u8))
791            }
792            (ReferencePathNode::Hop { .. }, ReferencePathNode::Route { .. }) => Ordering::Less,
793            (ReferencePathNode::Route { .. }, ReferencePathNode::Hop { .. }) => Ordering::Greater,
794            (
795                ReferencePathNode::Route {
796                    graph: left_graph,
797                    start: left_start,
798                    terminal: left_terminal,
799                    start_mechanism: left_mechanism,
800                    ..
801                },
802                ReferencePathNode::Route {
803                    graph: right_graph,
804                    start: right_start,
805                    terminal: right_terminal,
806                    start_mechanism: right_mechanism,
807                    ..
808                },
809            ) => (
810                route_remap[left_graph.0 as usize].0,
811                left_start.0,
812                left_terminal.0,
813                left_mechanism.map(|mechanism| mechanism as u8),
814            )
815                .cmp(&(
816                    route_remap[right_graph.0 as usize].0,
817                    right_start.0,
818                    right_terminal.0,
819                    right_mechanism.map(|mechanism| mechanism as u8),
820                )),
821        })
822}
823
824fn compare_route_graph_specs(
825    left: &ReferenceRouteGraphSpec,
826    right: &ReferenceRouteGraphSpec,
827) -> Ordering {
828    left.nodes.len().cmp(&right.nodes.len()).then_with(|| {
829        left.nodes
830            .iter()
831            .zip(&right.nodes)
832            .find_map(|(left_node, right_node)| {
833                let ordering = (
834                    left_node.target.0,
835                    left_node.mechanism as u8,
836                    &left_node.successors,
837                )
838                    .cmp(&(
839                        right_node.target.0,
840                        right_node.mechanism as u8,
841                        &right_node.successors,
842                    ));
843                (ordering != Ordering::Equal).then_some(ordering)
844            })
845            .unwrap_or(Ordering::Equal)
846    })
847}
848
849fn finalize_route_graphs(
850    graphs: &[ReferenceRouteGraphSpec],
851) -> (ReferenceRoutes, Vec<ReferenceRouteGraphId>) {
852    let mut order: Vec<usize> = (0..graphs.len()).collect();
853    order
854        .sort_unstable_by(|&left, &right| compare_route_graph_specs(&graphs[left], &graphs[right]));
855
856    let mut remap = vec![ReferenceRouteGraphId(0); graphs.len()];
857    let mut finalized = ReferenceRoutes::default();
858    for old_index in order {
859        let graph_id = ReferenceRouteGraphId(finalized.graphs.len() as u32);
860        remap[old_index] = graph_id;
861        let node_start = finalized.nodes.len() as u32;
862        for node in &graphs[old_index].nodes {
863            let edge_start = finalized.edges.len() as u32;
864            finalized.edges.extend_from_slice(&node.successors);
865            finalized.nodes.push(ReferenceRouteNode {
866                target: node.target,
867                mechanism: node.mechanism,
868                successors: edge_start..finalized.edges.len() as u32,
869            });
870        }
871        finalized.graphs.push(ReferenceRouteGraph {
872            nodes: node_start..finalized.nodes.len() as u32,
873        });
874    }
875    (finalized, remap)
876}
877
878impl ReferencePathId {
879    fn from_index(index: usize) -> Self {
880        let Some(encoded) = u32::try_from(index)
881            .ok()
882            .and_then(|index| index.checked_add(1))
883            .and_then(NonZeroU32::new)
884        else {
885            panic!("a process cannot allocate more than u32::MAX reference path nodes");
886        };
887        Self(encoded)
888    }
889
890    pub(crate) const fn index(self) -> usize {
891        (self.0.get() - 1) as usize
892    }
893}
894
895/// How an export is referenced.
896#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
897pub enum ReferenceKind {
898    /// A named import (`import { foo }`).
899    NamedImport,
900    /// A default import (`import Foo`).
901    DefaultImport,
902    /// A namespace import (`import * as ns`).
903    NamespaceImport,
904    /// A re-export (`export { foo } from './bar'`).
905    ReExport,
906    /// A dynamic import (`import('./foo')`).
907    DynamicImport,
908    /// A side-effect import (`import './styles'`).
909    SideEffectImport,
910}
911
912#[cfg(target_pointer_width = "64")]
913const _: () = assert!(std::mem::size_of::<ExportSymbol>() == 152);
914#[cfg(target_pointer_width = "64")]
915const _: () = assert!(std::mem::size_of::<SymbolReference>() == 16);
916#[cfg(target_pointer_width = "64")]
917const _: () = assert!(std::mem::size_of::<ReExportEdge>() == 64);
918#[cfg(all(target_pointer_width = "64", unix))]
919const _: () = assert!(std::mem::size_of::<ModuleNode>() == 96);
920
921#[cfg(test)]
922mod tests {
923    use super::*;
924    use crate::graph::ExportNamespace;
925
926    fn module_with_reference_paths(paths: &[Option<ReferencePathId>]) -> ModuleNode {
927        ModuleNode {
928            file_id: FileId(0),
929            path: PathBuf::from("/project/source.ts"),
930            edge_range: 0..0,
931            exports: vec![ExportSymbol {
932                name: ExportName::Named("value".to_string()),
933                is_type_only: false,
934                is_side_effect_used: false,
935                visibility: VisibilityTag::None,
936                expected_unused_reason: None,
937                span: oxc_span::Span::default(),
938                references: paths
939                    .iter()
940                    .map(|_| SymbolReference {
941                        from_file: FileId(0),
942                        kind: ReferenceKind::NamedImport,
943                        namespace: ExportNamespace::Value,
944                        import_span: oxc_span::Span::default(),
945                    })
946                    .collect(),
947                reference_paths: paths.to_vec(),
948                members: Vec::new(),
949                deprecated: false,
950                deprecated_reason: None,
951            }],
952            re_exports: Vec::new(),
953            flags: 0,
954        }
955    }
956
957    fn export_with_reference_files(files: &[u32]) -> ExportSymbol {
958        ExportSymbol {
959            name: ExportName::Named("value".to_string()),
960            is_type_only: false,
961            is_side_effect_used: false,
962            visibility: VisibilityTag::None,
963            expected_unused_reason: None,
964            span: oxc_span::Span::default(),
965            references: files
966                .iter()
967                .map(|file| SymbolReference {
968                    from_file: FileId(*file),
969                    kind: ReferenceKind::NamedImport,
970                    namespace: ExportNamespace::Value,
971                    import_span: oxc_span::Span::default(),
972                })
973                .collect(),
974            reference_paths: Vec::new(),
975            members: Vec::new(),
976            deprecated: false,
977            deprecated_reason: None,
978        }
979    }
980
981    #[test]
982    fn physical_references_deduplicate_small_and_large_sets() {
983        let small = export_with_reference_files(&[0, 1, 2, 3, 4, 5, 6, 0]);
984        let large = export_with_reference_files(&[0, 1, 2, 3, 4, 5, 6, 7, 0]);
985
986        assert_eq!(small.physical_references().count(), 7);
987        assert_eq!(large.physical_references().count(), 8);
988    }
989
990    #[test]
991    fn reference_path_metadata_tracks_exact_depth_and_hop_bounds() {
992        let mut interner = ReferencePathInterner::default();
993        let root = interner
994            .direct(FileId(10), ModuleLoadMechanism::EsModule)
995            .expect("tracked interner must return a path");
996        let lower = interner
997            .extend(Some(root), FileId(5), ModuleLoadMechanism::EsModule)
998            .expect("tracked interner must extend a path");
999        let upper = interner
1000            .extend(Some(lower), FileId(20), ModuleLoadMechanism::EsModule)
1001            .expect("tracked interner must extend a path");
1002
1003        assert_eq!(interner.metadata[root.index()].depth, 0);
1004        assert_eq!(
1005            interner.metadata[lower.index()].hop_target_bounds,
1006            Some((FileId(5), FileId(10)))
1007        );
1008        assert_eq!(interner.metadata[upper.index()].depth, 2);
1009        assert_eq!(
1010            interner.metadata[upper.index()].hop_target_bounds,
1011            Some((FileId(5), FileId(20)))
1012        );
1013
1014        let repeated = interner.extend(Some(upper), FileId(10), ModuleLoadMechanism::EsModule);
1015        assert_eq!(repeated, Some(upper));
1016    }
1017
1018    #[test]
1019    fn finalized_reference_paths_are_independent_of_interning_order() {
1020        let mut first = ReferencePathInterner::default();
1021        let first_parent = first.direct(FileId(1), ModuleLoadMechanism::EsModule);
1022        let first_direct = first.direct(FileId(2), ModuleLoadMechanism::CommonJsRequire);
1023        let first_chain = first.extend(first_parent, FileId(3), ModuleLoadMechanism::EsModule);
1024        let mut first_modules = vec![module_with_reference_paths(&[first_direct, first_chain])];
1025        let first_nodes = first.finalize(&mut first_modules);
1026
1027        let mut second = ReferencePathInterner::default();
1028        let second_direct = second.direct(FileId(2), ModuleLoadMechanism::CommonJsRequire);
1029        let second_parent = second.direct(FileId(1), ModuleLoadMechanism::EsModule);
1030        let second_chain = second.extend(second_parent, FileId(3), ModuleLoadMechanism::EsModule);
1031        let mut second_modules = vec![module_with_reference_paths(&[second_direct, second_chain])];
1032        let second_nodes = second.finalize(&mut second_modules);
1033
1034        assert_eq!(first_nodes, second_nodes);
1035        assert_eq!(
1036            first_modules[0].exports[0].reference_paths,
1037            second_modules[0].exports[0].reference_paths
1038        );
1039    }
1040
1041    fn two_hop_route(first: FileId, second: FileId) -> ReferenceRouteGraphSpec {
1042        ReferenceRouteGraphSpec::new(vec![
1043            ReferenceRouteNodeSpec::new(
1044                first,
1045                ModuleLoadMechanism::EsModule,
1046                vec![ReferenceRouteNodeId(1)],
1047            ),
1048            ReferenceRouteNodeSpec::new(second, ModuleLoadMechanism::EsModule, Vec::new()),
1049        ])
1050    }
1051
1052    #[test]
1053    fn finalized_reference_routes_are_independent_of_interning_order() {
1054        let route_a = two_hop_route(FileId(1), FileId(2));
1055        let route_b = two_hop_route(FileId(3), FileId(4));
1056
1057        let mut first = ReferencePathInterner::default();
1058        let first_a = first.intern_route_graph(route_a.clone());
1059        let first_b = first.intern_route_graph(route_b.clone());
1060        let first_b_path = first.route(
1061            None,
1062            first_b,
1063            ReferenceRouteNodeId(0),
1064            ReferenceRouteNodeId(1),
1065            Some(ModuleLoadMechanism::CommonJsRequire),
1066        );
1067        let first_a_path = first.route(
1068            None,
1069            first_a,
1070            ReferenceRouteNodeId(0),
1071            ReferenceRouteNodeId(1),
1072            Some(ModuleLoadMechanism::EsModule),
1073        );
1074        let mut first_modules = vec![module_with_reference_paths(&[first_b_path, first_a_path])];
1075        let first_paths = first.finalize(&mut first_modules);
1076
1077        let mut second = ReferencePathInterner::default();
1078        let second_b = second.intern_route_graph(route_b);
1079        let second_a = second.intern_route_graph(route_a);
1080        let second_b_path = second.route(
1081            None,
1082            second_b,
1083            ReferenceRouteNodeId(0),
1084            ReferenceRouteNodeId(1),
1085            Some(ModuleLoadMechanism::CommonJsRequire),
1086        );
1087        let second_a_path = second.route(
1088            None,
1089            second_a,
1090            ReferenceRouteNodeId(0),
1091            ReferenceRouteNodeId(1),
1092            Some(ModuleLoadMechanism::EsModule),
1093        );
1094        let mut second_modules = vec![module_with_reference_paths(&[second_b_path, second_a_path])];
1095        let second_paths = second.finalize(&mut second_modules);
1096
1097        assert_eq!(first_paths, second_paths);
1098        assert_eq!(
1099            first_modules[0].exports[0].reference_paths,
1100            second_modules[0].exports[0].reference_paths
1101        );
1102    }
1103
1104    #[test]
1105    fn push_reference_without_paths_never_allocates_the_side_table() {
1106        let mut export = ExportSymbol {
1107            name: ExportName::Named("value".to_string()),
1108            is_type_only: false,
1109            is_side_effect_used: false,
1110            visibility: VisibilityTag::None,
1111            expected_unused_reason: None,
1112            span: oxc_span::Span::default(),
1113            references: Vec::new(),
1114            reference_paths: Vec::new(),
1115            members: Vec::new(),
1116            deprecated: false,
1117            deprecated_reason: None,
1118        };
1119        for id in 0..3 {
1120            export.push_reference(
1121                SymbolReference {
1122                    from_file: FileId(id),
1123                    kind: ReferenceKind::NamedImport,
1124                    namespace: ExportNamespace::Value,
1125                    import_span: oxc_span::Span::default(),
1126                },
1127                None,
1128            );
1129        }
1130        assert_eq!(export.references.len(), 3);
1131        assert!(export.reference_paths.is_empty());
1132        assert_eq!(export.reference_paths.capacity(), 0);
1133        assert_eq!(export.reference_path(1), None);
1134        assert!(export.has_reference_from(
1135            FileId(1),
1136            oxc_span::Span::default(),
1137            None,
1138            ExportNamespace::Value
1139        ));
1140        assert!(!export.has_reference_from(
1141            FileId(9),
1142            oxc_span::Span::default(),
1143            None,
1144            ExportNamespace::Value
1145        ));
1146    }
1147
1148    #[test]
1149    fn push_reference_backfills_the_side_table_on_the_first_tracked_path() {
1150        let mut export = ExportSymbol {
1151            name: ExportName::Named("value".to_string()),
1152            is_type_only: false,
1153            is_side_effect_used: false,
1154            visibility: VisibilityTag::None,
1155            expected_unused_reason: None,
1156            span: oxc_span::Span::default(),
1157            references: Vec::new(),
1158            reference_paths: Vec::new(),
1159            members: Vec::new(),
1160            deprecated: false,
1161            deprecated_reason: None,
1162        };
1163        let reference = SymbolReference {
1164            from_file: FileId(0),
1165            kind: ReferenceKind::NamedImport,
1166            namespace: ExportNamespace::Value,
1167            import_span: oxc_span::Span::default(),
1168        };
1169        export.push_reference(reference, None);
1170        let tracked = ReferencePathId::from_index(4);
1171        export.push_reference(reference, Some(tracked));
1172        export.push_reference(reference, None);
1173
1174        assert_eq!(export.reference_paths, vec![None, Some(tracked), None]);
1175        assert_eq!(export.reference_path(0), None);
1176        assert_eq!(export.reference_path(1), Some(tracked));
1177        assert!(export.has_reference_from(
1178            FileId(0),
1179            reference.import_span,
1180            Some(tracked),
1181            ExportNamespace::Value
1182        ));
1183        assert!(!export.has_reference_from(
1184            FileId(0),
1185            reference.import_span,
1186            Some(ReferencePathId::from_index(7)),
1187            ExportNamespace::Value
1188        ));
1189    }
1190
1191    #[test]
1192    fn module_node_construction() {
1193        let mut node = ModuleNode {
1194            file_id: FileId(0),
1195            path: PathBuf::from("/project/src/index.ts"),
1196            edge_range: 0..5,
1197            exports: vec![],
1198            re_exports: vec![],
1199            flags: ModuleNode::flags_from(true, true, false),
1200        };
1201        node.set_reachable(true);
1202        assert_eq!(node.file_id, FileId(0));
1203        assert!(node.is_entry_point());
1204        assert!(node.is_reachable());
1205        assert!(node.is_runtime_reachable());
1206        assert!(!node.is_test_reachable());
1207        assert!(!node.has_cjs_exports());
1208        assert_eq!(node.edge_range, 0..5);
1209    }
1210
1211    #[test]
1212    fn module_node_non_entry_unreachable() {
1213        let node = ModuleNode {
1214            file_id: FileId(5),
1215            path: PathBuf::from("/project/src/orphan.ts"),
1216            edge_range: 0..0,
1217            exports: vec![],
1218            re_exports: vec![],
1219            flags: ModuleNode::flags_from(false, false, false),
1220        };
1221        assert!(!node.is_entry_point());
1222        assert!(!node.is_reachable());
1223        assert!(!node.is_runtime_reachable());
1224        assert!(!node.is_test_reachable());
1225        assert!(node.edge_range.is_empty());
1226    }
1227
1228    #[test]
1229    fn module_node_cjs_exports() {
1230        let mut node = ModuleNode {
1231            file_id: FileId(2),
1232            path: PathBuf::from("/project/lib/legacy.js"),
1233            edge_range: 3..7,
1234            exports: vec![],
1235            re_exports: vec![],
1236            flags: ModuleNode::flags_from(false, true, true),
1237        };
1238        node.set_reachable(true);
1239        assert!(node.has_cjs_exports());
1240        assert!(node.is_runtime_reachable());
1241        assert_eq!(node.edge_range.len(), 4);
1242    }
1243
1244    #[test]
1245    fn module_node_with_exports_and_re_exports() {
1246        let node = ModuleNode {
1247            file_id: FileId(1),
1248            path: PathBuf::from("/project/src/barrel.ts"),
1249            edge_range: 0..3,
1250            exports: vec![ExportSymbol {
1251                name: ExportName::Named("localFn".to_string()),
1252                is_type_only: false,
1253                is_side_effect_used: false,
1254                visibility: VisibilityTag::None,
1255                expected_unused_reason: None,
1256                span: oxc_span::Span::new(0, 20),
1257                references: vec![],
1258                reference_paths: Vec::new(),
1259                members: vec![],
1260                deprecated: false,
1261                deprecated_reason: None,
1262            }],
1263            re_exports: vec![ReExportEdge {
1264                source_file: FileId(2),
1265                imported_name: "*".to_string(),
1266                exported_name: "*".to_string(),
1267                is_type_only: false,
1268                span: oxc_span::Span::default(),
1269            }],
1270            flags: ModuleNode::flags_from(false, true, false),
1271        };
1272        assert_eq!(node.exports.len(), 1);
1273        assert_eq!(node.re_exports.len(), 1);
1274        assert_eq!(node.re_exports[0].source_file, FileId(2));
1275    }
1276}