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_reachable(&mut self, v: bool) {
73 if v {
74 self.flags |= FLAG_REACHABLE;
75 } else {
76 self.flags &= !FLAG_REACHABLE;
77 }
78 }
79
80 #[inline]
82 pub(crate) fn set_runtime_reachable(&mut self, v: bool) {
83 if v {
84 self.flags |= FLAG_RUNTIME_REACHABLE;
85 } else {
86 self.flags &= !FLAG_RUNTIME_REACHABLE;
87 }
88 }
89
90 #[inline]
92 pub(crate) fn set_test_reachable(&mut self, v: bool) {
93 if v {
94 self.flags |= FLAG_TEST_REACHABLE;
95 } else {
96 self.flags &= !FLAG_TEST_REACHABLE;
97 }
98 }
99
100 #[inline]
102 pub fn set_cjs_exports(&mut self, v: bool) {
103 if v {
104 self.flags |= FLAG_CJS_EXPORTS;
105 } else {
106 self.flags &= !FLAG_CJS_EXPORTS;
107 }
108 }
109
110 #[inline]
112 pub(crate) fn flags_from(
113 is_entry_point: bool,
114 is_runtime_reachable: bool,
115 has_cjs_exports: bool,
116 ) -> u8 {
117 let mut f = 0u8;
118 if is_entry_point {
119 f |= FLAG_ENTRY_POINT;
120 }
121 if is_runtime_reachable {
122 f |= FLAG_RUNTIME_REACHABLE;
123 }
124 if has_cjs_exports {
125 f |= FLAG_CJS_EXPORTS;
126 }
127 f
128 }
129}
130
131#[derive(Debug, serde::Serialize, serde::Deserialize)]
133pub struct ReExportEdge {
134 pub source_file: FileId,
136 pub imported_name: String,
138 pub exported_name: String,
140 pub is_type_only: bool,
142 #[serde(with = "crate::cache::span_serde")]
146 pub span: oxc_span::Span,
147}
148
149#[derive(Debug, serde::Serialize, serde::Deserialize)]
151pub struct ExportSymbol {
152 pub name: ExportName,
154 pub is_type_only: bool,
156 pub is_side_effect_used: bool,
161 pub visibility: VisibilityTag,
164 pub expected_unused_reason: Option<String>,
166 pub deprecated: bool,
168 pub deprecated_reason: Option<Box<str>>,
170 #[serde(with = "crate::cache::span_serde")]
172 pub span: oxc_span::Span,
173 pub references: Vec<SymbolReference>,
175 #[serde(default)]
184 pub reference_paths: Vec<Option<ReferencePathId>>,
185 #[serde(with = "crate::cache::member_serde")]
192 pub members: Vec<fallow_types::extract::MemberInfo>,
193}
194
195#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
197pub struct SymbolReference {
198 pub from_file: FileId,
200 pub kind: ReferenceKind,
202 pub namespace: super::ExportNamespace,
204 #[serde(with = "crate::cache::span_serde")]
207 pub import_span: oxc_span::Span,
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
215pub struct ReferencePathId(NonZeroU32);
216
217#[derive(Clone, Copy)]
221pub(crate) struct RoutedReference {
222 pub(crate) reference: SymbolReference,
223 pub(crate) path: Option<ReferencePathId>,
224}
225
226pub(crate) type RoutedReferenceKey = (
227 FileId,
228 oxc_span::Span,
229 Option<ReferencePathId>,
230 super::ExportNamespace,
231);
232
233impl RoutedReference {
234 pub(crate) const fn key(self) -> RoutedReferenceKey {
235 (
236 self.reference.from_file,
237 self.reference.import_span,
238 self.path,
239 self.reference.namespace,
240 )
241 }
242}
243
244impl ExportSymbol {
245 const INLINE_PHYSICAL_REFERENCE_LIMIT: usize = 8;
246
247 pub fn references_in(
249 &self,
250 namespace: super::ExportNamespace,
251 ) -> impl Iterator<Item = &SymbolReference> {
252 self.references
253 .iter()
254 .filter(move |reference| reference.namespace == namespace)
255 }
256
257 pub fn physical_references(&self) -> impl Iterator<Item = &SymbolReference> {
260 let mut seen = (self.references.len() > Self::INLINE_PHYSICAL_REFERENCE_LIMIT)
261 .then(FxHashSet::default);
262 self.references
263 .iter()
264 .enumerate()
265 .filter(move |(index, reference)| {
266 let key = (
267 reference.from_file,
268 reference.import_span,
269 self.reference_path(*index),
270 );
271 if let Some(seen) = &mut seen {
272 return seen.insert(key);
273 }
274 !(0..*index).any(|prior_index| {
275 let prior = &self.references[prior_index];
276 key == (
277 prior.from_file,
278 prior.import_span,
279 self.reference_path(prior_index),
280 )
281 })
282 })
283 .map(|(_, reference)| reference)
284 }
285
286 pub(crate) fn reference_path(&self, index: usize) -> Option<ReferencePathId> {
288 self.reference_paths.get(index).copied().flatten()
289 }
290
291 pub(crate) fn has_reference_from(
294 &self,
295 from_file: FileId,
296 import_span: oxc_span::Span,
297 path: Option<ReferencePathId>,
298 namespace: super::ExportNamespace,
299 ) -> bool {
300 self.references
301 .iter()
302 .enumerate()
303 .any(|(index, reference)| {
304 reference.from_file == from_file
305 && reference.import_span == import_span
306 && self.reference_path(index) == path
307 && reference.namespace == namespace
308 })
309 }
310
311 pub(crate) fn push_reference(
316 &mut self,
317 reference: SymbolReference,
318 path: Option<ReferencePathId>,
319 ) {
320 if path.is_some() || !self.reference_paths.is_empty() {
321 self.reference_paths.resize(self.references.len(), None);
322 self.reference_paths.push(path);
323 }
324 self.references.push(reference);
325 }
326
327 pub(crate) fn routed_references(&self) -> impl Iterator<Item = RoutedReference> + '_ {
329 self.references
330 .iter()
331 .enumerate()
332 .map(|(index, reference)| RoutedReference {
333 reference: *reference,
334 path: self.reference_path(index),
335 })
336 }
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
341pub(crate) enum ReferencePathNode {
342 Hop {
344 parent: Option<ReferencePathId>,
346 target: FileId,
348 mechanism: ModuleLoadMechanism,
350 },
351 Route {
353 parent: Option<ReferencePathId>,
356 graph: ReferenceRouteGraphId,
358 start: ReferenceRouteNodeId,
360 terminal: ReferenceRouteNodeId,
362 start_mechanism: Option<ModuleLoadMechanism>,
366 },
367}
368
369impl ReferencePathNode {
370 pub(crate) const fn parent(self) -> Option<ReferencePathId> {
371 match self {
372 Self::Hop { parent, .. } | Self::Route { parent, .. } => parent,
373 }
374 }
375
376 fn remap_parent(&mut self, remap: &[ReferencePathId]) {
377 match self {
378 Self::Hop { parent, .. } | Self::Route { parent, .. } => {
379 *parent = parent.map(|path| remap[path.index()]);
380 }
381 }
382 }
383}
384
385#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
387pub(crate) struct ReferenceRouteGraphId(pub(crate) u32);
388
389#[derive(
391 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
392)]
393pub(crate) struct ReferenceRouteNodeId(pub(crate) u32);
394
395#[derive(Debug, Clone, PartialEq, Eq, Hash)]
397pub(crate) struct ReferenceRouteNodeSpec {
398 target: FileId,
399 mechanism: ModuleLoadMechanism,
400 successors: Vec<ReferenceRouteNodeId>,
401}
402
403impl ReferenceRouteNodeSpec {
404 pub(crate) fn new(
405 target: FileId,
406 mechanism: ModuleLoadMechanism,
407 mut successors: Vec<ReferenceRouteNodeId>,
408 ) -> Self {
409 successors.sort_unstable_by_key(|successor| successor.0);
410 successors.dedup();
411 Self {
412 target,
413 mechanism,
414 successors,
415 }
416 }
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Hash)]
421pub(crate) struct ReferenceRouteGraphSpec {
422 nodes: Vec<ReferenceRouteNodeSpec>,
423}
424
425impl ReferenceRouteGraphSpec {
426 pub(crate) fn new(nodes: Vec<ReferenceRouteNodeSpec>) -> Self {
427 debug_assert!(nodes.iter().all(|node| {
428 node.successors
429 .iter()
430 .all(|successor| successor.0 < nodes.len() as u32)
431 }));
432 Self { nodes }
433 }
434}
435
436#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
438pub(crate) struct ReferenceRouteGraph {
439 pub(crate) nodes: Range<u32>,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
444pub(crate) struct ReferenceRouteNode {
445 pub(crate) target: FileId,
446 pub(crate) mechanism: ModuleLoadMechanism,
447 pub(crate) successors: Range<u32>,
448}
449
450#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
452pub(crate) struct ReferenceRoutes {
453 pub(crate) graphs: Vec<ReferenceRouteGraph>,
454 pub(crate) nodes: Vec<ReferenceRouteNode>,
455 pub(crate) edges: Vec<ReferenceRouteNodeId>,
456}
457
458impl ReferenceRoutes {
459 #[cfg(test)]
460 pub(crate) fn canonical_hops(
461 &self,
462 graph_id: ReferenceRouteGraphId,
463 start: ReferenceRouteNodeId,
464 terminal: ReferenceRouteNodeId,
465 start_mechanism: Option<ModuleLoadMechanism>,
466 ) -> Vec<(FileId, ModuleLoadMechanism)> {
467 let Some(graph) = self.graphs.get(graph_id.0 as usize) else {
468 return Vec::new();
469 };
470 let node_count = graph.nodes.end.saturating_sub(graph.nodes.start) as usize;
471 let start_index = start.0 as usize;
472 let terminal_index = terminal.0 as usize;
473 if start_index >= node_count || terminal_index >= node_count {
474 return Vec::new();
475 }
476
477 let mut predecessor = vec![None; node_count];
478 let mut visited = vec![false; node_count];
479 let mut queue = std::collections::VecDeque::from([start_index]);
480 visited[start_index] = true;
481 while let Some(local_index) = queue.pop_front() {
482 if local_index == terminal_index {
483 break;
484 }
485 let Some(node) = self.nodes.get(graph.nodes.start as usize + local_index) else {
486 return Vec::new();
487 };
488 let Some(successors) = self
489 .edges
490 .get(node.successors.start as usize..node.successors.end as usize)
491 else {
492 return Vec::new();
493 };
494 for successor in successors {
495 let successor_index = successor.0 as usize;
496 if successor_index >= node_count || visited[successor_index] {
497 continue;
498 }
499 visited[successor_index] = true;
500 predecessor[successor_index] = Some(local_index);
501 queue.push_back(successor_index);
502 }
503 }
504 if !visited[terminal_index] {
505 return Vec::new();
506 }
507
508 let mut hops = Vec::new();
509 let mut current = terminal_index;
510 loop {
511 let node = &self.nodes[graph.nodes.start as usize + current];
512 if current != start_index {
513 hops.push((node.target, node.mechanism));
514 } else {
515 if let Some(mechanism) = start_mechanism {
516 hops.push((node.target, mechanism));
517 }
518 break;
519 }
520 let Some(parent) = predecessor[current] else {
521 return Vec::new();
522 };
523 current = parent;
524 }
525 hops
526 }
527}
528
529#[derive(Debug, PartialEq, Eq)]
531pub(crate) struct FinalizedReferencePaths {
532 pub(crate) paths: Vec<ReferencePathNode>,
533 pub(crate) routes: ReferenceRoutes,
534}
535
536pub(crate) struct ReferencePathInterner {
538 track_provenance: bool,
539 nodes: Vec<ReferencePathNode>,
540 metadata: Vec<ReferencePathMetadata>,
541 ids: FxHashMap<ReferencePathNode, ReferencePathId>,
542 route_graphs: Vec<ReferenceRouteGraphSpec>,
543 route_graph_ids: FxHashMap<ReferenceRouteGraphSpec, ReferenceRouteGraphId>,
544}
545
546#[derive(Clone, Copy)]
547struct ReferencePathMetadata {
548 depth: usize,
549 hop_target_bounds: Option<(FileId, FileId)>,
550}
551
552impl Default for ReferencePathInterner {
553 fn default() -> Self {
554 Self::new(true)
555 }
556}
557
558impl ReferencePathInterner {
559 pub(crate) fn new(track_provenance: bool) -> Self {
560 Self {
561 track_provenance,
562 nodes: Vec::new(),
563 metadata: Vec::new(),
564 ids: FxHashMap::default(),
565 route_graphs: Vec::new(),
566 route_graph_ids: FxHashMap::default(),
567 }
568 }
569
570 pub(crate) const fn tracks_provenance(&self) -> bool {
571 self.track_provenance
572 }
573
574 pub(crate) fn direct(
576 &mut self,
577 target: FileId,
578 mechanism: ModuleLoadMechanism,
579 ) -> Option<ReferencePathId> {
580 self.track_provenance.then(|| {
581 self.intern(ReferencePathNode::Hop {
582 parent: None,
583 target,
584 mechanism,
585 })
586 })
587 }
588
589 pub(crate) fn extend(
591 &mut self,
592 parent: Option<ReferencePathId>,
593 target: FileId,
594 mechanism: ModuleLoadMechanism,
595 ) -> Option<ReferencePathId> {
596 if !self.track_provenance {
597 debug_assert!(parent.is_none());
598 return None;
599 }
600 let Some(parent) = parent else {
601 debug_assert!(false, "tracked reference paths require an interned parent");
602 return None;
603 };
604 let may_contain_target = self
605 .metadata
606 .get(parent.index())
607 .and_then(|metadata| metadata.hop_target_bounds)
608 .is_some_and(|(minimum, maximum)| target.0 >= minimum.0 && target.0 <= maximum.0);
609 if may_contain_target && self.contains_target(parent, target) {
610 return Some(parent);
611 }
612 Some(self.intern(ReferencePathNode::Hop {
613 parent: Some(parent),
614 target,
615 mechanism,
616 }))
617 }
618
619 pub(crate) fn intern_route_graph(
621 &mut self,
622 graph: ReferenceRouteGraphSpec,
623 ) -> ReferenceRouteGraphId {
624 debug_assert!(self.track_provenance);
625 if let Some(id) = self.route_graph_ids.get(&graph) {
626 return *id;
627 }
628 let id = ReferenceRouteGraphId(self.route_graphs.len() as u32);
629 self.route_graphs.push(graph.clone());
630 self.route_graph_ids.insert(graph, id);
631 id
632 }
633
634 pub(crate) fn route(
636 &mut self,
637 parent: Option<ReferencePathId>,
638 graph: ReferenceRouteGraphId,
639 start: ReferenceRouteNodeId,
640 terminal: ReferenceRouteNodeId,
641 start_mechanism: Option<ModuleLoadMechanism>,
642 ) -> Option<ReferencePathId> {
643 if !self.track_provenance {
644 return None;
645 }
646 Some(self.intern(ReferencePathNode::Route {
647 parent,
648 graph,
649 start,
650 terminal,
651 start_mechanism,
652 }))
653 }
654
655 fn contains_target(&self, mut path: ReferencePathId, target: FileId) -> bool {
656 loop {
657 let Some(node) = self.nodes.get(path.index()) else {
658 return false;
659 };
660 if let ReferencePathNode::Hop {
661 target: hop_target, ..
662 } = node
663 && *hop_target == target
664 {
665 return true;
666 }
667 let Some(parent) = node.parent() else {
668 return false;
669 };
670 path = parent;
671 }
672 }
673
674 fn intern(&mut self, node: ReferencePathNode) -> ReferencePathId {
675 if let Some(path) = self.ids.get(&node) {
676 return *path;
677 }
678 let path = ReferencePathId::from_index(self.nodes.len());
679 let parent_metadata = node
680 .parent()
681 .and_then(|parent| self.metadata.get(parent.index()).copied());
682 let depth = parent_metadata.map_or(0, |metadata| metadata.depth + 1);
683 let hop_target_bounds = match node {
684 ReferencePathNode::Hop { target, .. } => Some(
685 parent_metadata
686 .and_then(|metadata| metadata.hop_target_bounds)
687 .map_or((target, target), |(minimum, maximum)| {
688 (
689 FileId(minimum.0.min(target.0)),
690 FileId(maximum.0.max(target.0)),
691 )
692 }),
693 ),
694 ReferencePathNode::Route { .. } => {
695 parent_metadata.and_then(|metadata| metadata.hop_target_bounds)
696 }
697 };
698 self.nodes.push(node);
699 self.metadata.push(ReferencePathMetadata {
700 depth,
701 hop_target_bounds,
702 });
703 self.ids.insert(node, path);
704 path
705 }
706
707 pub(crate) fn finalize(self, modules: &mut [ModuleNode]) -> FinalizedReferencePaths {
713 if self.nodes.is_empty() && self.route_graphs.is_empty() {
714 return FinalizedReferencePaths {
715 paths: Vec::new(),
716 routes: ReferenceRoutes::default(),
717 };
718 }
719
720 let (routes, route_remap) = finalize_route_graphs(&self.route_graphs);
721 let max_depth = self
722 .metadata
723 .iter()
724 .map(|metadata| metadata.depth)
725 .max()
726 .unwrap_or(0);
727
728 let mut paths_by_depth = vec![Vec::new(); max_depth.saturating_add(1)];
729 for (old_index, metadata) in self.metadata.iter().enumerate() {
730 paths_by_depth[metadata.depth].push(old_index);
731 }
732
733 let mut remap = vec![ReferencePathId::from_index(0); self.nodes.len()];
734 let mut finalized = Vec::with_capacity(self.nodes.len());
735 for mut paths in paths_by_depth {
736 paths.sort_unstable_by(|&left, &right| {
737 compare_path_nodes(self.nodes[left], self.nodes[right], &remap, &route_remap)
738 });
739 for old_index in paths {
740 let mut node = self.nodes[old_index];
741 node.remap_parent(&remap);
742 if let ReferencePathNode::Route { graph, .. } = &mut node {
743 *graph = route_remap[graph.0 as usize];
744 }
745 let canonical = ReferencePathId::from_index(finalized.len());
746 remap[old_index] = canonical;
747 finalized.push(node);
748 }
749 }
750
751 for path in modules
752 .iter_mut()
753 .flat_map(|module| &mut module.exports)
754 .flat_map(|export| &mut export.reference_paths)
755 {
756 if let Some(existing) = *path {
757 *path = Some(remap[existing.index()]);
758 }
759 }
760
761 FinalizedReferencePaths {
762 paths: finalized,
763 routes,
764 }
765 }
766}
767
768fn compare_path_nodes(
769 left: ReferencePathNode,
770 right: ReferencePathNode,
771 path_remap: &[ReferencePathId],
772 route_remap: &[ReferenceRouteGraphId],
773) -> Ordering {
774 let left_parent = left.parent().map(|parent| path_remap[parent.index()].0);
775 let right_parent = right.parent().map(|parent| path_remap[parent.index()].0);
776 left_parent
777 .cmp(&right_parent)
778 .then_with(|| match (left, right) {
779 (
780 ReferencePathNode::Hop {
781 target: left_target,
782 mechanism: left_mechanism,
783 ..
784 },
785 ReferencePathNode::Hop {
786 target: right_target,
787 mechanism: right_mechanism,
788 ..
789 },
790 ) => {
791 (left_target.0, left_mechanism as u8).cmp(&(right_target.0, right_mechanism as u8))
792 }
793 (ReferencePathNode::Hop { .. }, ReferencePathNode::Route { .. }) => Ordering::Less,
794 (ReferencePathNode::Route { .. }, ReferencePathNode::Hop { .. }) => Ordering::Greater,
795 (
796 ReferencePathNode::Route {
797 graph: left_graph,
798 start: left_start,
799 terminal: left_terminal,
800 start_mechanism: left_mechanism,
801 ..
802 },
803 ReferencePathNode::Route {
804 graph: right_graph,
805 start: right_start,
806 terminal: right_terminal,
807 start_mechanism: right_mechanism,
808 ..
809 },
810 ) => (
811 route_remap[left_graph.0 as usize].0,
812 left_start.0,
813 left_terminal.0,
814 left_mechanism.map(|mechanism| mechanism as u8),
815 )
816 .cmp(&(
817 route_remap[right_graph.0 as usize].0,
818 right_start.0,
819 right_terminal.0,
820 right_mechanism.map(|mechanism| mechanism as u8),
821 )),
822 })
823}
824
825fn compare_route_graph_specs(
826 left: &ReferenceRouteGraphSpec,
827 right: &ReferenceRouteGraphSpec,
828) -> Ordering {
829 left.nodes.len().cmp(&right.nodes.len()).then_with(|| {
830 left.nodes
831 .iter()
832 .zip(&right.nodes)
833 .find_map(|(left_node, right_node)| {
834 let ordering = (
835 left_node.target.0,
836 left_node.mechanism as u8,
837 &left_node.successors,
838 )
839 .cmp(&(
840 right_node.target.0,
841 right_node.mechanism as u8,
842 &right_node.successors,
843 ));
844 (ordering != Ordering::Equal).then_some(ordering)
845 })
846 .unwrap_or(Ordering::Equal)
847 })
848}
849
850fn finalize_route_graphs(
851 graphs: &[ReferenceRouteGraphSpec],
852) -> (ReferenceRoutes, Vec<ReferenceRouteGraphId>) {
853 let mut order: Vec<usize> = (0..graphs.len()).collect();
854 order
855 .sort_unstable_by(|&left, &right| compare_route_graph_specs(&graphs[left], &graphs[right]));
856
857 let mut remap = vec![ReferenceRouteGraphId(0); graphs.len()];
858 let mut finalized = ReferenceRoutes::default();
859 for old_index in order {
860 let graph_id = ReferenceRouteGraphId(finalized.graphs.len() as u32);
861 remap[old_index] = graph_id;
862 let node_start = finalized.nodes.len() as u32;
863 for node in &graphs[old_index].nodes {
864 let edge_start = finalized.edges.len() as u32;
865 finalized.edges.extend_from_slice(&node.successors);
866 finalized.nodes.push(ReferenceRouteNode {
867 target: node.target,
868 mechanism: node.mechanism,
869 successors: edge_start..finalized.edges.len() as u32,
870 });
871 }
872 finalized.graphs.push(ReferenceRouteGraph {
873 nodes: node_start..finalized.nodes.len() as u32,
874 });
875 }
876 (finalized, remap)
877}
878
879impl ReferencePathId {
880 fn from_index(index: usize) -> Self {
881 let Some(encoded) = u32::try_from(index)
882 .ok()
883 .and_then(|index| index.checked_add(1))
884 .and_then(NonZeroU32::new)
885 else {
886 panic!("a process cannot allocate more than u32::MAX reference path nodes");
887 };
888 Self(encoded)
889 }
890
891 pub(crate) const fn index(self) -> usize {
892 (self.0.get() - 1) as usize
893 }
894}
895
896#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
898pub enum ReferenceKind {
899 NamedImport,
901 DefaultImport,
903 NamespaceImport,
905 ReExport,
907 DynamicImport,
909 SideEffectImport,
911}
912
913#[cfg(target_pointer_width = "64")]
914const _: () = assert!(std::mem::size_of::<ExportSymbol>() == 152);
915#[cfg(target_pointer_width = "64")]
916const _: () = assert!(std::mem::size_of::<SymbolReference>() == 16);
917#[cfg(target_pointer_width = "64")]
918const _: () = assert!(std::mem::size_of::<ReExportEdge>() == 64);
919#[cfg(all(target_pointer_width = "64", unix))]
920const _: () = assert!(std::mem::size_of::<ModuleNode>() == 96);
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925 use crate::graph::ExportNamespace;
926
927 fn module_with_reference_paths(paths: &[Option<ReferencePathId>]) -> ModuleNode {
928 ModuleNode {
929 file_id: FileId(0),
930 path: PathBuf::from("/project/source.ts"),
931 edge_range: 0..0,
932 exports: vec![ExportSymbol {
933 name: ExportName::Named("value".to_string()),
934 is_type_only: false,
935 is_side_effect_used: false,
936 visibility: VisibilityTag::None,
937 expected_unused_reason: None,
938 span: oxc_span::Span::default(),
939 references: paths
940 .iter()
941 .map(|_| SymbolReference {
942 from_file: FileId(0),
943 kind: ReferenceKind::NamedImport,
944 namespace: ExportNamespace::Value,
945 import_span: oxc_span::Span::default(),
946 })
947 .collect(),
948 reference_paths: paths.to_vec(),
949 members: Vec::new(),
950 deprecated: false,
951 deprecated_reason: None,
952 }],
953 re_exports: Vec::new(),
954 flags: 0,
955 }
956 }
957
958 fn export_with_reference_files(files: &[u32]) -> ExportSymbol {
959 ExportSymbol {
960 name: ExportName::Named("value".to_string()),
961 is_type_only: false,
962 is_side_effect_used: false,
963 visibility: VisibilityTag::None,
964 expected_unused_reason: None,
965 span: oxc_span::Span::default(),
966 references: files
967 .iter()
968 .map(|file| SymbolReference {
969 from_file: FileId(*file),
970 kind: ReferenceKind::NamedImport,
971 namespace: ExportNamespace::Value,
972 import_span: oxc_span::Span::default(),
973 })
974 .collect(),
975 reference_paths: Vec::new(),
976 members: Vec::new(),
977 deprecated: false,
978 deprecated_reason: None,
979 }
980 }
981
982 #[test]
983 fn physical_references_deduplicate_small_and_large_sets() {
984 let small = export_with_reference_files(&[0, 1, 2, 3, 4, 5, 6, 0]);
985 let large = export_with_reference_files(&[0, 1, 2, 3, 4, 5, 6, 7, 0]);
986
987 assert_eq!(small.physical_references().count(), 7);
988 assert_eq!(large.physical_references().count(), 8);
989 }
990
991 #[test]
992 fn reference_path_metadata_tracks_exact_depth_and_hop_bounds() {
993 let mut interner = ReferencePathInterner::default();
994 let root = interner
995 .direct(FileId(10), ModuleLoadMechanism::EsModule)
996 .expect("tracked interner must return a path");
997 let lower = interner
998 .extend(Some(root), FileId(5), ModuleLoadMechanism::EsModule)
999 .expect("tracked interner must extend a path");
1000 let upper = interner
1001 .extend(Some(lower), FileId(20), ModuleLoadMechanism::EsModule)
1002 .expect("tracked interner must extend a path");
1003
1004 assert_eq!(interner.metadata[root.index()].depth, 0);
1005 assert_eq!(
1006 interner.metadata[lower.index()].hop_target_bounds,
1007 Some((FileId(5), FileId(10)))
1008 );
1009 assert_eq!(interner.metadata[upper.index()].depth, 2);
1010 assert_eq!(
1011 interner.metadata[upper.index()].hop_target_bounds,
1012 Some((FileId(5), FileId(20)))
1013 );
1014
1015 let repeated = interner.extend(Some(upper), FileId(10), ModuleLoadMechanism::EsModule);
1016 assert_eq!(repeated, Some(upper));
1017 }
1018
1019 #[test]
1020 fn finalized_reference_paths_are_independent_of_interning_order() {
1021 let mut first = ReferencePathInterner::default();
1022 let first_parent = first.direct(FileId(1), ModuleLoadMechanism::EsModule);
1023 let first_direct = first.direct(FileId(2), ModuleLoadMechanism::CommonJsRequire);
1024 let first_chain = first.extend(first_parent, FileId(3), ModuleLoadMechanism::EsModule);
1025 let mut first_modules = vec![module_with_reference_paths(&[first_direct, first_chain])];
1026 let first_nodes = first.finalize(&mut first_modules);
1027
1028 let mut second = ReferencePathInterner::default();
1029 let second_direct = second.direct(FileId(2), ModuleLoadMechanism::CommonJsRequire);
1030 let second_parent = second.direct(FileId(1), ModuleLoadMechanism::EsModule);
1031 let second_chain = second.extend(second_parent, FileId(3), ModuleLoadMechanism::EsModule);
1032 let mut second_modules = vec![module_with_reference_paths(&[second_direct, second_chain])];
1033 let second_nodes = second.finalize(&mut second_modules);
1034
1035 assert_eq!(first_nodes, second_nodes);
1036 assert_eq!(
1037 first_modules[0].exports[0].reference_paths,
1038 second_modules[0].exports[0].reference_paths
1039 );
1040 }
1041
1042 fn two_hop_route(first: FileId, second: FileId) -> ReferenceRouteGraphSpec {
1043 ReferenceRouteGraphSpec::new(vec![
1044 ReferenceRouteNodeSpec::new(
1045 first,
1046 ModuleLoadMechanism::EsModule,
1047 vec![ReferenceRouteNodeId(1)],
1048 ),
1049 ReferenceRouteNodeSpec::new(second, ModuleLoadMechanism::EsModule, Vec::new()),
1050 ])
1051 }
1052
1053 #[test]
1054 fn finalized_reference_routes_are_independent_of_interning_order() {
1055 let route_a = two_hop_route(FileId(1), FileId(2));
1056 let route_b = two_hop_route(FileId(3), FileId(4));
1057
1058 let mut first = ReferencePathInterner::default();
1059 let first_a = first.intern_route_graph(route_a.clone());
1060 let first_b = first.intern_route_graph(route_b.clone());
1061 let first_b_path = first.route(
1062 None,
1063 first_b,
1064 ReferenceRouteNodeId(0),
1065 ReferenceRouteNodeId(1),
1066 Some(ModuleLoadMechanism::CommonJsRequire),
1067 );
1068 let first_a_path = first.route(
1069 None,
1070 first_a,
1071 ReferenceRouteNodeId(0),
1072 ReferenceRouteNodeId(1),
1073 Some(ModuleLoadMechanism::EsModule),
1074 );
1075 let mut first_modules = vec![module_with_reference_paths(&[first_b_path, first_a_path])];
1076 let first_paths = first.finalize(&mut first_modules);
1077
1078 let mut second = ReferencePathInterner::default();
1079 let second_b = second.intern_route_graph(route_b);
1080 let second_a = second.intern_route_graph(route_a);
1081 let second_b_path = second.route(
1082 None,
1083 second_b,
1084 ReferenceRouteNodeId(0),
1085 ReferenceRouteNodeId(1),
1086 Some(ModuleLoadMechanism::CommonJsRequire),
1087 );
1088 let second_a_path = second.route(
1089 None,
1090 second_a,
1091 ReferenceRouteNodeId(0),
1092 ReferenceRouteNodeId(1),
1093 Some(ModuleLoadMechanism::EsModule),
1094 );
1095 let mut second_modules = vec![module_with_reference_paths(&[second_b_path, second_a_path])];
1096 let second_paths = second.finalize(&mut second_modules);
1097
1098 assert_eq!(first_paths, second_paths);
1099 assert_eq!(
1100 first_modules[0].exports[0].reference_paths,
1101 second_modules[0].exports[0].reference_paths
1102 );
1103 }
1104
1105 #[test]
1106 fn push_reference_without_paths_never_allocates_the_side_table() {
1107 let mut export = ExportSymbol {
1108 name: ExportName::Named("value".to_string()),
1109 is_type_only: false,
1110 is_side_effect_used: false,
1111 visibility: VisibilityTag::None,
1112 expected_unused_reason: None,
1113 span: oxc_span::Span::default(),
1114 references: Vec::new(),
1115 reference_paths: Vec::new(),
1116 members: Vec::new(),
1117 deprecated: false,
1118 deprecated_reason: None,
1119 };
1120 for id in 0..3 {
1121 export.push_reference(
1122 SymbolReference {
1123 from_file: FileId(id),
1124 kind: ReferenceKind::NamedImport,
1125 namespace: ExportNamespace::Value,
1126 import_span: oxc_span::Span::default(),
1127 },
1128 None,
1129 );
1130 }
1131 assert_eq!(export.references.len(), 3);
1132 assert!(export.reference_paths.is_empty());
1133 assert_eq!(export.reference_paths.capacity(), 0);
1134 assert_eq!(export.reference_path(1), None);
1135 assert!(export.has_reference_from(
1136 FileId(1),
1137 oxc_span::Span::default(),
1138 None,
1139 ExportNamespace::Value
1140 ));
1141 assert!(!export.has_reference_from(
1142 FileId(9),
1143 oxc_span::Span::default(),
1144 None,
1145 ExportNamespace::Value
1146 ));
1147 }
1148
1149 #[test]
1150 fn push_reference_backfills_the_side_table_on_the_first_tracked_path() {
1151 let mut export = ExportSymbol {
1152 name: ExportName::Named("value".to_string()),
1153 is_type_only: false,
1154 is_side_effect_used: false,
1155 visibility: VisibilityTag::None,
1156 expected_unused_reason: None,
1157 span: oxc_span::Span::default(),
1158 references: Vec::new(),
1159 reference_paths: Vec::new(),
1160 members: Vec::new(),
1161 deprecated: false,
1162 deprecated_reason: None,
1163 };
1164 let reference = SymbolReference {
1165 from_file: FileId(0),
1166 kind: ReferenceKind::NamedImport,
1167 namespace: ExportNamespace::Value,
1168 import_span: oxc_span::Span::default(),
1169 };
1170 export.push_reference(reference, None);
1171 let tracked = ReferencePathId::from_index(4);
1172 export.push_reference(reference, Some(tracked));
1173 export.push_reference(reference, None);
1174
1175 assert_eq!(export.reference_paths, vec![None, Some(tracked), None]);
1176 assert_eq!(export.reference_path(0), None);
1177 assert_eq!(export.reference_path(1), Some(tracked));
1178 assert!(export.has_reference_from(
1179 FileId(0),
1180 reference.import_span,
1181 Some(tracked),
1182 ExportNamespace::Value
1183 ));
1184 assert!(!export.has_reference_from(
1185 FileId(0),
1186 reference.import_span,
1187 Some(ReferencePathId::from_index(7)),
1188 ExportNamespace::Value
1189 ));
1190 }
1191
1192 #[test]
1193 fn module_node_construction() {
1194 let mut node = ModuleNode {
1195 file_id: FileId(0),
1196 path: PathBuf::from("/project/src/index.ts"),
1197 edge_range: 0..5,
1198 exports: vec![],
1199 re_exports: vec![],
1200 flags: ModuleNode::flags_from(true, true, false),
1201 };
1202 node.set_reachable(true);
1203 assert_eq!(node.file_id, FileId(0));
1204 assert!(node.is_entry_point());
1205 assert!(node.is_reachable());
1206 assert!(node.is_runtime_reachable());
1207 assert!(!node.is_test_reachable());
1208 assert!(!node.has_cjs_exports());
1209 assert_eq!(node.edge_range, 0..5);
1210 }
1211
1212 #[test]
1213 fn module_node_non_entry_unreachable() {
1214 let node = ModuleNode {
1215 file_id: FileId(5),
1216 path: PathBuf::from("/project/src/orphan.ts"),
1217 edge_range: 0..0,
1218 exports: vec![],
1219 re_exports: vec![],
1220 flags: ModuleNode::flags_from(false, false, false),
1221 };
1222 assert!(!node.is_entry_point());
1223 assert!(!node.is_reachable());
1224 assert!(!node.is_runtime_reachable());
1225 assert!(!node.is_test_reachable());
1226 assert!(node.edge_range.is_empty());
1227 }
1228
1229 #[test]
1230 fn module_node_cjs_exports() {
1231 let mut node = ModuleNode {
1232 file_id: FileId(2),
1233 path: PathBuf::from("/project/lib/legacy.js"),
1234 edge_range: 3..7,
1235 exports: vec![],
1236 re_exports: vec![],
1237 flags: ModuleNode::flags_from(false, true, true),
1238 };
1239 node.set_reachable(true);
1240 assert!(node.has_cjs_exports());
1241 assert!(node.is_runtime_reachable());
1242 assert_eq!(node.edge_range.len(), 4);
1243 }
1244
1245 #[test]
1246 fn module_node_with_exports_and_re_exports() {
1247 let node = ModuleNode {
1248 file_id: FileId(1),
1249 path: PathBuf::from("/project/src/barrel.ts"),
1250 edge_range: 0..3,
1251 exports: vec![ExportSymbol {
1252 name: ExportName::Named("localFn".to_string()),
1253 is_type_only: false,
1254 is_side_effect_used: false,
1255 visibility: VisibilityTag::None,
1256 expected_unused_reason: None,
1257 span: oxc_span::Span::new(0, 20),
1258 references: vec![],
1259 reference_paths: Vec::new(),
1260 members: vec![],
1261 deprecated: false,
1262 deprecated_reason: None,
1263 }],
1264 re_exports: vec![ReExportEdge {
1265 source_file: FileId(2),
1266 imported_name: "*".to_string(),
1267 exported_name: "*".to_string(),
1268 is_type_only: false,
1269 span: oxc_span::Span::default(),
1270 }],
1271 flags: ModuleNode::flags_from(false, true, false),
1272 };
1273 assert_eq!(node.exports.len(), 1);
1274 assert_eq!(node.re_exports.len(), 1);
1275 assert_eq!(node.re_exports[0].source_file, FileId(2));
1276 }
1277}