1use 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#[derive(Debug, serde::Serialize, serde::Deserialize)]
18pub struct ModuleNode {
19 pub file_id: FileId,
21 pub path: PathBuf,
23 pub edge_range: Range<usize>,
25 pub exports: Vec<ExportSymbol>,
27 pub re_exports: Vec<ReExportEdge>,
29 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 #[inline]
42 pub const fn is_entry_point(&self) -> bool {
43 self.flags & FLAG_ENTRY_POINT != 0
44 }
45
46 #[inline]
48 pub const fn is_reachable(&self) -> bool {
49 self.flags & FLAG_REACHABLE != 0
50 }
51
52 #[inline]
54 pub const fn is_runtime_reachable(&self) -> bool {
55 self.flags & FLAG_RUNTIME_REACHABLE != 0
56 }
57
58 #[inline]
60 pub const fn is_test_reachable(&self) -> bool {
61 self.flags & FLAG_TEST_REACHABLE != 0
62 }
63
64 #[inline]
66 pub const fn has_cjs_exports(&self) -> bool {
67 self.flags & FLAG_CJS_EXPORTS != 0
68 }
69
70 #[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 #[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 #[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 #[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 #[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 #[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#[derive(Debug, serde::Serialize, serde::Deserialize)]
143pub struct ReExportEdge {
144 pub source_file: FileId,
146 pub imported_name: String,
148 pub exported_name: String,
150 pub is_type_only: bool,
152 #[serde(with = "crate::cache::span_serde")]
156 pub span: oxc_span::Span,
157}
158
159#[derive(Debug, serde::Serialize, serde::Deserialize)]
161pub struct ExportSymbol {
162 pub name: ExportName,
164 pub is_type_only: bool,
166 pub is_side_effect_used: bool,
171 pub visibility: VisibilityTag,
174 pub expected_unused_reason: Option<String>,
176 #[serde(with = "crate::cache::span_serde")]
178 pub span: oxc_span::Span,
179 pub references: Vec<SymbolReference>,
181 #[serde(default)]
190 pub reference_paths: Vec<Option<ReferencePathId>>,
191 #[serde(with = "crate::cache::member_serde")]
198 pub members: Vec<fallow_types::extract::MemberInfo>,
199}
200
201#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
203pub struct SymbolReference {
204 pub from_file: FileId,
206 pub kind: ReferenceKind,
208 #[serde(with = "crate::cache::span_serde")]
211 pub import_span: oxc_span::Span,
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
219pub struct ReferencePathId(NonZeroU32);
220
221#[derive(Clone, Copy)]
225pub(crate) struct RoutedReference {
226 pub(crate) reference: SymbolReference,
227 pub(crate) path: Option<ReferencePathId>,
228}
229
230impl ExportSymbol {
231 pub(crate) fn reference_path(&self, index: usize) -> Option<ReferencePathId> {
233 self.reference_paths.get(index).copied().flatten()
234 }
235
236 pub(crate) fn has_reference_from(
239 &self,
240 from_file: FileId,
241 path: Option<ReferencePathId>,
242 ) -> bool {
243 self.references
244 .iter()
245 .enumerate()
246 .any(|(index, reference)| {
247 reference.from_file == from_file && self.reference_path(index) == path
248 })
249 }
250
251 pub(crate) fn push_reference(
256 &mut self,
257 reference: SymbolReference,
258 path: Option<ReferencePathId>,
259 ) {
260 if path.is_some() || !self.reference_paths.is_empty() {
261 self.reference_paths.resize(self.references.len(), None);
262 self.reference_paths.push(path);
263 }
264 self.references.push(reference);
265 }
266
267 pub(crate) fn routed_references(&self) -> impl Iterator<Item = RoutedReference> + '_ {
269 self.references
270 .iter()
271 .enumerate()
272 .map(|(index, reference)| RoutedReference {
273 reference: *reference,
274 path: self.reference_path(index),
275 })
276 }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
281pub(crate) enum ReferencePathNode {
282 Hop {
284 parent: Option<ReferencePathId>,
286 target: FileId,
288 mechanism: ModuleLoadMechanism,
290 },
291 Route {
293 parent: Option<ReferencePathId>,
296 graph: ReferenceRouteGraphId,
298 start: ReferenceRouteNodeId,
300 terminal: ReferenceRouteNodeId,
302 start_mechanism: Option<ModuleLoadMechanism>,
306 },
307}
308
309impl ReferencePathNode {
310 pub(crate) const fn parent(self) -> Option<ReferencePathId> {
311 match self {
312 Self::Hop { parent, .. } | Self::Route { parent, .. } => parent,
313 }
314 }
315
316 fn remap_parent(&mut self, remap: &[ReferencePathId]) {
317 match self {
318 Self::Hop { parent, .. } | Self::Route { parent, .. } => {
319 *parent = parent.map(|path| remap[path.index()]);
320 }
321 }
322 }
323}
324
325#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
327pub(crate) struct ReferenceRouteGraphId(pub(crate) u32);
328
329#[derive(
331 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
332)]
333pub(crate) struct ReferenceRouteNodeId(pub(crate) u32);
334
335#[derive(Debug, Clone, PartialEq, Eq, Hash)]
337pub(crate) struct ReferenceRouteNodeSpec {
338 target: FileId,
339 mechanism: ModuleLoadMechanism,
340 successors: Vec<ReferenceRouteNodeId>,
341}
342
343impl ReferenceRouteNodeSpec {
344 pub(crate) fn new(
345 target: FileId,
346 mechanism: ModuleLoadMechanism,
347 mut successors: Vec<ReferenceRouteNodeId>,
348 ) -> Self {
349 successors.sort_unstable_by_key(|successor| successor.0);
350 successors.dedup();
351 Self {
352 target,
353 mechanism,
354 successors,
355 }
356 }
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, Hash)]
361pub(crate) struct ReferenceRouteGraphSpec {
362 nodes: Vec<ReferenceRouteNodeSpec>,
363}
364
365impl ReferenceRouteGraphSpec {
366 pub(crate) fn new(nodes: Vec<ReferenceRouteNodeSpec>) -> Self {
367 debug_assert!(nodes.iter().all(|node| {
368 node.successors
369 .iter()
370 .all(|successor| successor.0 < nodes.len() as u32)
371 }));
372 Self { nodes }
373 }
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
378pub(crate) struct ReferenceRouteGraph {
379 pub(crate) nodes: Range<u32>,
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
384pub(crate) struct ReferenceRouteNode {
385 pub(crate) target: FileId,
386 pub(crate) mechanism: ModuleLoadMechanism,
387 pub(crate) successors: Range<u32>,
388}
389
390#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
392pub(crate) struct ReferenceRoutes {
393 pub(crate) graphs: Vec<ReferenceRouteGraph>,
394 pub(crate) nodes: Vec<ReferenceRouteNode>,
395 pub(crate) edges: Vec<ReferenceRouteNodeId>,
396}
397
398impl ReferenceRoutes {
399 #[cfg(test)]
400 pub(crate) fn canonical_hops(
401 &self,
402 graph_id: ReferenceRouteGraphId,
403 start: ReferenceRouteNodeId,
404 terminal: ReferenceRouteNodeId,
405 start_mechanism: Option<ModuleLoadMechanism>,
406 ) -> Vec<(FileId, ModuleLoadMechanism)> {
407 let Some(graph) = self.graphs.get(graph_id.0 as usize) else {
408 return Vec::new();
409 };
410 let node_count = graph.nodes.end.saturating_sub(graph.nodes.start) as usize;
411 let start_index = start.0 as usize;
412 let terminal_index = terminal.0 as usize;
413 if start_index >= node_count || terminal_index >= node_count {
414 return Vec::new();
415 }
416
417 let mut predecessor = vec![None; node_count];
418 let mut visited = vec![false; node_count];
419 let mut queue = std::collections::VecDeque::from([start_index]);
420 visited[start_index] = true;
421 while let Some(local_index) = queue.pop_front() {
422 if local_index == terminal_index {
423 break;
424 }
425 let Some(node) = self.nodes.get(graph.nodes.start as usize + local_index) else {
426 return Vec::new();
427 };
428 let Some(successors) = self
429 .edges
430 .get(node.successors.start as usize..node.successors.end as usize)
431 else {
432 return Vec::new();
433 };
434 for successor in successors {
435 let successor_index = successor.0 as usize;
436 if successor_index >= node_count || visited[successor_index] {
437 continue;
438 }
439 visited[successor_index] = true;
440 predecessor[successor_index] = Some(local_index);
441 queue.push_back(successor_index);
442 }
443 }
444 if !visited[terminal_index] {
445 return Vec::new();
446 }
447
448 let mut hops = Vec::new();
449 let mut current = terminal_index;
450 loop {
451 let node = &self.nodes[graph.nodes.start as usize + current];
452 if current != start_index {
453 hops.push((node.target, node.mechanism));
454 } else {
455 if let Some(mechanism) = start_mechanism {
456 hops.push((node.target, mechanism));
457 }
458 break;
459 }
460 let Some(parent) = predecessor[current] else {
461 return Vec::new();
462 };
463 current = parent;
464 }
465 hops
466 }
467}
468
469#[derive(Debug, PartialEq, Eq)]
471pub(crate) struct FinalizedReferencePaths {
472 pub(crate) paths: Vec<ReferencePathNode>,
473 pub(crate) routes: ReferenceRoutes,
474}
475
476pub(crate) struct ReferencePathInterner {
478 track_provenance: bool,
479 nodes: Vec<ReferencePathNode>,
480 metadata: Vec<ReferencePathMetadata>,
481 ids: FxHashMap<ReferencePathNode, ReferencePathId>,
482 route_graphs: Vec<ReferenceRouteGraphSpec>,
483 route_graph_ids: FxHashMap<ReferenceRouteGraphSpec, ReferenceRouteGraphId>,
484}
485
486#[derive(Clone, Copy)]
487struct ReferencePathMetadata {
488 depth: usize,
489 hop_target_bounds: Option<(FileId, FileId)>,
490}
491
492impl Default for ReferencePathInterner {
493 fn default() -> Self {
494 Self::new(true)
495 }
496}
497
498impl ReferencePathInterner {
499 pub(crate) fn new(track_provenance: bool) -> Self {
500 Self {
501 track_provenance,
502 nodes: Vec::new(),
503 metadata: Vec::new(),
504 ids: FxHashMap::default(),
505 route_graphs: Vec::new(),
506 route_graph_ids: FxHashMap::default(),
507 }
508 }
509
510 pub(crate) const fn tracks_provenance(&self) -> bool {
511 self.track_provenance
512 }
513
514 pub(crate) fn direct(
516 &mut self,
517 target: FileId,
518 mechanism: ModuleLoadMechanism,
519 ) -> Option<ReferencePathId> {
520 self.track_provenance.then(|| {
521 self.intern(ReferencePathNode::Hop {
522 parent: None,
523 target,
524 mechanism,
525 })
526 })
527 }
528
529 pub(crate) fn extend(
531 &mut self,
532 parent: Option<ReferencePathId>,
533 target: FileId,
534 mechanism: ModuleLoadMechanism,
535 ) -> Option<ReferencePathId> {
536 if !self.track_provenance {
537 debug_assert!(parent.is_none());
538 return None;
539 }
540 let Some(parent) = parent else {
541 debug_assert!(false, "tracked reference paths require an interned parent");
542 return None;
543 };
544 let may_contain_target = self
545 .metadata
546 .get(parent.index())
547 .and_then(|metadata| metadata.hop_target_bounds)
548 .is_some_and(|(minimum, maximum)| target.0 >= minimum.0 && target.0 <= maximum.0);
549 if may_contain_target && self.contains_target(parent, target) {
550 return Some(parent);
551 }
552 Some(self.intern(ReferencePathNode::Hop {
553 parent: Some(parent),
554 target,
555 mechanism,
556 }))
557 }
558
559 pub(crate) fn intern_route_graph(
561 &mut self,
562 graph: ReferenceRouteGraphSpec,
563 ) -> ReferenceRouteGraphId {
564 debug_assert!(self.track_provenance);
565 if let Some(id) = self.route_graph_ids.get(&graph) {
566 return *id;
567 }
568 let id = ReferenceRouteGraphId(self.route_graphs.len() as u32);
569 self.route_graphs.push(graph.clone());
570 self.route_graph_ids.insert(graph, id);
571 id
572 }
573
574 pub(crate) fn route(
576 &mut self,
577 parent: Option<ReferencePathId>,
578 graph: ReferenceRouteGraphId,
579 start: ReferenceRouteNodeId,
580 terminal: ReferenceRouteNodeId,
581 start_mechanism: Option<ModuleLoadMechanism>,
582 ) -> Option<ReferencePathId> {
583 if !self.track_provenance {
584 return None;
585 }
586 Some(self.intern(ReferencePathNode::Route {
587 parent,
588 graph,
589 start,
590 terminal,
591 start_mechanism,
592 }))
593 }
594
595 fn contains_target(&self, mut path: ReferencePathId, target: FileId) -> bool {
596 loop {
597 let Some(node) = self.nodes.get(path.index()) else {
598 return false;
599 };
600 if let ReferencePathNode::Hop {
601 target: hop_target, ..
602 } = node
603 && *hop_target == target
604 {
605 return true;
606 }
607 let Some(parent) = node.parent() else {
608 return false;
609 };
610 path = parent;
611 }
612 }
613
614 fn intern(&mut self, node: ReferencePathNode) -> ReferencePathId {
615 if let Some(path) = self.ids.get(&node) {
616 return *path;
617 }
618 let path = ReferencePathId::from_index(self.nodes.len());
619 let parent_metadata = node
620 .parent()
621 .and_then(|parent| self.metadata.get(parent.index()).copied());
622 let depth = parent_metadata.map_or(0, |metadata| metadata.depth + 1);
623 let hop_target_bounds = match node {
624 ReferencePathNode::Hop { target, .. } => Some(
625 parent_metadata
626 .and_then(|metadata| metadata.hop_target_bounds)
627 .map_or((target, target), |(minimum, maximum)| {
628 (
629 FileId(minimum.0.min(target.0)),
630 FileId(maximum.0.max(target.0)),
631 )
632 }),
633 ),
634 ReferencePathNode::Route { .. } => {
635 parent_metadata.and_then(|metadata| metadata.hop_target_bounds)
636 }
637 };
638 self.nodes.push(node);
639 self.metadata.push(ReferencePathMetadata {
640 depth,
641 hop_target_bounds,
642 });
643 self.ids.insert(node, path);
644 path
645 }
646
647 pub(crate) fn finalize(self, modules: &mut [ModuleNode]) -> FinalizedReferencePaths {
653 if self.nodes.is_empty() && self.route_graphs.is_empty() {
654 return FinalizedReferencePaths {
655 paths: Vec::new(),
656 routes: ReferenceRoutes::default(),
657 };
658 }
659
660 let (routes, route_remap) = finalize_route_graphs(&self.route_graphs);
661 let max_depth = self
662 .metadata
663 .iter()
664 .map(|metadata| metadata.depth)
665 .max()
666 .unwrap_or(0);
667
668 let mut paths_by_depth = vec![Vec::new(); max_depth.saturating_add(1)];
669 for (old_index, metadata) in self.metadata.iter().enumerate() {
670 paths_by_depth[metadata.depth].push(old_index);
671 }
672
673 let mut remap = vec![ReferencePathId::from_index(0); self.nodes.len()];
674 let mut finalized = Vec::with_capacity(self.nodes.len());
675 for mut paths in paths_by_depth {
676 paths.sort_unstable_by(|&left, &right| {
677 compare_path_nodes(self.nodes[left], self.nodes[right], &remap, &route_remap)
678 });
679 for old_index in paths {
680 let mut node = self.nodes[old_index];
681 node.remap_parent(&remap);
682 if let ReferencePathNode::Route { graph, .. } = &mut node {
683 *graph = route_remap[graph.0 as usize];
684 }
685 let canonical = ReferencePathId::from_index(finalized.len());
686 remap[old_index] = canonical;
687 finalized.push(node);
688 }
689 }
690
691 for path in modules
692 .iter_mut()
693 .flat_map(|module| &mut module.exports)
694 .flat_map(|export| &mut export.reference_paths)
695 {
696 if let Some(existing) = *path {
697 *path = Some(remap[existing.index()]);
698 }
699 }
700
701 FinalizedReferencePaths {
702 paths: finalized,
703 routes,
704 }
705 }
706}
707
708fn compare_path_nodes(
709 left: ReferencePathNode,
710 right: ReferencePathNode,
711 path_remap: &[ReferencePathId],
712 route_remap: &[ReferenceRouteGraphId],
713) -> Ordering {
714 let left_parent = left.parent().map(|parent| path_remap[parent.index()].0);
715 let right_parent = right.parent().map(|parent| path_remap[parent.index()].0);
716 left_parent
717 .cmp(&right_parent)
718 .then_with(|| match (left, right) {
719 (
720 ReferencePathNode::Hop {
721 target: left_target,
722 mechanism: left_mechanism,
723 ..
724 },
725 ReferencePathNode::Hop {
726 target: right_target,
727 mechanism: right_mechanism,
728 ..
729 },
730 ) => {
731 (left_target.0, left_mechanism as u8).cmp(&(right_target.0, right_mechanism as u8))
732 }
733 (ReferencePathNode::Hop { .. }, ReferencePathNode::Route { .. }) => Ordering::Less,
734 (ReferencePathNode::Route { .. }, ReferencePathNode::Hop { .. }) => Ordering::Greater,
735 (
736 ReferencePathNode::Route {
737 graph: left_graph,
738 start: left_start,
739 terminal: left_terminal,
740 start_mechanism: left_mechanism,
741 ..
742 },
743 ReferencePathNode::Route {
744 graph: right_graph,
745 start: right_start,
746 terminal: right_terminal,
747 start_mechanism: right_mechanism,
748 ..
749 },
750 ) => (
751 route_remap[left_graph.0 as usize].0,
752 left_start.0,
753 left_terminal.0,
754 left_mechanism.map(|mechanism| mechanism as u8),
755 )
756 .cmp(&(
757 route_remap[right_graph.0 as usize].0,
758 right_start.0,
759 right_terminal.0,
760 right_mechanism.map(|mechanism| mechanism as u8),
761 )),
762 })
763}
764
765fn compare_route_graph_specs(
766 left: &ReferenceRouteGraphSpec,
767 right: &ReferenceRouteGraphSpec,
768) -> Ordering {
769 left.nodes.len().cmp(&right.nodes.len()).then_with(|| {
770 left.nodes
771 .iter()
772 .zip(&right.nodes)
773 .find_map(|(left_node, right_node)| {
774 let ordering = (
775 left_node.target.0,
776 left_node.mechanism as u8,
777 &left_node.successors,
778 )
779 .cmp(&(
780 right_node.target.0,
781 right_node.mechanism as u8,
782 &right_node.successors,
783 ));
784 (ordering != Ordering::Equal).then_some(ordering)
785 })
786 .unwrap_or(Ordering::Equal)
787 })
788}
789
790fn finalize_route_graphs(
791 graphs: &[ReferenceRouteGraphSpec],
792) -> (ReferenceRoutes, Vec<ReferenceRouteGraphId>) {
793 let mut order: Vec<usize> = (0..graphs.len()).collect();
794 order
795 .sort_unstable_by(|&left, &right| compare_route_graph_specs(&graphs[left], &graphs[right]));
796
797 let mut remap = vec![ReferenceRouteGraphId(0); graphs.len()];
798 let mut finalized = ReferenceRoutes::default();
799 for old_index in order {
800 let graph_id = ReferenceRouteGraphId(finalized.graphs.len() as u32);
801 remap[old_index] = graph_id;
802 let node_start = finalized.nodes.len() as u32;
803 for node in &graphs[old_index].nodes {
804 let edge_start = finalized.edges.len() as u32;
805 finalized.edges.extend_from_slice(&node.successors);
806 finalized.nodes.push(ReferenceRouteNode {
807 target: node.target,
808 mechanism: node.mechanism,
809 successors: edge_start..finalized.edges.len() as u32,
810 });
811 }
812 finalized.graphs.push(ReferenceRouteGraph {
813 nodes: node_start..finalized.nodes.len() as u32,
814 });
815 }
816 (finalized, remap)
817}
818
819impl ReferencePathId {
820 fn from_index(index: usize) -> Self {
821 let Some(encoded) = u32::try_from(index)
822 .ok()
823 .and_then(|index| index.checked_add(1))
824 .and_then(NonZeroU32::new)
825 else {
826 panic!("a process cannot allocate more than u32::MAX reference path nodes");
827 };
828 Self(encoded)
829 }
830
831 pub(crate) const fn index(self) -> usize {
832 (self.0.get() - 1) as usize
833 }
834}
835
836#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
838pub enum ReferenceKind {
839 NamedImport,
841 DefaultImport,
843 NamespaceImport,
845 ReExport,
847 DynamicImport,
849 SideEffectImport,
851}
852
853#[cfg(target_pointer_width = "64")]
854const _: () = assert!(std::mem::size_of::<ExportSymbol>() == 136);
855#[cfg(target_pointer_width = "64")]
856const _: () = assert!(std::mem::size_of::<SymbolReference>() == 16);
857#[cfg(target_pointer_width = "64")]
858const _: () = assert!(std::mem::size_of::<ReExportEdge>() == 64);
859#[cfg(all(target_pointer_width = "64", unix))]
860const _: () = assert!(std::mem::size_of::<ModuleNode>() == 96);
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865
866 #[test]
867 fn reference_kind_equality() {
868 assert_eq!(ReferenceKind::NamedImport, ReferenceKind::NamedImport);
869 assert_ne!(ReferenceKind::NamedImport, ReferenceKind::DefaultImport);
870 }
871
872 #[test]
873 fn reference_kind_all_variants_are_distinct() {
874 let all = [
875 ReferenceKind::NamedImport,
876 ReferenceKind::DefaultImport,
877 ReferenceKind::NamespaceImport,
878 ReferenceKind::ReExport,
879 ReferenceKind::DynamicImport,
880 ReferenceKind::SideEffectImport,
881 ];
882 for (i, a) in all.iter().enumerate() {
883 for (j, b) in all.iter().enumerate() {
884 if i == j {
885 assert_eq!(a, b);
886 } else {
887 assert_ne!(a, b);
888 }
889 }
890 }
891 }
892
893 #[test]
894 fn reference_kind_copy() {
895 let original = ReferenceKind::NamespaceImport;
896 let copied = original;
897 assert_eq!(original, copied);
898 }
899
900 #[test]
901 fn reference_kind_debug_format() {
902 let kind = ReferenceKind::DynamicImport;
903 let debug_str = format!("{kind:?}");
904 assert_eq!(debug_str, "DynamicImport");
905 }
906
907 fn module_with_reference_paths(paths: &[Option<ReferencePathId>]) -> ModuleNode {
908 ModuleNode {
909 file_id: FileId(0),
910 path: PathBuf::from("/project/source.ts"),
911 edge_range: 0..0,
912 exports: vec![ExportSymbol {
913 name: ExportName::Named("value".to_string()),
914 is_type_only: false,
915 is_side_effect_used: false,
916 visibility: VisibilityTag::None,
917 expected_unused_reason: None,
918 span: oxc_span::Span::default(),
919 references: paths
920 .iter()
921 .map(|_| SymbolReference {
922 from_file: FileId(0),
923 kind: ReferenceKind::NamedImport,
924 import_span: oxc_span::Span::default(),
925 })
926 .collect(),
927 reference_paths: paths.to_vec(),
928 members: Vec::new(),
929 }],
930 re_exports: Vec::new(),
931 flags: 0,
932 }
933 }
934
935 #[test]
936 fn reference_path_metadata_tracks_exact_depth_and_hop_bounds() {
937 let mut interner = ReferencePathInterner::default();
938 let root = interner
939 .direct(FileId(10), ModuleLoadMechanism::EsModule)
940 .expect("tracked interner must return a path");
941 let lower = interner
942 .extend(Some(root), FileId(5), ModuleLoadMechanism::EsModule)
943 .expect("tracked interner must extend a path");
944 let upper = interner
945 .extend(Some(lower), FileId(20), ModuleLoadMechanism::EsModule)
946 .expect("tracked interner must extend a path");
947
948 assert_eq!(interner.metadata[root.index()].depth, 0);
949 assert_eq!(
950 interner.metadata[lower.index()].hop_target_bounds,
951 Some((FileId(5), FileId(10)))
952 );
953 assert_eq!(interner.metadata[upper.index()].depth, 2);
954 assert_eq!(
955 interner.metadata[upper.index()].hop_target_bounds,
956 Some((FileId(5), FileId(20)))
957 );
958
959 let repeated = interner.extend(Some(upper), FileId(10), ModuleLoadMechanism::EsModule);
960 assert_eq!(repeated, Some(upper));
961 }
962
963 #[test]
964 fn finalized_reference_paths_are_independent_of_interning_order() {
965 let mut first = ReferencePathInterner::default();
966 let first_parent = first.direct(FileId(1), ModuleLoadMechanism::EsModule);
967 let first_direct = first.direct(FileId(2), ModuleLoadMechanism::CommonJsRequire);
968 let first_chain = first.extend(first_parent, FileId(3), ModuleLoadMechanism::EsModule);
969 let mut first_modules = vec![module_with_reference_paths(&[first_direct, first_chain])];
970 let first_nodes = first.finalize(&mut first_modules);
971
972 let mut second = ReferencePathInterner::default();
973 let second_direct = second.direct(FileId(2), ModuleLoadMechanism::CommonJsRequire);
974 let second_parent = second.direct(FileId(1), ModuleLoadMechanism::EsModule);
975 let second_chain = second.extend(second_parent, FileId(3), ModuleLoadMechanism::EsModule);
976 let mut second_modules = vec![module_with_reference_paths(&[second_direct, second_chain])];
977 let second_nodes = second.finalize(&mut second_modules);
978
979 assert_eq!(first_nodes, second_nodes);
980 assert_eq!(
981 first_modules[0].exports[0].reference_paths,
982 second_modules[0].exports[0].reference_paths
983 );
984 }
985
986 fn two_hop_route(first: FileId, second: FileId) -> ReferenceRouteGraphSpec {
987 ReferenceRouteGraphSpec::new(vec![
988 ReferenceRouteNodeSpec::new(
989 first,
990 ModuleLoadMechanism::EsModule,
991 vec![ReferenceRouteNodeId(1)],
992 ),
993 ReferenceRouteNodeSpec::new(second, ModuleLoadMechanism::EsModule, Vec::new()),
994 ])
995 }
996
997 #[test]
998 fn finalized_reference_routes_are_independent_of_interning_order() {
999 let route_a = two_hop_route(FileId(1), FileId(2));
1000 let route_b = two_hop_route(FileId(3), FileId(4));
1001
1002 let mut first = ReferencePathInterner::default();
1003 let first_a = first.intern_route_graph(route_a.clone());
1004 let first_b = first.intern_route_graph(route_b.clone());
1005 let first_b_path = first.route(
1006 None,
1007 first_b,
1008 ReferenceRouteNodeId(0),
1009 ReferenceRouteNodeId(1),
1010 Some(ModuleLoadMechanism::CommonJsRequire),
1011 );
1012 let first_a_path = first.route(
1013 None,
1014 first_a,
1015 ReferenceRouteNodeId(0),
1016 ReferenceRouteNodeId(1),
1017 Some(ModuleLoadMechanism::EsModule),
1018 );
1019 let mut first_modules = vec![module_with_reference_paths(&[first_b_path, first_a_path])];
1020 let first_paths = first.finalize(&mut first_modules);
1021
1022 let mut second = ReferencePathInterner::default();
1023 let second_b = second.intern_route_graph(route_b);
1024 let second_a = second.intern_route_graph(route_a);
1025 let second_b_path = second.route(
1026 None,
1027 second_b,
1028 ReferenceRouteNodeId(0),
1029 ReferenceRouteNodeId(1),
1030 Some(ModuleLoadMechanism::CommonJsRequire),
1031 );
1032 let second_a_path = second.route(
1033 None,
1034 second_a,
1035 ReferenceRouteNodeId(0),
1036 ReferenceRouteNodeId(1),
1037 Some(ModuleLoadMechanism::EsModule),
1038 );
1039 let mut second_modules = vec![module_with_reference_paths(&[second_b_path, second_a_path])];
1040 let second_paths = second.finalize(&mut second_modules);
1041
1042 assert_eq!(first_paths, second_paths);
1043 assert_eq!(
1044 first_modules[0].exports[0].reference_paths,
1045 second_modules[0].exports[0].reference_paths
1046 );
1047 }
1048
1049 #[test]
1050 fn symbol_reference_construction() {
1051 let reference = SymbolReference {
1052 from_file: FileId(42),
1053 kind: ReferenceKind::NamedImport,
1054 import_span: oxc_span::Span::new(10, 30),
1055 };
1056 assert_eq!(reference.from_file, FileId(42));
1057 assert_eq!(reference.kind, ReferenceKind::NamedImport);
1058 assert_eq!(reference.import_span.start, 10);
1059 assert_eq!(reference.import_span.end, 30);
1060 }
1061
1062 #[test]
1063 fn symbol_reference_copy_preserves_all_fields() {
1064 let reference = SymbolReference {
1065 from_file: FileId(7),
1066 kind: ReferenceKind::ReExport,
1067 import_span: oxc_span::Span::new(5, 25),
1068 };
1069 let copied = reference;
1070 assert_eq!(copied.from_file, reference.from_file);
1071 assert_eq!(copied.kind, reference.kind);
1072 assert_eq!(copied.import_span.start, reference.import_span.start);
1073 assert_eq!(copied.import_span.end, reference.import_span.end);
1074 }
1075
1076 #[test]
1077 fn re_export_edge_construction() {
1078 let edge = ReExportEdge {
1079 source_file: FileId(3),
1080 imported_name: "*".to_string(),
1081 exported_name: "*".to_string(),
1082 is_type_only: false,
1083 span: oxc_span::Span::default(),
1084 };
1085 assert_eq!(edge.source_file, FileId(3));
1086 assert_eq!(edge.imported_name, "*");
1087 assert_eq!(edge.exported_name, "*");
1088 assert!(!edge.is_type_only);
1089 }
1090
1091 #[test]
1092 fn re_export_edge_type_only() {
1093 let edge = ReExportEdge {
1094 source_file: FileId(1),
1095 imported_name: "MyType".to_string(),
1096 exported_name: "MyType".to_string(),
1097 is_type_only: true,
1098 span: oxc_span::Span::default(),
1099 };
1100 assert!(edge.is_type_only);
1101 }
1102
1103 #[test]
1104 fn re_export_edge_renamed() {
1105 let edge = ReExportEdge {
1106 source_file: FileId(2),
1107 imported_name: "internal".to_string(),
1108 exported_name: "public".to_string(),
1109 is_type_only: false,
1110 span: oxc_span::Span::default(),
1111 };
1112 assert_ne!(edge.imported_name, edge.exported_name);
1113 assert_eq!(edge.imported_name, "internal");
1114 assert_eq!(edge.exported_name, "public");
1115 }
1116
1117 #[test]
1118 fn export_symbol_named() {
1119 let sym = ExportSymbol {
1120 name: ExportName::Named("myFunction".to_string()),
1121 is_type_only: false,
1122 is_side_effect_used: false,
1123 visibility: VisibilityTag::None,
1124 expected_unused_reason: None,
1125 span: oxc_span::Span::new(0, 50),
1126 references: vec![],
1127 reference_paths: Vec::new(),
1128 members: vec![],
1129 };
1130 assert!(matches!(sym.name, ExportName::Named(ref n) if n == "myFunction"));
1131 assert!(!sym.is_type_only);
1132 assert_eq!(sym.visibility, VisibilityTag::None);
1133 }
1134
1135 #[test]
1136 fn export_symbol_default() {
1137 let sym = ExportSymbol {
1138 name: ExportName::Default,
1139 is_type_only: false,
1140 is_side_effect_used: false,
1141 visibility: VisibilityTag::None,
1142 expected_unused_reason: None,
1143 span: oxc_span::Span::new(0, 20),
1144 references: vec![],
1145 reference_paths: Vec::new(),
1146 members: vec![],
1147 };
1148 assert!(matches!(sym.name, ExportName::Default));
1149 }
1150
1151 #[test]
1152 fn export_symbol_public_tag() {
1153 let sym = ExportSymbol {
1154 name: ExportName::Named("api".to_string()),
1155 is_type_only: false,
1156 is_side_effect_used: false,
1157 visibility: VisibilityTag::Public,
1158 expected_unused_reason: None,
1159 span: oxc_span::Span::new(0, 10),
1160 references: vec![],
1161 reference_paths: Vec::new(),
1162 members: vec![],
1163 };
1164 assert_eq!(sym.visibility, VisibilityTag::Public);
1165 }
1166
1167 #[test]
1168 fn export_symbol_type_only() {
1169 let sym = ExportSymbol {
1170 name: ExportName::Named("MyInterface".to_string()),
1171 is_type_only: true,
1172 is_side_effect_used: false,
1173 visibility: VisibilityTag::None,
1174 expected_unused_reason: None,
1175 span: oxc_span::Span::new(0, 30),
1176 references: vec![],
1177 reference_paths: Vec::new(),
1178 members: vec![],
1179 };
1180 assert!(sym.is_type_only);
1181 }
1182
1183 #[test]
1184 fn export_symbol_with_references() {
1185 let sym = ExportSymbol {
1186 name: ExportName::Named("helper".to_string()),
1187 is_type_only: false,
1188 is_side_effect_used: false,
1189 visibility: VisibilityTag::None,
1190 expected_unused_reason: None,
1191 span: oxc_span::Span::new(0, 20),
1192 references: vec![
1193 SymbolReference {
1194 from_file: FileId(1),
1195 kind: ReferenceKind::NamedImport,
1196 import_span: oxc_span::Span::new(0, 10),
1197 },
1198 SymbolReference {
1199 from_file: FileId(2),
1200 kind: ReferenceKind::ReExport,
1201 import_span: oxc_span::Span::new(5, 15),
1202 },
1203 ],
1204 reference_paths: vec![
1205 Some(ReferencePathId::from_index(0)),
1206 Some(ReferencePathId::from_index(1)),
1207 ],
1208 members: vec![],
1209 };
1210 assert_eq!(sym.references.len(), 2);
1211 assert_eq!(sym.references[0].from_file, FileId(1));
1212 assert_eq!(sym.references[1].kind, ReferenceKind::ReExport);
1213 }
1214
1215 #[test]
1216 fn push_reference_without_paths_never_allocates_the_side_table() {
1217 let mut export = ExportSymbol {
1218 name: ExportName::Named("value".to_string()),
1219 is_type_only: false,
1220 is_side_effect_used: false,
1221 visibility: VisibilityTag::None,
1222 expected_unused_reason: None,
1223 span: oxc_span::Span::default(),
1224 references: Vec::new(),
1225 reference_paths: Vec::new(),
1226 members: Vec::new(),
1227 };
1228 for id in 0..3 {
1229 export.push_reference(
1230 SymbolReference {
1231 from_file: FileId(id),
1232 kind: ReferenceKind::NamedImport,
1233 import_span: oxc_span::Span::default(),
1234 },
1235 None,
1236 );
1237 }
1238 assert_eq!(export.references.len(), 3);
1239 assert!(export.reference_paths.is_empty());
1240 assert_eq!(export.reference_paths.capacity(), 0);
1241 assert_eq!(export.reference_path(1), None);
1242 assert!(export.has_reference_from(FileId(1), None));
1243 assert!(!export.has_reference_from(FileId(9), None));
1244 }
1245
1246 #[test]
1247 fn push_reference_backfills_the_side_table_on_the_first_tracked_path() {
1248 let mut export = ExportSymbol {
1249 name: ExportName::Named("value".to_string()),
1250 is_type_only: false,
1251 is_side_effect_used: false,
1252 visibility: VisibilityTag::None,
1253 expected_unused_reason: None,
1254 span: oxc_span::Span::default(),
1255 references: Vec::new(),
1256 reference_paths: Vec::new(),
1257 members: Vec::new(),
1258 };
1259 let reference = SymbolReference {
1260 from_file: FileId(0),
1261 kind: ReferenceKind::NamedImport,
1262 import_span: oxc_span::Span::default(),
1263 };
1264 export.push_reference(reference, None);
1265 let tracked = ReferencePathId::from_index(4);
1266 export.push_reference(reference, Some(tracked));
1267 export.push_reference(reference, None);
1268
1269 assert_eq!(export.reference_paths, vec![None, Some(tracked), None]);
1270 assert_eq!(export.reference_path(0), None);
1271 assert_eq!(export.reference_path(1), Some(tracked));
1272 assert!(export.has_reference_from(FileId(0), Some(tracked)));
1273 assert!(!export.has_reference_from(FileId(0), Some(ReferencePathId::from_index(7))));
1274 }
1275
1276 #[test]
1277 fn module_node_construction() {
1278 let mut node = ModuleNode {
1279 file_id: FileId(0),
1280 path: PathBuf::from("/project/src/index.ts"),
1281 edge_range: 0..5,
1282 exports: vec![],
1283 re_exports: vec![],
1284 flags: ModuleNode::flags_from(true, true, false),
1285 };
1286 node.set_reachable(true);
1287 assert_eq!(node.file_id, FileId(0));
1288 assert!(node.is_entry_point());
1289 assert!(node.is_reachable());
1290 assert!(node.is_runtime_reachable());
1291 assert!(!node.is_test_reachable());
1292 assert!(!node.has_cjs_exports());
1293 assert_eq!(node.edge_range, 0..5);
1294 }
1295
1296 #[test]
1297 fn module_node_non_entry_unreachable() {
1298 let node = ModuleNode {
1299 file_id: FileId(5),
1300 path: PathBuf::from("/project/src/orphan.ts"),
1301 edge_range: 0..0,
1302 exports: vec![],
1303 re_exports: vec![],
1304 flags: ModuleNode::flags_from(false, false, false),
1305 };
1306 assert!(!node.is_entry_point());
1307 assert!(!node.is_reachable());
1308 assert!(!node.is_runtime_reachable());
1309 assert!(!node.is_test_reachable());
1310 assert!(node.edge_range.is_empty());
1311 }
1312
1313 #[test]
1314 fn module_node_cjs_exports() {
1315 let mut node = ModuleNode {
1316 file_id: FileId(2),
1317 path: PathBuf::from("/project/lib/legacy.js"),
1318 edge_range: 3..7,
1319 exports: vec![],
1320 re_exports: vec![],
1321 flags: ModuleNode::flags_from(false, true, true),
1322 };
1323 node.set_reachable(true);
1324 assert!(node.has_cjs_exports());
1325 assert!(node.is_runtime_reachable());
1326 assert_eq!(node.edge_range.len(), 4);
1327 }
1328
1329 #[test]
1330 fn module_node_with_exports_and_re_exports() {
1331 let node = ModuleNode {
1332 file_id: FileId(1),
1333 path: PathBuf::from("/project/src/barrel.ts"),
1334 edge_range: 0..3,
1335 exports: vec![ExportSymbol {
1336 name: ExportName::Named("localFn".to_string()),
1337 is_type_only: false,
1338 is_side_effect_used: false,
1339 visibility: VisibilityTag::None,
1340 expected_unused_reason: None,
1341 span: oxc_span::Span::new(0, 20),
1342 references: vec![],
1343 reference_paths: Vec::new(),
1344 members: vec![],
1345 }],
1346 re_exports: vec![ReExportEdge {
1347 source_file: FileId(2),
1348 imported_name: "*".to_string(),
1349 exported_name: "*".to_string(),
1350 is_type_only: false,
1351 span: oxc_span::Span::default(),
1352 }],
1353 flags: ModuleNode::flags_from(false, true, false),
1354 };
1355 assert_eq!(node.exports.len(), 1);
1356 assert_eq!(node.re_exports.len(), 1);
1357 assert_eq!(node.re_exports[0].source_file, FileId(2));
1358 }
1359}