Skip to main content

fallow_graph/graph/
types.rs

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