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