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 if let Some(mechanism) = start_mechanism {
514 hops.push((node.target, mechanism));
515 }
516 break;
517 }
518 hops.push((node.target, node.mechanism));
519 let Some(parent) = predecessor[current] else {
520 return Vec::new();
521 };
522 current = parent;
523 }
524 hops
525 }
526}
527
528#[derive(Debug, PartialEq, Eq)]
530pub(crate) struct FinalizedReferencePaths {
531 pub(crate) paths: Vec<ReferencePathNode>,
532 pub(crate) routes: ReferenceRoutes,
533}
534
535pub(crate) struct ReferencePathInterner {
537 track_provenance: bool,
538 nodes: Vec<ReferencePathNode>,
539 metadata: Vec<ReferencePathMetadata>,
540 ids: FxHashMap<ReferencePathNode, ReferencePathId>,
541 route_graphs: Vec<ReferenceRouteGraphSpec>,
542 route_graph_ids: FxHashMap<ReferenceRouteGraphSpec, ReferenceRouteGraphId>,
543}
544
545#[derive(Clone, Copy)]
546struct ReferencePathMetadata {
547 depth: usize,
548 hop_target_bounds: Option<(FileId, FileId)>,
549}
550
551impl Default for ReferencePathInterner {
552 fn default() -> Self {
553 Self::new(true)
554 }
555}
556
557impl ReferencePathInterner {
558 pub(crate) fn new(track_provenance: bool) -> Self {
559 Self {
560 track_provenance,
561 nodes: Vec::new(),
562 metadata: Vec::new(),
563 ids: FxHashMap::default(),
564 route_graphs: Vec::new(),
565 route_graph_ids: FxHashMap::default(),
566 }
567 }
568
569 pub(crate) const fn tracks_provenance(&self) -> bool {
570 self.track_provenance
571 }
572
573 pub(crate) fn direct(
575 &mut self,
576 target: FileId,
577 mechanism: ModuleLoadMechanism,
578 ) -> Option<ReferencePathId> {
579 self.track_provenance.then(|| {
580 self.intern(ReferencePathNode::Hop {
581 parent: None,
582 target,
583 mechanism,
584 })
585 })
586 }
587
588 pub(crate) fn extend(
590 &mut self,
591 parent: Option<ReferencePathId>,
592 target: FileId,
593 mechanism: ModuleLoadMechanism,
594 ) -> Option<ReferencePathId> {
595 if !self.track_provenance {
596 debug_assert!(parent.is_none());
597 return None;
598 }
599 let Some(parent) = parent else {
600 debug_assert!(false, "tracked reference paths require an interned parent");
601 return None;
602 };
603 let may_contain_target = self
604 .metadata
605 .get(parent.index())
606 .and_then(|metadata| metadata.hop_target_bounds)
607 .is_some_and(|(minimum, maximum)| target.0 >= minimum.0 && target.0 <= maximum.0);
608 if may_contain_target && self.contains_target(parent, target) {
609 return Some(parent);
610 }
611 Some(self.intern(ReferencePathNode::Hop {
612 parent: Some(parent),
613 target,
614 mechanism,
615 }))
616 }
617
618 pub(crate) fn intern_route_graph(
620 &mut self,
621 graph: ReferenceRouteGraphSpec,
622 ) -> ReferenceRouteGraphId {
623 debug_assert!(self.track_provenance);
624 if let Some(id) = self.route_graph_ids.get(&graph) {
625 return *id;
626 }
627 let id = ReferenceRouteGraphId(self.route_graphs.len() as u32);
628 self.route_graphs.push(graph.clone());
629 self.route_graph_ids.insert(graph, id);
630 id
631 }
632
633 pub(crate) fn route(
635 &mut self,
636 parent: Option<ReferencePathId>,
637 graph: ReferenceRouteGraphId,
638 start: ReferenceRouteNodeId,
639 terminal: ReferenceRouteNodeId,
640 start_mechanism: Option<ModuleLoadMechanism>,
641 ) -> Option<ReferencePathId> {
642 if !self.track_provenance {
643 return None;
644 }
645 Some(self.intern(ReferencePathNode::Route {
646 parent,
647 graph,
648 start,
649 terminal,
650 start_mechanism,
651 }))
652 }
653
654 fn contains_target(&self, mut path: ReferencePathId, target: FileId) -> bool {
655 loop {
656 let Some(node) = self.nodes.get(path.index()) else {
657 return false;
658 };
659 if let ReferencePathNode::Hop {
660 target: hop_target, ..
661 } = node
662 && *hop_target == target
663 {
664 return true;
665 }
666 let Some(parent) = node.parent() else {
667 return false;
668 };
669 path = parent;
670 }
671 }
672
673 fn intern(&mut self, node: ReferencePathNode) -> ReferencePathId {
674 if let Some(path) = self.ids.get(&node) {
675 return *path;
676 }
677 let path = ReferencePathId::from_index(self.nodes.len());
678 let parent_metadata = node
679 .parent()
680 .and_then(|parent| self.metadata.get(parent.index()).copied());
681 let depth = parent_metadata.map_or(0, |metadata| metadata.depth + 1);
682 let hop_target_bounds = match node {
683 ReferencePathNode::Hop { target, .. } => Some(
684 parent_metadata
685 .and_then(|metadata| metadata.hop_target_bounds)
686 .map_or((target, target), |(minimum, maximum)| {
687 (
688 FileId(minimum.0.min(target.0)),
689 FileId(maximum.0.max(target.0)),
690 )
691 }),
692 ),
693 ReferencePathNode::Route { .. } => {
694 parent_metadata.and_then(|metadata| metadata.hop_target_bounds)
695 }
696 };
697 self.nodes.push(node);
698 self.metadata.push(ReferencePathMetadata {
699 depth,
700 hop_target_bounds,
701 });
702 self.ids.insert(node, path);
703 path
704 }
705
706 pub(crate) fn finalize(self, modules: &mut [ModuleNode]) -> FinalizedReferencePaths {
712 if self.nodes.is_empty() && self.route_graphs.is_empty() {
713 return FinalizedReferencePaths {
714 paths: Vec::new(),
715 routes: ReferenceRoutes::default(),
716 };
717 }
718
719 let (routes, route_remap) = finalize_route_graphs(&self.route_graphs);
720 let max_depth = self
721 .metadata
722 .iter()
723 .map(|metadata| metadata.depth)
724 .max()
725 .unwrap_or(0);
726
727 let mut paths_by_depth = vec![Vec::new(); max_depth.saturating_add(1)];
728 for (old_index, metadata) in self.metadata.iter().enumerate() {
729 paths_by_depth[metadata.depth].push(old_index);
730 }
731
732 let mut remap = vec![ReferencePathId::from_index(0); self.nodes.len()];
733 let mut finalized = Vec::with_capacity(self.nodes.len());
734 for mut paths in paths_by_depth {
735 paths.sort_unstable_by(|&left, &right| {
736 compare_path_nodes(self.nodes[left], self.nodes[right], &remap, &route_remap)
737 });
738 for old_index in paths {
739 let mut node = self.nodes[old_index];
740 node.remap_parent(&remap);
741 if let ReferencePathNode::Route { graph, .. } = &mut node {
742 *graph = route_remap[graph.0 as usize];
743 }
744 let canonical = ReferencePathId::from_index(finalized.len());
745 remap[old_index] = canonical;
746 finalized.push(node);
747 }
748 }
749
750 for path in modules
751 .iter_mut()
752 .flat_map(|module| &mut module.exports)
753 .flat_map(|export| &mut export.reference_paths)
754 {
755 if let Some(existing) = *path {
756 *path = Some(remap[existing.index()]);
757 }
758 }
759
760 FinalizedReferencePaths {
761 paths: finalized,
762 routes,
763 }
764 }
765}
766
767fn compare_path_nodes(
768 left: ReferencePathNode,
769 right: ReferencePathNode,
770 path_remap: &[ReferencePathId],
771 route_remap: &[ReferenceRouteGraphId],
772) -> Ordering {
773 let left_parent = left.parent().map(|parent| path_remap[parent.index()].0);
774 let right_parent = right.parent().map(|parent| path_remap[parent.index()].0);
775 left_parent
776 .cmp(&right_parent)
777 .then_with(|| match (left, right) {
778 (
779 ReferencePathNode::Hop {
780 target: left_target,
781 mechanism: left_mechanism,
782 ..
783 },
784 ReferencePathNode::Hop {
785 target: right_target,
786 mechanism: right_mechanism,
787 ..
788 },
789 ) => {
790 (left_target.0, left_mechanism as u8).cmp(&(right_target.0, right_mechanism as u8))
791 }
792 (ReferencePathNode::Hop { .. }, ReferencePathNode::Route { .. }) => Ordering::Less,
793 (ReferencePathNode::Route { .. }, ReferencePathNode::Hop { .. }) => Ordering::Greater,
794 (
795 ReferencePathNode::Route {
796 graph: left_graph,
797 start: left_start,
798 terminal: left_terminal,
799 start_mechanism: left_mechanism,
800 ..
801 },
802 ReferencePathNode::Route {
803 graph: right_graph,
804 start: right_start,
805 terminal: right_terminal,
806 start_mechanism: right_mechanism,
807 ..
808 },
809 ) => (
810 route_remap[left_graph.0 as usize].0,
811 left_start.0,
812 left_terminal.0,
813 left_mechanism.map(|mechanism| mechanism as u8),
814 )
815 .cmp(&(
816 route_remap[right_graph.0 as usize].0,
817 right_start.0,
818 right_terminal.0,
819 right_mechanism.map(|mechanism| mechanism as u8),
820 )),
821 })
822}
823
824fn compare_route_graph_specs(
825 left: &ReferenceRouteGraphSpec,
826 right: &ReferenceRouteGraphSpec,
827) -> Ordering {
828 left.nodes.len().cmp(&right.nodes.len()).then_with(|| {
829 left.nodes
830 .iter()
831 .zip(&right.nodes)
832 .find_map(|(left_node, right_node)| {
833 let ordering = (
834 left_node.target.0,
835 left_node.mechanism as u8,
836 &left_node.successors,
837 )
838 .cmp(&(
839 right_node.target.0,
840 right_node.mechanism as u8,
841 &right_node.successors,
842 ));
843 (ordering != Ordering::Equal).then_some(ordering)
844 })
845 .unwrap_or(Ordering::Equal)
846 })
847}
848
849fn finalize_route_graphs(
850 graphs: &[ReferenceRouteGraphSpec],
851) -> (ReferenceRoutes, Vec<ReferenceRouteGraphId>) {
852 let mut order: Vec<usize> = (0..graphs.len()).collect();
853 order
854 .sort_unstable_by(|&left, &right| compare_route_graph_specs(&graphs[left], &graphs[right]));
855
856 let mut remap = vec![ReferenceRouteGraphId(0); graphs.len()];
857 let mut finalized = ReferenceRoutes::default();
858 for old_index in order {
859 let graph_id = ReferenceRouteGraphId(finalized.graphs.len() as u32);
860 remap[old_index] = graph_id;
861 let node_start = finalized.nodes.len() as u32;
862 for node in &graphs[old_index].nodes {
863 let edge_start = finalized.edges.len() as u32;
864 finalized.edges.extend_from_slice(&node.successors);
865 finalized.nodes.push(ReferenceRouteNode {
866 target: node.target,
867 mechanism: node.mechanism,
868 successors: edge_start..finalized.edges.len() as u32,
869 });
870 }
871 finalized.graphs.push(ReferenceRouteGraph {
872 nodes: node_start..finalized.nodes.len() as u32,
873 });
874 }
875 (finalized, remap)
876}
877
878impl ReferencePathId {
879 fn from_index(index: usize) -> Self {
880 let Some(encoded) = u32::try_from(index)
881 .ok()
882 .and_then(|index| index.checked_add(1))
883 .and_then(NonZeroU32::new)
884 else {
885 panic!("a process cannot allocate more than u32::MAX reference path nodes");
886 };
887 Self(encoded)
888 }
889
890 pub(crate) const fn index(self) -> usize {
891 (self.0.get() - 1) as usize
892 }
893}
894
895#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
897pub enum ReferenceKind {
898 NamedImport,
900 DefaultImport,
902 NamespaceImport,
904 ReExport,
906 DynamicImport,
908 SideEffectImport,
910}
911
912#[cfg(target_pointer_width = "64")]
913const _: () = assert!(std::mem::size_of::<ExportSymbol>() == 152);
914#[cfg(target_pointer_width = "64")]
915const _: () = assert!(std::mem::size_of::<SymbolReference>() == 16);
916#[cfg(target_pointer_width = "64")]
917const _: () = assert!(std::mem::size_of::<ReExportEdge>() == 64);
918#[cfg(all(target_pointer_width = "64", unix))]
919const _: () = assert!(std::mem::size_of::<ModuleNode>() == 96);
920
921#[cfg(test)]
922mod tests {
923 use super::*;
924 use crate::graph::ExportNamespace;
925
926 fn module_with_reference_paths(paths: &[Option<ReferencePathId>]) -> ModuleNode {
927 ModuleNode {
928 file_id: FileId(0),
929 path: PathBuf::from("/project/source.ts"),
930 edge_range: 0..0,
931 exports: vec![ExportSymbol {
932 name: ExportName::Named("value".to_string()),
933 is_type_only: false,
934 is_side_effect_used: false,
935 visibility: VisibilityTag::None,
936 expected_unused_reason: None,
937 span: oxc_span::Span::default(),
938 references: paths
939 .iter()
940 .map(|_| SymbolReference {
941 from_file: FileId(0),
942 kind: ReferenceKind::NamedImport,
943 namespace: ExportNamespace::Value,
944 import_span: oxc_span::Span::default(),
945 })
946 .collect(),
947 reference_paths: paths.to_vec(),
948 members: Vec::new(),
949 deprecated: false,
950 deprecated_reason: None,
951 }],
952 re_exports: Vec::new(),
953 flags: 0,
954 }
955 }
956
957 fn export_with_reference_files(files: &[u32]) -> ExportSymbol {
958 ExportSymbol {
959 name: ExportName::Named("value".to_string()),
960 is_type_only: false,
961 is_side_effect_used: false,
962 visibility: VisibilityTag::None,
963 expected_unused_reason: None,
964 span: oxc_span::Span::default(),
965 references: files
966 .iter()
967 .map(|file| SymbolReference {
968 from_file: FileId(*file),
969 kind: ReferenceKind::NamedImport,
970 namespace: ExportNamespace::Value,
971 import_span: oxc_span::Span::default(),
972 })
973 .collect(),
974 reference_paths: Vec::new(),
975 members: Vec::new(),
976 deprecated: false,
977 deprecated_reason: None,
978 }
979 }
980
981 #[test]
982 fn physical_references_deduplicate_small_and_large_sets() {
983 let small = export_with_reference_files(&[0, 1, 2, 3, 4, 5, 6, 0]);
984 let large = export_with_reference_files(&[0, 1, 2, 3, 4, 5, 6, 7, 0]);
985
986 assert_eq!(small.physical_references().count(), 7);
987 assert_eq!(large.physical_references().count(), 8);
988 }
989
990 #[test]
991 fn reference_path_metadata_tracks_exact_depth_and_hop_bounds() {
992 let mut interner = ReferencePathInterner::default();
993 let root = interner
994 .direct(FileId(10), ModuleLoadMechanism::EsModule)
995 .expect("tracked interner must return a path");
996 let lower = interner
997 .extend(Some(root), FileId(5), ModuleLoadMechanism::EsModule)
998 .expect("tracked interner must extend a path");
999 let upper = interner
1000 .extend(Some(lower), FileId(20), ModuleLoadMechanism::EsModule)
1001 .expect("tracked interner must extend a path");
1002
1003 assert_eq!(interner.metadata[root.index()].depth, 0);
1004 assert_eq!(
1005 interner.metadata[lower.index()].hop_target_bounds,
1006 Some((FileId(5), FileId(10)))
1007 );
1008 assert_eq!(interner.metadata[upper.index()].depth, 2);
1009 assert_eq!(
1010 interner.metadata[upper.index()].hop_target_bounds,
1011 Some((FileId(5), FileId(20)))
1012 );
1013
1014 let repeated = interner.extend(Some(upper), FileId(10), ModuleLoadMechanism::EsModule);
1015 assert_eq!(repeated, Some(upper));
1016 }
1017
1018 #[test]
1019 fn finalized_reference_paths_are_independent_of_interning_order() {
1020 let mut first = ReferencePathInterner::default();
1021 let first_parent = first.direct(FileId(1), ModuleLoadMechanism::EsModule);
1022 let first_direct = first.direct(FileId(2), ModuleLoadMechanism::CommonJsRequire);
1023 let first_chain = first.extend(first_parent, FileId(3), ModuleLoadMechanism::EsModule);
1024 let mut first_modules = vec![module_with_reference_paths(&[first_direct, first_chain])];
1025 let first_nodes = first.finalize(&mut first_modules);
1026
1027 let mut second = ReferencePathInterner::default();
1028 let second_direct = second.direct(FileId(2), ModuleLoadMechanism::CommonJsRequire);
1029 let second_parent = second.direct(FileId(1), ModuleLoadMechanism::EsModule);
1030 let second_chain = second.extend(second_parent, FileId(3), ModuleLoadMechanism::EsModule);
1031 let mut second_modules = vec![module_with_reference_paths(&[second_direct, second_chain])];
1032 let second_nodes = second.finalize(&mut second_modules);
1033
1034 assert_eq!(first_nodes, second_nodes);
1035 assert_eq!(
1036 first_modules[0].exports[0].reference_paths,
1037 second_modules[0].exports[0].reference_paths
1038 );
1039 }
1040
1041 fn two_hop_route(first: FileId, second: FileId) -> ReferenceRouteGraphSpec {
1042 ReferenceRouteGraphSpec::new(vec![
1043 ReferenceRouteNodeSpec::new(
1044 first,
1045 ModuleLoadMechanism::EsModule,
1046 vec![ReferenceRouteNodeId(1)],
1047 ),
1048 ReferenceRouteNodeSpec::new(second, ModuleLoadMechanism::EsModule, Vec::new()),
1049 ])
1050 }
1051
1052 #[test]
1053 fn finalized_reference_routes_are_independent_of_interning_order() {
1054 let route_a = two_hop_route(FileId(1), FileId(2));
1055 let route_b = two_hop_route(FileId(3), FileId(4));
1056
1057 let mut first = ReferencePathInterner::default();
1058 let first_a = first.intern_route_graph(route_a.clone());
1059 let first_b = first.intern_route_graph(route_b.clone());
1060 let first_b_path = first.route(
1061 None,
1062 first_b,
1063 ReferenceRouteNodeId(0),
1064 ReferenceRouteNodeId(1),
1065 Some(ModuleLoadMechanism::CommonJsRequire),
1066 );
1067 let first_a_path = first.route(
1068 None,
1069 first_a,
1070 ReferenceRouteNodeId(0),
1071 ReferenceRouteNodeId(1),
1072 Some(ModuleLoadMechanism::EsModule),
1073 );
1074 let mut first_modules = vec![module_with_reference_paths(&[first_b_path, first_a_path])];
1075 let first_paths = first.finalize(&mut first_modules);
1076
1077 let mut second = ReferencePathInterner::default();
1078 let second_b = second.intern_route_graph(route_b);
1079 let second_a = second.intern_route_graph(route_a);
1080 let second_b_path = second.route(
1081 None,
1082 second_b,
1083 ReferenceRouteNodeId(0),
1084 ReferenceRouteNodeId(1),
1085 Some(ModuleLoadMechanism::CommonJsRequire),
1086 );
1087 let second_a_path = second.route(
1088 None,
1089 second_a,
1090 ReferenceRouteNodeId(0),
1091 ReferenceRouteNodeId(1),
1092 Some(ModuleLoadMechanism::EsModule),
1093 );
1094 let mut second_modules = vec![module_with_reference_paths(&[second_b_path, second_a_path])];
1095 let second_paths = second.finalize(&mut second_modules);
1096
1097 assert_eq!(first_paths, second_paths);
1098 assert_eq!(
1099 first_modules[0].exports[0].reference_paths,
1100 second_modules[0].exports[0].reference_paths
1101 );
1102 }
1103
1104 #[test]
1105 fn push_reference_without_paths_never_allocates_the_side_table() {
1106 let mut export = ExportSymbol {
1107 name: ExportName::Named("value".to_string()),
1108 is_type_only: false,
1109 is_side_effect_used: false,
1110 visibility: VisibilityTag::None,
1111 expected_unused_reason: None,
1112 span: oxc_span::Span::default(),
1113 references: Vec::new(),
1114 reference_paths: Vec::new(),
1115 members: Vec::new(),
1116 deprecated: false,
1117 deprecated_reason: None,
1118 };
1119 for id in 0..3 {
1120 export.push_reference(
1121 SymbolReference {
1122 from_file: FileId(id),
1123 kind: ReferenceKind::NamedImport,
1124 namespace: ExportNamespace::Value,
1125 import_span: oxc_span::Span::default(),
1126 },
1127 None,
1128 );
1129 }
1130 assert_eq!(export.references.len(), 3);
1131 assert!(export.reference_paths.is_empty());
1132 assert_eq!(export.reference_paths.capacity(), 0);
1133 assert_eq!(export.reference_path(1), None);
1134 assert!(export.has_reference_from(
1135 FileId(1),
1136 oxc_span::Span::default(),
1137 None,
1138 ExportNamespace::Value
1139 ));
1140 assert!(!export.has_reference_from(
1141 FileId(9),
1142 oxc_span::Span::default(),
1143 None,
1144 ExportNamespace::Value
1145 ));
1146 }
1147
1148 #[test]
1149 fn push_reference_backfills_the_side_table_on_the_first_tracked_path() {
1150 let mut export = ExportSymbol {
1151 name: ExportName::Named("value".to_string()),
1152 is_type_only: false,
1153 is_side_effect_used: false,
1154 visibility: VisibilityTag::None,
1155 expected_unused_reason: None,
1156 span: oxc_span::Span::default(),
1157 references: Vec::new(),
1158 reference_paths: Vec::new(),
1159 members: Vec::new(),
1160 deprecated: false,
1161 deprecated_reason: None,
1162 };
1163 let reference = SymbolReference {
1164 from_file: FileId(0),
1165 kind: ReferenceKind::NamedImport,
1166 namespace: ExportNamespace::Value,
1167 import_span: oxc_span::Span::default(),
1168 };
1169 export.push_reference(reference, None);
1170 let tracked = ReferencePathId::from_index(4);
1171 export.push_reference(reference, Some(tracked));
1172 export.push_reference(reference, None);
1173
1174 assert_eq!(export.reference_paths, vec![None, Some(tracked), None]);
1175 assert_eq!(export.reference_path(0), None);
1176 assert_eq!(export.reference_path(1), Some(tracked));
1177 assert!(export.has_reference_from(
1178 FileId(0),
1179 reference.import_span,
1180 Some(tracked),
1181 ExportNamespace::Value
1182 ));
1183 assert!(!export.has_reference_from(
1184 FileId(0),
1185 reference.import_span,
1186 Some(ReferencePathId::from_index(7)),
1187 ExportNamespace::Value
1188 ));
1189 }
1190
1191 #[test]
1192 fn module_node_construction() {
1193 let mut node = ModuleNode {
1194 file_id: FileId(0),
1195 path: PathBuf::from("/project/src/index.ts"),
1196 edge_range: 0..5,
1197 exports: vec![],
1198 re_exports: vec![],
1199 flags: ModuleNode::flags_from(true, true, false),
1200 };
1201 node.set_reachable(true);
1202 assert_eq!(node.file_id, FileId(0));
1203 assert!(node.is_entry_point());
1204 assert!(node.is_reachable());
1205 assert!(node.is_runtime_reachable());
1206 assert!(!node.is_test_reachable());
1207 assert!(!node.has_cjs_exports());
1208 assert_eq!(node.edge_range, 0..5);
1209 }
1210
1211 #[test]
1212 fn module_node_non_entry_unreachable() {
1213 let node = ModuleNode {
1214 file_id: FileId(5),
1215 path: PathBuf::from("/project/src/orphan.ts"),
1216 edge_range: 0..0,
1217 exports: vec![],
1218 re_exports: vec![],
1219 flags: ModuleNode::flags_from(false, false, false),
1220 };
1221 assert!(!node.is_entry_point());
1222 assert!(!node.is_reachable());
1223 assert!(!node.is_runtime_reachable());
1224 assert!(!node.is_test_reachable());
1225 assert!(node.edge_range.is_empty());
1226 }
1227
1228 #[test]
1229 fn module_node_cjs_exports() {
1230 let mut node = ModuleNode {
1231 file_id: FileId(2),
1232 path: PathBuf::from("/project/lib/legacy.js"),
1233 edge_range: 3..7,
1234 exports: vec![],
1235 re_exports: vec![],
1236 flags: ModuleNode::flags_from(false, true, true),
1237 };
1238 node.set_reachable(true);
1239 assert!(node.has_cjs_exports());
1240 assert!(node.is_runtime_reachable());
1241 assert_eq!(node.edge_range.len(), 4);
1242 }
1243
1244 #[test]
1245 fn module_node_with_exports_and_re_exports() {
1246 let node = ModuleNode {
1247 file_id: FileId(1),
1248 path: PathBuf::from("/project/src/barrel.ts"),
1249 edge_range: 0..3,
1250 exports: vec![ExportSymbol {
1251 name: ExportName::Named("localFn".to_string()),
1252 is_type_only: false,
1253 is_side_effect_used: false,
1254 visibility: VisibilityTag::None,
1255 expected_unused_reason: None,
1256 span: oxc_span::Span::new(0, 20),
1257 references: vec![],
1258 reference_paths: Vec::new(),
1259 members: vec![],
1260 deprecated: false,
1261 deprecated_reason: None,
1262 }],
1263 re_exports: vec![ReExportEdge {
1264 source_file: FileId(2),
1265 imported_name: "*".to_string(),
1266 exported_name: "*".to_string(),
1267 is_type_only: false,
1268 span: oxc_span::Span::default(),
1269 }],
1270 flags: ModuleNode::flags_from(false, true, false),
1271 };
1272 assert_eq!(node.exports.len(), 1);
1273 assert_eq!(node.re_exports.len(), 1);
1274 assert_eq!(node.re_exports[0].source_file, FileId(2));
1275 }
1276}