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, FxHashSet};
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 pub namespace: super::ExportNamespace,
210 #[serde(with = "crate::cache::span_serde")]
213 pub import_span: oxc_span::Span,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
221pub struct ReferencePathId(NonZeroU32);
222
223#[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 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 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 #[must_use]
294 pub fn physical_reference_count(&self) -> usize {
295 self.physical_references().count()
296 }
297
298 pub(crate) fn reference_path(&self, index: usize) -> Option<ReferencePathId> {
300 self.reference_paths.get(index).copied().flatten()
301 }
302
303 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
353pub(crate) enum ReferencePathNode {
354 Hop {
356 parent: Option<ReferencePathId>,
358 target: FileId,
360 mechanism: ModuleLoadMechanism,
362 },
363 Route {
365 parent: Option<ReferencePathId>,
368 graph: ReferenceRouteGraphId,
370 start: ReferenceRouteNodeId,
372 terminal: ReferenceRouteNodeId,
374 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
399pub(crate) struct ReferenceRouteGraphId(pub(crate) u32);
400
401#[derive(
403 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
404)]
405pub(crate) struct ReferenceRouteNodeId(pub(crate) u32);
406
407#[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#[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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
450pub(crate) struct ReferenceRouteGraph {
451 pub(crate) nodes: Range<u32>,
452}
453
454#[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#[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#[derive(Debug, PartialEq, Eq)]
543pub(crate) struct FinalizedReferencePaths {
544 pub(crate) paths: Vec<ReferencePathNode>,
545 pub(crate) routes: ReferenceRoutes,
546}
547
548pub(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 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
910pub enum ReferenceKind {
911 NamedImport,
913 DefaultImport,
915 NamespaceImport,
917 ReExport,
919 DynamicImport,
921 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}