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