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