1mod ambiguity;
7mod build;
8mod cycles;
9mod effective_exports;
10mod effective_re_exports;
11mod entry_load;
12mod fan_io;
13mod impact_closure;
14mod namespace_aliases;
15mod namespace_indexes;
16mod namespace_re_exports;
17mod narrowing;
18mod package_entry;
19mod partition_order;
20mod public_exports;
21mod re_exports;
22mod reachability;
23mod shortest_import_path;
24pub mod types;
25
26use std::path::Path;
27
28use fixedbitset::FixedBitSet;
29use rustc_hash::{FxHashMap, FxHashSet};
30
31use crate::resolve::{ResolvedModule, ResolvedReplacedModuleTarget};
32use fallow_types::discover::{DiscoveredFile, EntryPoint, FileId};
33use fallow_types::extract::{ImportLoadKind, ImportedName, ModuleLoadMechanism};
34use types::{ReferencePathInterner, ReferencePathNode, ReferenceRouteNodeId, ReferenceRoutes};
35
36pub(super) fn relativize(path: &Path, root: &Path) -> String {
40 path.strip_prefix(root)
41 .unwrap_or(path)
42 .to_string_lossy()
43 .replace('\\', "/")
44}
45
46pub use ambiguity::{AmbiguityParticipants, AmbiguousStarExport};
47pub use cycles::CycleOptions;
48pub use effective_exports::{EffectiveExportBinding, EffectiveExportResolution, ExportNamespace};
49pub use effective_re_exports::EffectiveReExportRoute;
50pub use entry_load::{DominatingImport, EntryLoadClosure};
51pub use fan_io::{FocusFileFacts, FocusFileFactsPaths};
52pub use impact_closure::{
53 CoordinationGap, CoordinationGapPaths, ImpactClosure, ImpactClosurePaths,
54};
55pub use partition_order::{PartitionOrder, PartitionOrderPaths, ReviewUnit, ReviewUnitPaths};
56pub use public_exports::PublicExportOrigin;
57pub use re_exports::GraphReExportCycle;
58pub use shortest_import_path::ImportPathHop;
59pub use types::{
60 ExportSymbol, ModuleNode, ReExportEdge, ReferenceKind, ReferencePathId, SymbolReference,
61};
62
63#[derive(Debug, Clone, Copy)]
65pub struct EffectiveExportOrigin<'graph> {
66 file_id: FileId,
67 export: &'graph ExportSymbol,
68}
69
70#[derive(Debug, Clone, Copy)]
72pub struct EffectiveExportSurface<'graph> {
73 binding: EffectiveExportBinding,
74 namespace: ExportNamespace,
75 export: Option<&'graph ExportSymbol>,
76 origin: Option<EffectiveExportOrigin<'graph>>,
77 local_export: bool,
78}
79
80impl<'graph> EffectiveExportSurface<'graph> {
81 #[must_use]
83 pub const fn binding(self) -> EffectiveExportBinding {
84 self.binding
85 }
86
87 #[must_use]
89 pub const fn namespace(self) -> ExportNamespace {
90 self.namespace
91 }
92
93 #[must_use]
99 pub const fn export(self) -> Option<&'graph ExportSymbol> {
100 self.export
101 }
102
103 #[must_use]
105 pub const fn origin(self) -> Option<EffectiveExportOrigin<'graph>> {
106 self.origin
107 }
108}
109
110impl<'graph> EffectiveExportOrigin<'graph> {
111 #[must_use]
113 pub const fn file_id(self) -> FileId {
114 self.file_id
115 }
116
117 #[must_use]
119 pub const fn export(self) -> &'graph ExportSymbol {
120 self.export
121 }
122}
123
124#[must_use]
133pub fn is_declaration_file_path(path: &Path) -> bool {
134 path.file_name()
135 .and_then(|n| n.to_str())
136 .is_some_and(|name| {
137 name.ends_with(".d.ts") || name.ends_with(".d.mts") || name.ends_with(".d.cts")
138 })
139}
140
141#[derive(Debug, Clone, Copy)]
143pub struct ReachabilityRoots<'a> {
144 pub all: &'a [EntryPoint],
146 pub runtime: &'a [EntryPoint],
148 pub test: &'a [EntryPoint],
150}
151
152#[derive(Debug, serde::Serialize, serde::Deserialize)]
160pub struct ModuleGraph {
161 pub modules: Vec<ModuleNode>,
171 edges: Vec<Edge>,
173 pub package_usage: FxHashMap<String, Vec<FileId>>,
175 pub type_only_package_usage: FxHashMap<String, Vec<FileId>>,
179 pub asset_package_usage: FxHashMap<String, Vec<FileId>>,
185 pub eager_package_imports: FxHashMap<FileId, Vec<EagerPackageImport>>,
189 pub entry_points: FxHashSet<FileId>,
191 pub runtime_entry_points: FxHashSet<FileId>,
193 pub test_entry_points: FxHashSet<FileId>,
195 test_reachability_index: TestReachabilityIndex,
200 reference_paths: Vec<ReferencePathNode>,
202 reference_routes: ReferenceRoutes,
204 pub reverse_deps: Vec<Vec<FileId>>,
206 #[serde(skip, default)]
214 namespace_imported: FixedBitSet,
215 pub re_export_cycles: Vec<GraphReExportCycle>,
222 effective_exports: effective_exports::EffectiveExportIndex,
224}
225
226#[derive(Debug, serde::Serialize, serde::Deserialize)]
233pub struct Edge {
234 source: FileId,
236 target: FileId,
238 symbols: Vec<ImportedSymbol>,
240}
241
242impl Edge {
243 #[must_use]
248 pub(crate) fn runs_target_code(&self) -> bool {
249 self.symbols.is_empty()
250 || self
251 .symbols
252 .iter()
253 .any(|symbol| symbol.load_kind.runs_target_code())
254 }
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
259pub struct EagerPackageImport {
260 pub package: String,
262 pub specifier: String,
264}
265
266#[derive(Debug, serde::Serialize, serde::Deserialize)]
268pub struct ImportedSymbol {
269 pub imported_name: ImportedName,
272 pub local_name: String,
274 #[serde(with = "crate::cache::span_serde")]
276 pub import_span: oxc_span::Span,
277 pub is_type_only: bool,
280 pub is_type_only_star: bool,
283 mechanism: ModuleLoadMechanism,
285 load_kind: ImportLoadKind,
289}
290
291impl ImportedSymbol {
292 #[must_use]
294 pub const fn load_kind(&self) -> ImportLoadKind {
295 self.load_kind
296 }
297
298 #[must_use]
301 pub const fn is_eager_value(&self) -> bool {
302 self.load_kind.is_eager_value(self.is_type_only)
303 }
304
305 #[must_use]
309 pub const fn loads_target(&self) -> bool {
310 self.load_kind.loads_target()
311 }
312
313 #[must_use]
325 pub(crate) fn is_ambient_star(&self) -> bool {
326 self.is_type_only
327 && self.local_name.is_empty()
328 && matches!(
329 self.imported_name,
330 ImportedName::Namespace | ImportedName::Default
331 )
332 }
333
334 #[must_use]
342 pub(crate) fn is_value_bearing_ambient_star(&self) -> bool {
343 self.is_ambient_star() && !self.is_type_only_star
344 }
345}
346
347#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
355struct TestReachabilityIndex {
356 profile_count: usize,
357 words_per_file: usize,
358 reachable_profiles: Vec<u64>,
359 masked_profiles: Vec<MaskedTestProfiles>,
360}
361
362#[derive(Debug, serde::Serialize, serde::Deserialize)]
364struct MaskedTestProfiles {
365 target: FileId,
366 profiles: Vec<u64>,
367}
368
369impl TestReachabilityIndex {
370 fn new(file_capacity: usize, profile_count: usize) -> Self {
371 let words_per_file = profile_count.div_ceil(u64::BITS as usize);
372 let storage_len = file_capacity.saturating_mul(words_per_file);
373 Self {
374 profile_count,
375 words_per_file,
376 reachable_profiles: vec![0; storage_len],
377 masked_profiles: Vec::new(),
378 }
379 }
380
381 fn set_sparse_masks(&mut self, masks: FxHashMap<FileId, Vec<u64>>) {
382 let mut rows: Vec<_> = masks
383 .into_iter()
384 .map(|(target, profiles)| MaskedTestProfiles { target, profiles })
385 .collect();
386 rows.sort_unstable_by_key(|row| row.target.0);
387 self.masked_profiles = rows;
388 }
389
390 fn profiles_for<'a>(&self, storage: &'a [u64], file_id: FileId) -> Option<&'a [u64]> {
391 let start = (file_id.0 as usize).checked_mul(self.words_per_file)?;
392 let end = start.checked_add(self.words_per_file)?;
393 storage.get(start..end)
394 }
395
396 fn masked_profiles_for(&self, file_id: FileId) -> Option<&[u64]> {
397 self.masked_profiles
398 .binary_search_by_key(&file_id.0, |row| row.target.0)
399 .ok()
400 .map(|index| self.masked_profiles[index].profiles.as_slice())
401 }
402
403 fn covers_reference_path(
404 &self,
405 source: FileId,
406 path: types::ReferencePathId,
407 paths: &[ReferencePathNode],
408 routes: &ReferenceRoutes,
409 ) -> bool {
410 let Some(source_profiles) = self.profiles_for(&self.reachable_profiles, source) else {
411 return false;
412 };
413
414 for (word_index, &source_word) in source_profiles.iter().enumerate() {
415 let mut active_profiles = source_word;
416 if active_profiles == 0 {
417 continue;
418 }
419
420 let mut next = Some(path);
421 while let Some(path_id) = next {
422 let Some(path_node) = paths.get(path_id.index()) else {
423 return false;
424 };
425 next = path_node.parent();
426 active_profiles = match *path_node {
427 ReferencePathNode::Hop {
428 target, mechanism, ..
429 } => self.active_hop_profiles(target, mechanism, word_index, active_profiles),
430 ReferencePathNode::Route {
431 graph,
432 start,
433 terminal,
434 start_mechanism,
435 ..
436 } => self.active_route_profiles(
437 routes,
438 graph,
439 start,
440 terminal,
441 start_mechanism,
442 word_index,
443 active_profiles,
444 ),
445 };
446 if active_profiles == 0 {
447 break;
448 }
449 }
450
451 if active_profiles != 0 {
452 return true;
453 }
454 }
455
456 false
457 }
458
459 fn active_hop_profiles(
460 &self,
461 target: FileId,
462 mechanism: ModuleLoadMechanism,
463 word_index: usize,
464 mut active_profiles: u64,
465 ) -> u64 {
466 let Some(target_word) = self
467 .profiles_for(&self.reachable_profiles, target)
468 .and_then(|profiles| profiles.get(word_index))
469 else {
470 return 0;
471 };
472 active_profiles &= target_word;
473 if matches!(mechanism, ModuleLoadMechanism::EsModule)
474 && let Some(masked_profiles) = self.masked_profiles_for(target)
475 {
476 let Some(masked_word) = masked_profiles.get(word_index) else {
477 return 0;
478 };
479 active_profiles &= !masked_word;
480 }
481 active_profiles
482 }
483
484 #[expect(
488 clippy::too_many_arguments,
489 reason = "the route identity and profile word form one evaluation contract"
490 )]
491 fn active_route_profiles(
492 &self,
493 routes: &ReferenceRoutes,
494 graph_id: types::ReferenceRouteGraphId,
495 start: ReferenceRouteNodeId,
496 terminal: ReferenceRouteNodeId,
497 start_mechanism: Option<ModuleLoadMechanism>,
498 word_index: usize,
499 candidate_profiles: u64,
500 ) -> u64 {
501 let Some(graph) = routes.graphs.get(graph_id.0 as usize) else {
502 return 0;
503 };
504 let node_count = graph.nodes.end.saturating_sub(graph.nodes.start) as usize;
505 let start_index = start.0 as usize;
506 let terminal_index = terminal.0 as usize;
507 if start_index >= node_count || terminal_index >= node_count {
508 return 0;
509 }
510
511 let mut attempted = vec![0_u64; node_count];
512 let mut pending = vec![0_u64; node_count];
513 let mut queued = vec![false; node_count];
514 let mut queue = std::collections::VecDeque::from([start_index]);
515 pending[start_index] = candidate_profiles;
516 queued[start_index] = true;
517 let mut successful_profiles = 0_u64;
518
519 while let Some(local_index) = queue.pop_front() {
520 queued[local_index] = false;
521 let incoming = pending[local_index] & !attempted[local_index];
522 pending[local_index] = 0;
523 attempted[local_index] |= incoming;
524 if incoming == 0 {
525 continue;
526 }
527
528 let Some(node) = routes.nodes.get(graph.nodes.start as usize + local_index) else {
529 return 0;
530 };
531 let active = if local_index == start_index {
532 start_mechanism.map_or(incoming, |mechanism| {
533 self.active_hop_profiles(node.target, mechanism, word_index, incoming)
534 })
535 } else {
536 self.active_hop_profiles(node.target, node.mechanism, word_index, incoming)
537 };
538 if active == 0 {
539 continue;
540 }
541 if local_index == terminal_index {
542 successful_profiles |= active;
543 continue;
544 }
545
546 let Some(successors) = routes
547 .edges
548 .get(node.successors.start as usize..node.successors.end as usize)
549 else {
550 return 0;
551 };
552 for successor in successors {
553 let successor_index = successor.0 as usize;
554 if successor_index >= node_count {
555 return 0;
556 }
557 let new_profiles = active & !attempted[successor_index] & !pending[successor_index];
558 if new_profiles == 0 {
559 continue;
560 }
561 pending[successor_index] |= new_profiles;
562 if !queued[successor_index] {
563 queued[successor_index] = true;
564 queue.push_back(successor_index);
565 }
566 }
567 }
568
569 successful_profiles
570 }
571
572 #[cfg(test)]
573 fn profile_contains(&self, storage: &[u64], file_id: FileId, profile: usize) -> bool {
574 self.profiles_for(storage, file_id)
575 .and_then(|words| words.get(profile / u64::BITS as usize))
576 .is_some_and(|word| word & (1_u64 << (profile % u64::BITS as usize)) != 0)
577 }
578
579 #[cfg(test)]
580 fn profile_reaches(&self, file_id: FileId, profile: usize) -> bool {
581 self.profile_contains(&self.reachable_profiles, file_id, profile)
582 }
583
584 #[cfg(test)]
585 fn profile_masks(&self, file_id: FileId, profile: usize) -> bool {
586 self.masked_profiles_for(file_id)
587 .and_then(|words| words.get(profile / u64::BITS as usize))
588 .is_some_and(|word| word & (1_u64 << (profile % u64::BITS as usize)) != 0)
589 }
590}
591
592#[derive(Debug, Clone, Copy)]
595pub struct OutgoingEdgeSummary<'a> {
596 pub target: FileId,
598 pub all_type_only: bool,
601 pub span_start: Option<u32>,
604 pub symbols: &'a [ImportedSymbol],
607}
608
609#[derive(Debug, Clone, PartialEq, Eq)]
611pub struct DirectImporterSummary {
612 pub source: FileId,
614 pub symbols: Vec<ImportedSymbolSummary>,
616}
617
618#[derive(Debug, Clone, PartialEq, Eq)]
620pub struct ImportedSymbolSummary {
621 pub imported: String,
624 pub local: String,
626 pub type_only: bool,
628}
629
630#[cfg(target_pointer_width = "64")]
631const _: () = assert!(std::mem::size_of::<Edge>() == 32);
632#[cfg(target_pointer_width = "64")]
633const _: () = assert!(std::mem::size_of::<ImportedSymbol>() == 64);
634
635#[cold]
636#[inline(never)]
637fn propagate_namespace_references(
638 graph: &mut ModuleGraph,
639 module_by_id: &FxHashMap<FileId, &ResolvedModule>,
640 features: build::NamespaceFeatures,
641 exposed_namespace_targets: &re_exports::ExposedNamespaceTargets,
642 reference_paths: &mut ReferencePathInterner,
643) {
644 let indexes = namespace_indexes::NamespacePropagationIndexes::new(graph, module_by_id);
645 if features.has_aliases {
646 namespace_aliases::propagate_cross_package_aliases(
647 graph,
648 module_by_id,
649 &indexes,
650 reference_paths,
651 );
652 }
653 if features.has_re_exports {
654 namespace_re_exports::propagate_namespace_re_exports(
655 graph,
656 &indexes,
657 exposed_namespace_targets,
658 reference_paths,
659 );
660 }
661}
662
663impl ModuleGraph {
664 fn resolve_entry_point_ids(
665 entry_points: &[EntryPoint],
666 path_to_id: &FxHashMap<&Path, FileId>,
667 ) -> FxHashSet<FileId> {
668 entry_points
669 .iter()
670 .filter_map(|ep| {
671 path_to_id.get(ep.path.as_path()).copied().or_else(|| {
672 dunce::canonicalize(&ep.path)
673 .ok()
674 .and_then(|path| path_to_id.get(path.as_path()).copied())
675 })
676 })
677 .collect()
678 }
679
680 pub fn build(
682 resolved_modules: &[ResolvedModule],
683 entry_points: &[EntryPoint],
684 files: &[DiscoveredFile],
685 ) -> Self {
686 Self::build_with_reachability_roots(
687 resolved_modules,
688 entry_points,
689 entry_points,
690 &[],
691 files,
692 )
693 }
694
695 pub fn build_with_reachability_roots(
697 resolved_modules: &[ResolvedModule],
698 entry_points: &[EntryPoint],
699 runtime_entry_points: &[EntryPoint],
700 test_entry_points: &[EntryPoint],
701 files: &[DiscoveredFile],
702 ) -> Self {
703 Self::build_with_reachability_roots_and_replacements(
704 resolved_modules,
705 &[],
706 entry_points,
707 runtime_entry_points,
708 test_entry_points,
709 files,
710 )
711 }
712
713 pub fn build_with_reachability_roots_and_replacements(
715 resolved_modules: &[ResolvedModule],
716 replaced_module_targets: &[ResolvedReplacedModuleTarget],
717 entry_points: &[EntryPoint],
718 runtime_entry_points: &[EntryPoint],
719 test_entry_points: &[EntryPoint],
720 files: &[DiscoveredFile],
721 ) -> Self {
722 Self::build_with_declaration_seeding(
723 resolved_modules,
724 replaced_module_targets,
725 &ReachabilityRoots {
726 all: entry_points,
727 runtime: runtime_entry_points,
728 test: test_entry_points,
729 },
730 files,
731 &FxHashSet::default(),
732 )
733 }
734
735 pub fn build_with_declaration_seeding(
746 resolved_modules: &[ResolvedModule],
747 replaced_module_targets: &[ResolvedReplacedModuleTarget],
748 roots: &ReachabilityRoots<'_>,
749 files: &[DiscoveredFile],
750 unseeded_declaration_files: &FxHashSet<FileId>,
751 ) -> Self {
752 let ReachabilityRoots {
753 all: entry_points,
754 runtime: runtime_entry_points,
755 test: test_entry_points,
756 } = *roots;
757 let _span = tracing::info_span!("build_graph").entered();
758
759 let module_count = files.len();
760
761 let max_file_id = files
762 .iter()
763 .map(|f| f.id.0 as usize)
764 .max()
765 .map_or(0, |m| m + 1);
766 let total_capacity = max_file_id.max(module_count);
767
768 let path_to_id: FxHashMap<&Path, FileId> =
769 files.iter().map(|f| (f.path.as_path(), f.id)).collect();
770
771 let module_by_id: FxHashMap<FileId, &ResolvedModule> =
772 resolved_modules.iter().map(|m| (m.file_id, m)).collect();
773
774 let mut entry_point_ids = Self::resolve_entry_point_ids(entry_points, &path_to_id);
775 let runtime_entry_point_ids =
776 Self::resolve_entry_point_ids(runtime_entry_points, &path_to_id);
777 let test_entry_point_ids = Self::resolve_entry_point_ids(test_entry_points, &path_to_id);
778
779 for file in files {
780 if is_declaration_file_path(&file.path)
781 && !unseeded_declaration_files.contains(&file.id)
782 {
783 entry_point_ids.insert(file.id);
784 }
785 }
786
787 let (mut graph, namespace_features) = Self::populate_edges(&build::PopulateEdgesInput {
788 files,
789 module_by_id: &module_by_id,
790 entry_point_ids: &entry_point_ids,
791 runtime_entry_point_ids: &runtime_entry_point_ids,
792 test_entry_point_ids: &test_entry_point_ids,
793 module_count,
794 total_capacity,
795 });
796 graph.effective_exports = effective_exports::EffectiveExportIndex::build(resolved_modules);
797
798 let test_reachability_plan = reachability::TestReachabilityPlan::new(
799 &test_entry_point_ids,
800 replaced_module_targets,
801 total_capacity,
802 );
803
804 let mut reference_paths =
805 ReferencePathInterner::new(test_reachability_plan.requires_reference_provenance());
806 let whole_module_targets =
807 graph.populate_references(&module_by_id, &entry_point_ids, &mut reference_paths);
808 let entry_reachable = graph.collect_reachable(&entry_point_ids, total_capacity);
813 let exposed_namespace_targets = graph.collect_exposed_namespace_targets(
814 &whole_module_targets,
815 &entry_reachable,
816 &module_by_id,
817 );
818
819 if namespace_features.has_aliases || namespace_features.has_re_exports {
820 propagate_namespace_references(
821 &mut graph,
822 &module_by_id,
823 namespace_features,
824 &exposed_namespace_targets,
825 &mut reference_paths,
826 );
827 }
828
829 graph.mark_reachable(
830 &entry_reachable,
831 &entry_point_ids,
832 &runtime_entry_point_ids,
833 test_reachability_plan,
834 total_capacity,
835 );
836
837 graph.re_export_cycles = graph.resolve_re_export_chains(
838 &module_by_id,
839 &exposed_namespace_targets,
840 &mut reference_paths,
841 );
842 let finalized_paths = reference_paths.finalize(&mut graph.modules);
843 graph.reference_paths = finalized_paths.paths;
844 graph.reference_routes = finalized_paths.routes;
845
846 graph
847 }
848
849 #[must_use]
851 pub const fn module_count(&self) -> usize {
852 self.modules.len()
853 }
854
855 #[must_use]
857 pub const fn edge_count(&self) -> usize {
858 self.edges.len()
859 }
860
861 #[must_use]
863 pub fn is_test_reachable(&self, file_id: FileId) -> bool {
864 self.modules
865 .get(file_id.0 as usize)
866 .is_some_and(ModuleNode::is_test_reachable)
867 }
868
869 #[must_use]
877 pub fn is_test_reference_covered(&self, export: &ExportSymbol, reference_index: usize) -> bool {
878 let Some(reference) = export.references.get(reference_index) else {
879 return false;
880 };
881 if self.test_reachability_index.profile_count == 0 {
882 return self.is_test_reachable(reference.from_file);
883 }
884
885 let Some(path) = export.reference_path(reference_index) else {
886 return false;
887 };
888
889 self.test_reachability_index.covers_reference_path(
890 reference.from_file,
891 path,
892 &self.reference_paths,
893 &self.reference_routes,
894 )
895 }
896
897 #[must_use]
900 pub fn is_any_test_reference_covered(&self, export: &ExportSymbol) -> bool {
901 (0..export.references.len())
902 .any(|reference_index| self.is_test_reference_covered(export, reference_index))
903 }
904
905 #[cfg(test)]
906 fn reference_path_hops(
907 &self,
908 export: &ExportSymbol,
909 reference_index: usize,
910 ) -> Vec<(FileId, ModuleLoadMechanism)> {
911 let mut hops = Vec::new();
912 let mut next = export.reference_path(reference_index);
913 while let Some(path_id) = next {
914 let Some(node) = self.reference_paths.get(path_id.index()) else {
915 return Vec::new();
916 };
917 next = node.parent();
918 match *node {
919 ReferencePathNode::Hop {
920 target, mechanism, ..
921 } => hops.push((target, mechanism)),
922 ReferencePathNode::Route {
923 graph,
924 start,
925 terminal,
926 start_mechanism,
927 ..
928 } => hops.extend(self.reference_routes.canonical_hops(
929 graph,
930 start,
931 terminal,
932 start_mechanism,
933 )),
934 }
935 }
936 hops
937 }
938
939 pub(crate) fn reconstruct_namespace_imported(&mut self) {
954 let capacity = self
955 .edges
956 .iter()
957 .map(|edge| edge.target.0 as usize + 1)
958 .max()
959 .unwrap_or(0)
960 .max(self.modules.len());
961 let mut bitset = FixedBitSet::with_capacity(capacity);
962 for edge in &self.edges {
963 if edge
964 .symbols
965 .iter()
966 .any(|sym| matches!(sym.imported_name, ImportedName::Namespace))
967 {
968 let idx = edge.target.0 as usize;
969 if idx < capacity {
970 bitset.insert(idx);
971 }
972 }
973 }
974 self.namespace_imported = bitset;
975 }
976
977 #[must_use]
983 pub fn resolve_export(
984 &self,
985 file_id: FileId,
986 name: &str,
987 namespace: ExportNamespace,
988 ) -> EffectiveExportResolution {
989 self.effective_exports.resolve(file_id, name, namespace)
990 }
991
992 #[must_use]
999 pub fn effective_bindings_share_declaration_group(
1000 &self,
1001 left: EffectiveExportBinding,
1002 right: EffectiveExportBinding,
1003 ) -> bool {
1004 if left == right || left.origin_file() != right.origin_file() {
1005 return left == right;
1006 }
1007 let Some(right_slot) = right.origin_slot() else {
1008 return false;
1009 };
1010 self.effective_exports
1011 .declaration_group_slots(left)
1012 .contains(&right_slot)
1013 }
1014
1015 #[must_use]
1021 pub fn resolve_export_origin(
1022 &self,
1023 file_id: FileId,
1024 name: &str,
1025 namespace: ExportNamespace,
1026 ) -> Option<EffectiveExportOrigin<'_>> {
1027 let EffectiveExportResolution::Unique(binding) =
1028 self.resolve_export(file_id, name, namespace)
1029 else {
1030 return None;
1031 };
1032 self.export_binding_origin(binding)
1033 }
1034
1035 #[must_use]
1038 pub fn effective_export_surface(
1039 &self,
1040 file_id: FileId,
1041 name: &str,
1042 namespace: ExportNamespace,
1043 ) -> Option<EffectiveExportSurface<'_>> {
1044 let EffectiveExportResolution::Unique(binding) =
1045 self.resolve_export(file_id, name, namespace)
1046 else {
1047 return None;
1048 };
1049 let module = self.modules.get(file_id.0 as usize)?;
1050 let exact_surface = module.exports.iter().find(|export| {
1051 export.name.matches_str(name)
1052 && match namespace {
1053 ExportNamespace::Type => export.is_type_only,
1054 ExportNamespace::Value => !export.is_type_only,
1055 }
1056 });
1057 let surface_export = exact_surface.or_else(|| {
1058 module
1059 .exports
1060 .iter()
1061 .find(|export| export.name.matches_str(name))
1062 });
1063 let origin = self.export_binding_origin(binding);
1064 let export = surface_export.or_else(|| origin.map(|o| o.export));
1065 Some(EffectiveExportSurface {
1066 binding,
1067 namespace,
1068 export,
1069 origin,
1070 local_export: surface_export.is_some(),
1071 })
1072 }
1073
1074 #[must_use]
1080 pub fn effective_export_surface_re_export(
1081 &self,
1082 file_id: FileId,
1083 name: &str,
1084 namespace: ExportNamespace,
1085 ) -> Option<&ReExportEdge> {
1086 let EffectiveExportResolution::Unique(binding) =
1087 self.resolve_export(file_id, name, namespace)
1088 else {
1089 return None;
1090 };
1091 self.modules
1092 .get(file_id.0 as usize)?
1093 .re_exports
1094 .iter()
1095 .find(|re_export| {
1096 re_export.exported_name == name
1097 && (namespace == ExportNamespace::Type || !re_export.is_type_only)
1098 && if re_export.imported_name == "*" {
1099 binding.namespace_source() == Some(re_export.source_file)
1100 } else {
1101 self.resolve_export(
1102 re_export.source_file,
1103 &re_export.imported_name,
1104 namespace,
1105 ) == EffectiveExportResolution::Unique(binding)
1106 }
1107 })
1108 }
1109
1110 #[must_use]
1116 pub fn effective_export_surface_references(
1117 &self,
1118 file_id: FileId,
1119 name: &str,
1120 namespace: ExportNamespace,
1121 ) -> Vec<&SymbolReference> {
1122 let Some(surface) = self.effective_export_surface(file_id, name, namespace) else {
1123 return Vec::new();
1124 };
1125 let Some(export) = surface.export() else {
1126 return Vec::new();
1127 };
1128 if surface.local_export
1129 || surface
1130 .origin()
1131 .is_none_or(|origin| origin.file_id() == file_id)
1132 {
1133 return export.references_in(namespace).collect();
1134 }
1135 let mut exposed: FxHashMap<FileId, FxHashSet<String>> = FxHashMap::default();
1136 exposed.entry(file_id).or_default().insert(name.to_string());
1137 for route in self.effective_re_export_routes(file_id, name, namespace) {
1138 exposed
1139 .entry(route.barrel_file())
1140 .or_default()
1141 .insert(route.exported_name().to_string());
1142 }
1143 export
1144 .references
1145 .iter()
1146 .filter(|reference| {
1147 reference.namespace == namespace
1148 && self.reference_reaches_surface(reference, &exposed, namespace)
1149 })
1150 .collect()
1151 }
1152
1153 fn reference_reaches_surface(
1154 &self,
1155 reference: &SymbolReference,
1156 exposed: &FxHashMap<FileId, FxHashSet<String>>,
1157 namespace: ExportNamespace,
1158 ) -> bool {
1159 if reference.kind == ReferenceKind::ReExport && exposed.contains_key(&reference.from_file) {
1160 return true;
1161 }
1162 self.outgoing_symbol_edges(reference.from_file)
1163 .any(|(target, symbols)| {
1164 let Some(names) = exposed.get(&target) else {
1165 return false;
1166 };
1167 symbols.iter().any(|symbol| {
1168 symbol.import_span == reference.import_span
1169 && (namespace == ExportNamespace::Type
1170 || !symbol.is_type_only
1171 || symbol.is_value_bearing_ambient_star())
1172 && match &symbol.imported_name {
1173 ImportedName::Named(imported) => names.contains(imported.as_str()),
1174 ImportedName::Default => names.contains("default"),
1175 ImportedName::Namespace => true,
1176 ImportedName::SideEffect => false,
1177 }
1178 })
1179 })
1180 }
1181
1182 #[must_use]
1184 pub fn export_binding_origin(
1185 &self,
1186 binding: EffectiveExportBinding,
1187 ) -> Option<EffectiveExportOrigin<'_>> {
1188 let origin_file = binding.origin_file();
1189 let export = self
1190 .modules
1191 .get(origin_file.0 as usize)?
1192 .exports
1193 .get(binding.origin_slot()?)?;
1194 Some(EffectiveExportOrigin {
1195 file_id: origin_file,
1196 export,
1197 })
1198 }
1199
1200 #[must_use]
1205 pub fn unique_export_bindings(
1206 &self,
1207 file_id: FileId,
1208 namespace: ExportNamespace,
1209 ) -> FxHashSet<EffectiveExportBinding> {
1210 self.effective_exports.unique_bindings(file_id, namespace)
1211 }
1212
1213 #[must_use]
1221 pub fn importer_connects_export_origin(
1222 &self,
1223 importer: FileId,
1224 source: FileId,
1225 name: &str,
1226 namespace: ExportNamespace,
1227 ) -> bool {
1228 let Some(importer_module) = self.modules.get(importer.0 as usize) else {
1229 return false;
1230 };
1231 let re_export_count = importer_module
1232 .re_exports
1233 .iter()
1234 .filter(|re_export| re_export.source_file == source)
1235 .count();
1236 if self.edges[importer_module.edge_range.clone()]
1237 .iter()
1238 .any(|edge| edge.target == source && edge.symbols.len() > re_export_count)
1239 {
1240 return true;
1241 }
1242
1243 importer_module.re_exports.iter().any(|re_export| {
1244 if re_export.source_file != source
1245 || (namespace == ExportNamespace::Value && re_export.is_type_only)
1246 {
1247 return false;
1248 }
1249 let exported_name = if re_export.imported_name == "*" {
1250 if re_export.exported_name != "*" || name == "default" {
1251 return false;
1252 }
1253 name
1254 } else {
1255 if re_export.imported_name != name {
1256 return false;
1257 }
1258 &re_export.exported_name
1259 };
1260 self.effective_exports.contributes_through(
1261 importer,
1262 exported_name,
1263 source,
1264 name,
1265 namespace,
1266 )
1267 })
1268 }
1269
1270 #[must_use]
1273 pub fn has_namespace_import(&self, file_id: FileId) -> bool {
1274 let idx = file_id.0 as usize;
1275 if idx >= self.namespace_imported.len() {
1276 return false;
1277 }
1278 self.namespace_imported.contains(idx)
1279 }
1280
1281 #[must_use]
1283 pub fn edges_for(&self, file_id: FileId) -> Vec<FileId> {
1284 let idx = file_id.0 as usize;
1285 if idx >= self.modules.len() {
1286 return Vec::new();
1287 }
1288 let range = &self.modules[idx].edge_range;
1289 self.edges[range.clone()].iter().map(|e| e.target).collect()
1290 }
1291
1292 pub fn outgoing_symbol_edges(
1298 &self,
1299 file_id: FileId,
1300 ) -> impl Iterator<Item = (FileId, &[ImportedSymbol])> + '_ {
1301 let idx = file_id.0 as usize;
1302 let range = if idx < self.modules.len() {
1303 self.modules[idx].edge_range.clone()
1304 } else {
1305 0..0
1306 };
1307 self.edges[range]
1308 .iter()
1309 .map(|edge| (edge.target, edge.symbols.as_slice()))
1310 }
1311
1312 #[must_use]
1316 pub fn importers_of(&self, target: FileId) -> &[FileId] {
1317 self.reverse_deps
1318 .get(target.0 as usize)
1319 .map_or(&[], Vec::as_slice)
1320 }
1321
1322 #[must_use]
1327 pub fn direct_importer_summaries(&self, target: FileId) -> Vec<DirectImporterSummary> {
1328 let Some(importers) = self.reverse_deps.get(target.0 as usize) else {
1329 return Vec::new();
1330 };
1331
1332 let mut summaries = Vec::new();
1333 for &source in importers {
1334 let idx = source.0 as usize;
1335 let Some(source_node) = self.modules.get(idx) else {
1336 continue;
1337 };
1338 let mut symbols = Vec::new();
1339 for edge in &self.edges[source_node.edge_range.clone()] {
1340 if edge.target != target {
1341 continue;
1342 }
1343 symbols.extend(edge.symbols.iter().map(|symbol| ImportedSymbolSummary {
1344 imported: imported_name_label(&symbol.imported_name),
1345 local: symbol.local_name.clone(),
1346 type_only: symbol.is_type_only,
1347 }));
1348 }
1349 symbols.sort_by(|a, b| {
1350 a.imported
1351 .cmp(&b.imported)
1352 .then_with(|| a.local.cmp(&b.local))
1353 .then_with(|| a.type_only.cmp(&b.type_only))
1354 });
1355 symbols.dedup();
1356 summaries.push(DirectImporterSummary { source, symbols });
1357 }
1358 summaries.sort_by_key(|summary| summary.source.0);
1359 summaries
1360 }
1361
1362 #[must_use]
1371 pub fn find_import_span_start(&self, source: FileId, target: FileId) -> Option<u32> {
1372 let idx = source.0 as usize;
1373 if idx >= self.modules.len() {
1374 return None;
1375 }
1376 let range = &self.modules[idx].edge_range;
1377 for edge in &self.edges[range.clone()] {
1378 if edge.target == target {
1379 return edge
1380 .symbols
1381 .iter()
1382 .find(|s| s.is_eager_value())
1383 .or_else(|| edge.symbols.iter().find(|s| !s.is_type_only))
1384 .or_else(|| edge.symbols.first())
1385 .map(|s| s.import_span.start);
1386 }
1387 }
1388 None
1389 }
1390
1391 pub fn outgoing_edge_summaries(
1411 &self,
1412 file_id: FileId,
1413 ) -> impl Iterator<Item = OutgoingEdgeSummary<'_>> + '_ {
1414 self.outgoing_symbol_edges(file_id)
1415 .filter_map(|(target, symbols)| {
1416 let mut loading = symbols.iter().filter(|s| s.loads_target()).peekable();
1417 let first = loading.peek().copied()?;
1418 let value = loading.find(|s| !s.is_type_only);
1419 Some(OutgoingEdgeSummary {
1420 target,
1421 all_type_only: value.is_none(),
1422 span_start: Some(value.unwrap_or(first).import_span.start),
1423 symbols,
1424 })
1425 })
1426 }
1427
1428 pub fn outgoing_edge_symbols(
1439 &self,
1440 file_id: FileId,
1441 ) -> impl Iterator<Item = (FileId, &[ImportedSymbol])> + '_ {
1442 let idx = file_id.0 as usize;
1443 let range = if idx < self.modules.len() {
1444 self.modules[idx].edge_range.clone()
1445 } else {
1446 0..0
1447 };
1448 self.edges[range]
1449 .iter()
1450 .map(|edge| (edge.target, edge.symbols.as_slice()))
1451 }
1452
1453 pub fn outgoing_edge_summaries_with_exclusions<'a>(
1472 &'a self,
1473 file_id: FileId,
1474 excluded_span_starts: &'a FxHashSet<u32>,
1475 ) -> impl Iterator<Item = (FileId, bool, Option<u32>, bool)> + 'a {
1476 let idx = file_id.0 as usize;
1477 let range = if idx < self.modules.len() {
1478 self.modules[idx].edge_range.clone()
1479 } else {
1480 0..0
1481 };
1482 self.edges[range].iter().filter_map(move |edge| {
1483 let loading = || edge.symbols.iter().filter(|s| s.loads_target());
1484 let first = loading().next()?;
1485 let erased = |s: &&ImportedSymbol| {
1486 s.is_type_only || self.names_only_type_exports(edge.target, &s.imported_name)
1487 };
1488 let all_type_only = loading().all(|s| erased(&s));
1489 let span = loading()
1490 .find(|s| !erased(s))
1491 .unwrap_or(first)
1492 .import_span
1493 .start;
1494 let mut value_symbols = loading().filter(|s| !erased(s)).peekable();
1498 let all_client_only = value_symbols.peek().is_some()
1499 && value_symbols.all(|s| excluded_span_starts.contains(&s.import_span.start));
1500 Some((edge.target, all_type_only, Some(span), all_client_only))
1501 })
1502 }
1503}
1504
1505impl ModuleGraph {
1506 fn names_only_type_exports(&self, target: FileId, name: &ImportedName) -> bool {
1511 let ImportedName::Named(name) = name else {
1512 return false;
1513 };
1514 matches!(
1515 self.resolve_export(target, name, ExportNamespace::Value),
1516 EffectiveExportResolution::Missing
1517 ) && matches!(
1518 self.resolve_export(target, name, ExportNamespace::Type),
1519 EffectiveExportResolution::Unique(_)
1520 )
1521 }
1522}
1523
1524fn imported_name_label(name: &ImportedName) -> String {
1525 match name {
1526 ImportedName::Named(name) => name.clone(),
1527 ImportedName::Default => "default".to_string(),
1528 ImportedName::Namespace => "*".to_string(),
1529 ImportedName::SideEffect => "side-effect".to_string(),
1530 }
1531}
1532
1533#[cfg(test)]
1534mod tests {
1535 use super::*;
1536 use crate::resolve::{ResolveResult, ResolvedImport, ResolvedModule};
1537 use fallow_types::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
1538 use fallow_types::extract::{ExportName, ImportInfo, ImportedName, VisibilityTag};
1539 use std::path::PathBuf;
1540
1541 fn build_simple_graph() -> ModuleGraph {
1542 let files = vec![
1543 DiscoveredFile {
1544 id: FileId(0),
1545 path: PathBuf::from("/project/src/entry.ts"),
1546 size_bytes: 100,
1547 },
1548 DiscoveredFile {
1549 id: FileId(1),
1550 path: PathBuf::from("/project/src/utils.ts"),
1551 size_bytes: 50,
1552 },
1553 ];
1554
1555 let entry_points = vec![EntryPoint {
1556 path: PathBuf::from("/project/src/entry.ts"),
1557 source: EntryPointSource::PackageJsonMain,
1558 }];
1559
1560 let resolved_modules = vec![
1561 ResolvedModule {
1562 file_id: FileId(0),
1563 path: PathBuf::from("/project/src/entry.ts"),
1564 resolved_imports: vec![ResolvedImport {
1565 info: ImportInfo {
1566 source: "./utils".to_string(),
1567 imported_name: ImportedName::Named("foo".to_string()),
1568 local_name: "foo".to_string(),
1569 is_type_only: false,
1570 is_type_only_star: false,
1571 from_style: false,
1572 span: oxc_span::Span::new(0, 10),
1573 source_span: oxc_span::Span::default(),
1574 },
1575 target: ResolveResult::InternalModule(FileId(1)),
1576 }],
1577 ..Default::default()
1578 },
1579 ResolvedModule {
1580 file_id: FileId(1),
1581 path: PathBuf::from("/project/src/utils.ts"),
1582 exports: vec![
1583 fallow_types::extract::ExportInfo {
1584 name: ExportName::Named("foo".to_string()),
1585 local_name: Some("foo".to_string()),
1586 is_type_only: false,
1587 visibility: VisibilityTag::None,
1588 expected_unused_reason: None,
1589 span: oxc_span::Span::new(0, 20),
1590 members: vec![],
1591 is_side_effect_used: false,
1592 super_class: None,
1593 deprecated: false,
1594 deprecated_reason: None,
1595 },
1596 fallow_types::extract::ExportInfo {
1597 name: ExportName::Named("bar".to_string()),
1598 local_name: Some("bar".to_string()),
1599 is_type_only: false,
1600 visibility: VisibilityTag::None,
1601 expected_unused_reason: None,
1602 span: oxc_span::Span::new(25, 45),
1603 members: vec![],
1604 is_side_effect_used: false,
1605 super_class: None,
1606 deprecated: false,
1607 deprecated_reason: None,
1608 },
1609 ]
1610 .into(),
1611 ..Default::default()
1612 },
1613 ];
1614
1615 ModuleGraph::build(&resolved_modules, &entry_points, &files)
1616 }
1617
1618 fn declaration_seeding_graph(unseeded: &FxHashSet<FileId>) -> ModuleGraph {
1619 let paths = [
1620 "/project/types/ambient.d.ts",
1621 "/project/types/orphan.d.ts",
1622 "/project/types/helper.ts",
1623 ];
1624 let files: Vec<DiscoveredFile> = paths
1625 .iter()
1626 .zip(0u32..)
1627 .map(|(path, id)| DiscoveredFile {
1628 id: FileId(id),
1629 path: PathBuf::from(path),
1630 size_bytes: 10,
1631 })
1632 .collect();
1633 let resolved_modules = vec![
1634 ResolvedModule {
1635 file_id: FileId(0),
1636 path: files[0].path.clone(),
1637 ..Default::default()
1638 },
1639 ResolvedModule {
1640 file_id: FileId(1),
1641 path: files[1].path.clone(),
1642 resolved_imports: vec![ResolvedImport {
1643 info: ImportInfo {
1644 source: "./helper".to_string(),
1645 imported_name: ImportedName::Named("Helper".to_string()),
1646 local_name: "Helper".to_string(),
1647 is_type_only: true,
1648 is_type_only_star: false,
1649 from_style: false,
1650 span: oxc_span::Span::new(0, 10),
1651 source_span: oxc_span::Span::default(),
1652 },
1653 target: ResolveResult::InternalModule(FileId(2)),
1654 }],
1655 ..Default::default()
1656 },
1657 ResolvedModule {
1658 file_id: FileId(2),
1659 path: files[2].path.clone(),
1660 ..Default::default()
1661 },
1662 ];
1663 ModuleGraph::build_with_declaration_seeding(
1664 &resolved_modules,
1665 &[],
1666 &ReachabilityRoots {
1667 all: &[],
1668 runtime: &[],
1669 test: &[],
1670 },
1671 &files,
1672 unseeded,
1673 )
1674 }
1675
1676 #[test]
1677 fn every_declaration_file_is_seeded_by_default() {
1678 let graph = declaration_seeding_graph(&FxHashSet::default());
1679 assert!(graph.modules[0].is_entry_point());
1680 assert!(graph.modules[1].is_entry_point());
1681 assert!(graph.modules[2].is_reachable());
1682 }
1683
1684 #[test]
1685 fn unseeded_declaration_file_no_longer_keeps_its_imports_reachable() {
1686 let unseeded: FxHashSet<FileId> = std::iter::once(FileId(1)).collect();
1687 let graph = declaration_seeding_graph(&unseeded);
1688 assert!(graph.modules[0].is_entry_point());
1689 assert!(!graph.modules[1].is_entry_point());
1690 assert!(!graph.modules[1].is_reachable());
1691 assert!(!graph.modules[2].is_reachable());
1692 }
1693
1694 #[test]
1695 fn graph_module_count() {
1696 let graph = build_simple_graph();
1697 assert_eq!(graph.module_count(), 2);
1698 }
1699
1700 #[test]
1701 fn graph_edge_count() {
1702 let graph = build_simple_graph();
1703 assert_eq!(graph.edge_count(), 1);
1704 }
1705
1706 #[test]
1707 fn graph_entry_point_is_reachable() {
1708 let graph = build_simple_graph();
1709 assert!(graph.modules[0].is_entry_point());
1710 assert!(graph.modules[0].is_reachable());
1711 }
1712
1713 #[test]
1714 fn graph_imported_module_is_reachable() {
1715 let graph = build_simple_graph();
1716 assert!(!graph.modules[1].is_entry_point());
1717 assert!(graph.modules[1].is_reachable());
1718 }
1719
1720 #[test]
1721 #[expect(
1722 clippy::too_many_lines,
1723 reason = "this test fixture exercises four reachability roles end-to-end; splitting it \
1724 would obscure the cross-role assertions"
1725 )]
1726 fn graph_distinguishes_runtime_test_and_support_reachability() {
1727 let files = vec![
1728 DiscoveredFile {
1729 id: FileId(0),
1730 path: PathBuf::from("/project/src/main.ts"),
1731 size_bytes: 100,
1732 },
1733 DiscoveredFile {
1734 id: FileId(1),
1735 path: PathBuf::from("/project/src/runtime-only.ts"),
1736 size_bytes: 50,
1737 },
1738 DiscoveredFile {
1739 id: FileId(2),
1740 path: PathBuf::from("/project/tests/app.test.ts"),
1741 size_bytes: 50,
1742 },
1743 DiscoveredFile {
1744 id: FileId(3),
1745 path: PathBuf::from("/project/tests/setup.ts"),
1746 size_bytes: 50,
1747 },
1748 DiscoveredFile {
1749 id: FileId(4),
1750 path: PathBuf::from("/project/src/covered.ts"),
1751 size_bytes: 50,
1752 },
1753 ];
1754
1755 let all_entry_points = vec![
1756 EntryPoint {
1757 path: PathBuf::from("/project/src/main.ts"),
1758 source: EntryPointSource::PackageJsonMain,
1759 },
1760 EntryPoint {
1761 path: PathBuf::from("/project/tests/app.test.ts"),
1762 source: EntryPointSource::TestFile,
1763 },
1764 EntryPoint {
1765 path: PathBuf::from("/project/tests/setup.ts"),
1766 source: EntryPointSource::Plugin {
1767 name: "vitest".to_string(),
1768 },
1769 },
1770 ];
1771 let runtime_entry_points = vec![EntryPoint {
1772 path: PathBuf::from("/project/src/main.ts"),
1773 source: EntryPointSource::PackageJsonMain,
1774 }];
1775 let test_entry_points = vec![EntryPoint {
1776 path: PathBuf::from("/project/tests/app.test.ts"),
1777 source: EntryPointSource::TestFile,
1778 }];
1779
1780 let resolved_modules = vec![
1781 ResolvedModule {
1782 file_id: FileId(0),
1783 path: PathBuf::from("/project/src/main.ts"),
1784 resolved_imports: vec![ResolvedImport {
1785 info: ImportInfo {
1786 source: "./runtime-only".to_string(),
1787 imported_name: ImportedName::Named("runtimeOnly".to_string()),
1788 local_name: "runtimeOnly".to_string(),
1789 is_type_only: false,
1790 is_type_only_star: false,
1791 from_style: false,
1792 span: oxc_span::Span::new(0, 10),
1793 source_span: oxc_span::Span::default(),
1794 },
1795 target: ResolveResult::InternalModule(FileId(1)),
1796 }],
1797 ..Default::default()
1798 },
1799 ResolvedModule {
1800 file_id: FileId(1),
1801 path: PathBuf::from("/project/src/runtime-only.ts"),
1802 exports: vec![fallow_types::extract::ExportInfo {
1803 name: ExportName::Named("runtimeOnly".to_string()),
1804 local_name: Some("runtimeOnly".to_string()),
1805 is_type_only: false,
1806 visibility: VisibilityTag::None,
1807 expected_unused_reason: None,
1808 span: oxc_span::Span::new(0, 20),
1809 members: vec![],
1810 is_side_effect_used: false,
1811 super_class: None,
1812 deprecated: false,
1813 deprecated_reason: None,
1814 }]
1815 .into(),
1816 ..Default::default()
1817 },
1818 ResolvedModule {
1819 file_id: FileId(2),
1820 path: PathBuf::from("/project/tests/app.test.ts"),
1821 resolved_imports: vec![ResolvedImport {
1822 info: ImportInfo {
1823 source: "../src/covered".to_string(),
1824 imported_name: ImportedName::Named("covered".to_string()),
1825 local_name: "covered".to_string(),
1826 is_type_only: false,
1827 is_type_only_star: false,
1828 from_style: false,
1829 span: oxc_span::Span::new(0, 10),
1830 source_span: oxc_span::Span::default(),
1831 },
1832 target: ResolveResult::InternalModule(FileId(4)),
1833 }],
1834 ..Default::default()
1835 },
1836 ResolvedModule {
1837 file_id: FileId(3),
1838 path: PathBuf::from("/project/tests/setup.ts"),
1839 resolved_imports: vec![ResolvedImport {
1840 info: ImportInfo {
1841 source: "../src/runtime-only".to_string(),
1842 imported_name: ImportedName::Named("runtimeOnly".to_string()),
1843 local_name: "runtimeOnly".to_string(),
1844 is_type_only: false,
1845 is_type_only_star: false,
1846 from_style: false,
1847 span: oxc_span::Span::new(0, 10),
1848 source_span: oxc_span::Span::default(),
1849 },
1850 target: ResolveResult::InternalModule(FileId(1)),
1851 }],
1852 ..Default::default()
1853 },
1854 ResolvedModule {
1855 file_id: FileId(4),
1856 path: PathBuf::from("/project/src/covered.ts"),
1857 exports: vec![fallow_types::extract::ExportInfo {
1858 name: ExportName::Named("covered".to_string()),
1859 local_name: Some("covered".to_string()),
1860 is_type_only: false,
1861 visibility: VisibilityTag::None,
1862 expected_unused_reason: None,
1863 span: oxc_span::Span::new(0, 20),
1864 members: vec![],
1865 is_side_effect_used: false,
1866 super_class: None,
1867 deprecated: false,
1868 deprecated_reason: None,
1869 }]
1870 .into(),
1871 ..Default::default()
1872 },
1873 ];
1874
1875 let graph = ModuleGraph::build_with_reachability_roots(
1876 &resolved_modules,
1877 &all_entry_points,
1878 &runtime_entry_points,
1879 &test_entry_points,
1880 &files,
1881 );
1882
1883 assert!(graph.modules[1].is_reachable());
1884 assert!(graph.modules[1].is_runtime_reachable());
1885 assert!(
1886 !graph.modules[1].is_test_reachable(),
1887 "support roots should not make runtime-only modules test reachable"
1888 );
1889
1890 assert!(graph.modules[4].is_reachable());
1891 assert!(graph.modules[4].is_test_reachable());
1892 assert!(
1893 !graph.modules[4].is_runtime_reachable(),
1894 "test-only reachability should stay separate from runtime roots"
1895 );
1896 }
1897
1898 #[test]
1899 fn graph_export_has_reference() {
1900 let graph = build_simple_graph();
1901 let utils = &graph.modules[1];
1902 let foo_export = utils
1903 .exports
1904 .iter()
1905 .find(|e| e.name.to_string() == "foo")
1906 .unwrap();
1907 assert!(
1908 !foo_export.references.is_empty(),
1909 "foo should have references"
1910 );
1911 }
1912
1913 #[test]
1914 fn graph_unused_export_no_reference() {
1915 let graph = build_simple_graph();
1916 let utils = &graph.modules[1];
1917 let bar_export = utils
1918 .exports
1919 .iter()
1920 .find(|e| e.name.to_string() == "bar")
1921 .unwrap();
1922 assert!(
1923 bar_export.references.is_empty(),
1924 "bar should have no references"
1925 );
1926 }
1927
1928 #[test]
1929 fn graph_no_namespace_import() {
1930 let graph = build_simple_graph();
1931 assert!(!graph.has_namespace_import(FileId(0)));
1932 assert!(!graph.has_namespace_import(FileId(1)));
1933 }
1934
1935 #[test]
1936 fn graph_has_namespace_import() {
1937 let files = vec![
1938 DiscoveredFile {
1939 id: FileId(0),
1940 path: PathBuf::from("/project/entry.ts"),
1941 size_bytes: 100,
1942 },
1943 DiscoveredFile {
1944 id: FileId(1),
1945 path: PathBuf::from("/project/utils.ts"),
1946 size_bytes: 50,
1947 },
1948 ];
1949
1950 let entry_points = vec![EntryPoint {
1951 path: PathBuf::from("/project/entry.ts"),
1952 source: EntryPointSource::PackageJsonMain,
1953 }];
1954
1955 let resolved_modules = vec![
1956 ResolvedModule {
1957 file_id: FileId(0),
1958 path: PathBuf::from("/project/entry.ts"),
1959 resolved_imports: vec![ResolvedImport {
1960 info: ImportInfo {
1961 source: "./utils".to_string(),
1962 imported_name: ImportedName::Namespace,
1963 local_name: "utils".to_string(),
1964 is_type_only: false,
1965 is_type_only_star: false,
1966 from_style: false,
1967 span: oxc_span::Span::new(0, 10),
1968 source_span: oxc_span::Span::default(),
1969 },
1970 target: ResolveResult::InternalModule(FileId(1)),
1971 }],
1972 ..Default::default()
1973 },
1974 ResolvedModule {
1975 file_id: FileId(1),
1976 path: PathBuf::from("/project/utils.ts"),
1977 exports: vec![fallow_types::extract::ExportInfo {
1978 name: ExportName::Named("foo".to_string()),
1979 local_name: Some("foo".to_string()),
1980 is_type_only: false,
1981 visibility: VisibilityTag::None,
1982 expected_unused_reason: None,
1983 span: oxc_span::Span::new(0, 20),
1984 members: vec![],
1985 is_side_effect_used: false,
1986 super_class: None,
1987 deprecated: false,
1988 deprecated_reason: None,
1989 }]
1990 .into(),
1991 ..Default::default()
1992 },
1993 ];
1994
1995 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1996 assert!(
1997 graph.has_namespace_import(FileId(1)),
1998 "utils should have namespace import"
1999 );
2000 }
2001
2002 #[test]
2003 fn graph_has_namespace_import_out_of_bounds() {
2004 let graph = build_simple_graph();
2005 assert!(!graph.has_namespace_import(FileId(999)));
2006 }
2007
2008 #[test]
2013 fn reconstruct_namespace_imported_matches_fresh_build() {
2014 let files = vec![
2015 DiscoveredFile {
2016 id: FileId(0),
2017 path: PathBuf::from("/project/entry.ts"),
2018 size_bytes: 100,
2019 },
2020 DiscoveredFile {
2021 id: FileId(1),
2022 path: PathBuf::from("/project/utils.ts"),
2023 size_bytes: 50,
2024 },
2025 DiscoveredFile {
2026 id: FileId(2),
2027 path: PathBuf::from("/project/named-only.ts"),
2028 size_bytes: 50,
2029 },
2030 ];
2031 let entry_points = vec![EntryPoint {
2032 path: PathBuf::from("/project/entry.ts"),
2033 source: EntryPointSource::PackageJsonMain,
2034 }];
2035 let resolved_modules = vec![
2036 ResolvedModule {
2037 file_id: FileId(0),
2038 path: PathBuf::from("/project/entry.ts"),
2039 resolved_imports: vec![
2040 ResolvedImport {
2041 info: ImportInfo {
2042 source: "./utils".to_string(),
2043 imported_name: ImportedName::Namespace,
2044 local_name: "utils".to_string(),
2045 is_type_only: false,
2046 is_type_only_star: false,
2047 from_style: false,
2048 span: oxc_span::Span::new(0, 10),
2049 source_span: oxc_span::Span::default(),
2050 },
2051 target: ResolveResult::InternalModule(FileId(1)),
2052 },
2053 ResolvedImport {
2054 info: ImportInfo {
2055 source: "./named-only".to_string(),
2056 imported_name: ImportedName::Named("foo".to_string()),
2057 local_name: "foo".to_string(),
2058 is_type_only: false,
2059 is_type_only_star: false,
2060 from_style: false,
2061 span: oxc_span::Span::new(11, 20),
2062 source_span: oxc_span::Span::default(),
2063 },
2064 target: ResolveResult::InternalModule(FileId(2)),
2065 },
2066 ],
2067 ..Default::default()
2068 },
2069 ResolvedModule {
2070 file_id: FileId(1),
2071 path: PathBuf::from("/project/utils.ts"),
2072 ..Default::default()
2073 },
2074 ResolvedModule {
2075 file_id: FileId(2),
2076 path: PathBuf::from("/project/named-only.ts"),
2077 exports: vec![fallow_types::extract::ExportInfo {
2078 name: ExportName::Named("foo".to_string()),
2079 local_name: Some("foo".to_string()),
2080 is_type_only: false,
2081 visibility: VisibilityTag::None,
2082 expected_unused_reason: None,
2083 span: oxc_span::Span::new(0, 20),
2084 members: vec![],
2085 is_side_effect_used: false,
2086 super_class: None,
2087 deprecated: false,
2088 deprecated_reason: None,
2089 }]
2090 .into(),
2091 ..Default::default()
2092 },
2093 ];
2094
2095 let mut graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
2096 let fresh = graph.namespace_imported.clone();
2097
2098 assert!(graph.has_namespace_import(FileId(1)));
2100 assert!(!graph.has_namespace_import(FileId(2)));
2101
2102 graph.namespace_imported = FixedBitSet::default();
2105 graph.reconstruct_namespace_imported();
2106
2107 assert_eq!(
2108 graph.namespace_imported, fresh,
2109 "reconstructed namespace_imported must equal the fresh-built bitset"
2110 );
2111 assert!(graph.has_namespace_import(FileId(1)));
2112 assert!(!graph.has_namespace_import(FileId(2)));
2113 }
2114
2115 #[test]
2116 fn graph_unreachable_module() {
2117 let files = vec![
2118 DiscoveredFile {
2119 id: FileId(0),
2120 path: PathBuf::from("/project/entry.ts"),
2121 size_bytes: 100,
2122 },
2123 DiscoveredFile {
2124 id: FileId(1),
2125 path: PathBuf::from("/project/utils.ts"),
2126 size_bytes: 50,
2127 },
2128 DiscoveredFile {
2129 id: FileId(2),
2130 path: PathBuf::from("/project/orphan.ts"),
2131 size_bytes: 30,
2132 },
2133 ];
2134
2135 let entry_points = vec![EntryPoint {
2136 path: PathBuf::from("/project/entry.ts"),
2137 source: EntryPointSource::PackageJsonMain,
2138 }];
2139
2140 let resolved_modules = vec![
2141 ResolvedModule {
2142 file_id: FileId(0),
2143 path: PathBuf::from("/project/entry.ts"),
2144 resolved_imports: vec![ResolvedImport {
2145 info: ImportInfo {
2146 source: "./utils".to_string(),
2147 imported_name: ImportedName::Named("foo".to_string()),
2148 local_name: "foo".to_string(),
2149 is_type_only: false,
2150 is_type_only_star: false,
2151 from_style: false,
2152 span: oxc_span::Span::new(0, 10),
2153 source_span: oxc_span::Span::default(),
2154 },
2155 target: ResolveResult::InternalModule(FileId(1)),
2156 }],
2157 ..Default::default()
2158 },
2159 ResolvedModule {
2160 file_id: FileId(1),
2161 path: PathBuf::from("/project/utils.ts"),
2162 exports: vec![fallow_types::extract::ExportInfo {
2163 name: ExportName::Named("foo".to_string()),
2164 local_name: Some("foo".to_string()),
2165 is_type_only: false,
2166 visibility: VisibilityTag::None,
2167 expected_unused_reason: None,
2168 span: oxc_span::Span::new(0, 20),
2169 members: vec![],
2170 is_side_effect_used: false,
2171 super_class: None,
2172 deprecated: false,
2173 deprecated_reason: None,
2174 }]
2175 .into(),
2176 ..Default::default()
2177 },
2178 ResolvedModule {
2179 file_id: FileId(2),
2180 path: PathBuf::from("/project/orphan.ts"),
2181 exports: vec![fallow_types::extract::ExportInfo {
2182 name: ExportName::Named("orphan".to_string()),
2183 local_name: Some("orphan".to_string()),
2184 is_type_only: false,
2185 visibility: VisibilityTag::None,
2186 expected_unused_reason: None,
2187 span: oxc_span::Span::new(0, 20),
2188 members: vec![],
2189 is_side_effect_used: false,
2190 super_class: None,
2191 deprecated: false,
2192 deprecated_reason: None,
2193 }]
2194 .into(),
2195 ..Default::default()
2196 },
2197 ];
2198
2199 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
2200
2201 assert!(graph.modules[0].is_reachable(), "entry should be reachable");
2202 assert!(graph.modules[1].is_reachable(), "utils should be reachable");
2203 assert!(
2204 !graph.modules[2].is_reachable(),
2205 "orphan should NOT be reachable"
2206 );
2207 }
2208
2209 #[test]
2210 fn graph_package_usage_tracked() {
2211 let files = vec![DiscoveredFile {
2212 id: FileId(0),
2213 path: PathBuf::from("/project/entry.ts"),
2214 size_bytes: 100,
2215 }];
2216
2217 let entry_points = vec![EntryPoint {
2218 path: PathBuf::from("/project/entry.ts"),
2219 source: EntryPointSource::PackageJsonMain,
2220 }];
2221
2222 let resolved_modules = vec![ResolvedModule {
2223 file_id: FileId(0),
2224 path: PathBuf::from("/project/entry.ts"),
2225 exports: vec![].into(),
2226 re_exports: vec![],
2227 resolved_imports: vec![
2228 ResolvedImport {
2229 info: ImportInfo {
2230 source: "react".to_string(),
2231 imported_name: ImportedName::Default,
2232 local_name: "React".to_string(),
2233 is_type_only: false,
2234 is_type_only_star: false,
2235 from_style: false,
2236 span: oxc_span::Span::new(0, 10),
2237 source_span: oxc_span::Span::default(),
2238 },
2239 target: ResolveResult::NpmPackage("react".to_string()),
2240 },
2241 ResolvedImport {
2242 info: ImportInfo {
2243 source: "lodash".to_string(),
2244 imported_name: ImportedName::Named("merge".to_string()),
2245 local_name: "merge".to_string(),
2246 is_type_only: false,
2247 is_type_only_star: false,
2248 from_style: false,
2249 span: oxc_span::Span::new(15, 30),
2250 source_span: oxc_span::Span::default(),
2251 },
2252 target: ResolveResult::NpmPackage("lodash".to_string()),
2253 },
2254 ],
2255 ..Default::default()
2256 }];
2257
2258 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
2259 assert!(graph.package_usage.contains_key("react"));
2260 assert!(graph.package_usage.contains_key("lodash"));
2261 assert!(!graph.package_usage.contains_key("express"));
2262 }
2263
2264 #[test]
2265 fn graph_empty() {
2266 let graph = ModuleGraph::build(&[], &[], &[]);
2267 assert_eq!(graph.module_count(), 0);
2268 assert_eq!(graph.edge_count(), 0);
2269 }
2270
2271 #[test]
2277 fn graph_postcard_round_trip_is_lossless() {
2278 let graph = build_simple_graph();
2279
2280 let encoded = postcard::to_allocvec(&graph).expect("encode graph");
2281 let mut decoded: ModuleGraph = postcard::from_bytes(&encoded).expect("decode graph");
2282 decoded.reconstruct_namespace_imported();
2284
2285 assert_eq!(decoded.module_count(), graph.module_count());
2286 assert_eq!(decoded.edge_count(), graph.edge_count());
2287 assert_eq!(decoded.namespace_imported, graph.namespace_imported);
2288
2289 let utils = &decoded.modules[1];
2291 let foo = utils
2292 .exports
2293 .iter()
2294 .find(|e| e.name.to_string() == "foo")
2295 .expect("foo export survives round-trip");
2296 assert!(!foo.references.is_empty());
2297 let bar = utils
2298 .exports
2299 .iter()
2300 .find(|e| e.name.to_string() == "bar")
2301 .expect("bar export survives round-trip");
2302 assert!(bar.references.is_empty());
2303
2304 assert!(decoded.modules[0].is_entry_point());
2306 assert!(decoded.modules[0].is_reachable());
2307 assert!(decoded.modules[1].is_reachable());
2308 assert_eq!(decoded.entry_points, graph.entry_points);
2309 }
2310
2311 #[test]
2312 fn graph_cjs_exports_tracked() {
2313 let files = vec![DiscoveredFile {
2314 id: FileId(0),
2315 path: PathBuf::from("/project/entry.ts"),
2316 size_bytes: 100,
2317 }];
2318
2319 let entry_points = vec![EntryPoint {
2320 path: PathBuf::from("/project/entry.ts"),
2321 source: EntryPointSource::PackageJsonMain,
2322 }];
2323
2324 let resolved_modules = vec![ResolvedModule {
2325 file_id: FileId(0),
2326 path: PathBuf::from("/project/entry.ts"),
2327 has_cjs_exports: true,
2328 has_angular_component_template_url: false,
2329 ..Default::default()
2330 }];
2331
2332 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
2333 assert!(graph.modules[0].has_cjs_exports());
2334 }
2335
2336 #[test]
2337 fn graph_edges_for_returns_targets() {
2338 let graph = build_simple_graph();
2339 let targets = graph.edges_for(FileId(0));
2340 assert_eq!(targets, vec![FileId(1)]);
2341 }
2342
2343 #[test]
2344 fn graph_edges_for_no_imports() {
2345 let graph = build_simple_graph();
2346 let targets = graph.edges_for(FileId(1));
2347 assert!(targets.is_empty());
2348 }
2349
2350 #[test]
2351 fn graph_edges_for_out_of_bounds() {
2352 let graph = build_simple_graph();
2353 let targets = graph.edges_for(FileId(999));
2354 assert!(targets.is_empty());
2355 }
2356
2357 #[test]
2358 fn graph_direct_importer_summaries_include_symbols() {
2359 let graph = build_simple_graph();
2360 let summaries = graph.direct_importer_summaries(FileId(1));
2361
2362 assert_eq!(
2363 summaries,
2364 vec![DirectImporterSummary {
2365 source: FileId(0),
2366 symbols: vec![ImportedSymbolSummary {
2367 imported: "foo".to_string(),
2368 local: "foo".to_string(),
2369 type_only: false,
2370 }],
2371 }]
2372 );
2373 }
2374
2375 #[test]
2376 fn graph_find_import_span_start_found() {
2377 let graph = build_simple_graph();
2378 let span_start = graph.find_import_span_start(FileId(0), FileId(1));
2379 assert!(span_start.is_some());
2380 assert_eq!(span_start.unwrap(), 0);
2381 }
2382
2383 #[test]
2384 fn graph_find_import_span_start_prefers_value_import_on_mixed_edge() {
2385 let files = vec![
2386 DiscoveredFile {
2387 id: FileId(0),
2388 path: PathBuf::from("/project/entry.ts"),
2389 size_bytes: 100,
2390 },
2391 DiscoveredFile {
2392 id: FileId(1),
2393 path: PathBuf::from("/project/utils.ts"),
2394 size_bytes: 50,
2395 },
2396 ];
2397 let entry_points = vec![EntryPoint {
2398 path: PathBuf::from("/project/entry.ts"),
2399 source: EntryPointSource::PackageJsonMain,
2400 }];
2401 let resolved_modules = vec![
2402 ResolvedModule {
2403 file_id: FileId(0),
2404 path: PathBuf::from("/project/entry.ts"),
2405 resolved_imports: vec![
2406 ResolvedImport {
2407 info: ImportInfo {
2408 source: "./utils".to_string(),
2409 imported_name: ImportedName::Named("Foo".to_string()),
2410 local_name: "Foo".to_string(),
2411 is_type_only: true,
2412 is_type_only_star: false,
2413 from_style: false,
2414 span: oxc_span::Span::new(10, 20),
2415 source_span: oxc_span::Span::default(),
2416 },
2417 target: ResolveResult::InternalModule(FileId(1)),
2418 },
2419 ResolvedImport {
2420 info: ImportInfo {
2421 source: "./utils".to_string(),
2422 imported_name: ImportedName::Named("foo".to_string()),
2423 local_name: "foo".to_string(),
2424 is_type_only: false,
2425 is_type_only_star: false,
2426 from_style: false,
2427 span: oxc_span::Span::new(50, 60),
2428 source_span: oxc_span::Span::default(),
2429 },
2430 target: ResolveResult::InternalModule(FileId(1)),
2431 },
2432 ],
2433 ..Default::default()
2434 },
2435 ResolvedModule {
2436 file_id: FileId(1),
2437 path: PathBuf::from("/project/utils.ts"),
2438 ..Default::default()
2439 },
2440 ];
2441
2442 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
2443 assert_eq!(graph.find_import_span_start(FileId(0), FileId(1)), Some(50));
2444 }
2445
2446 #[test]
2447 fn graph_find_import_span_start_wrong_target() {
2448 let graph = build_simple_graph();
2449 let span_start = graph.find_import_span_start(FileId(0), FileId(0));
2450 assert!(span_start.is_none());
2451 }
2452
2453 #[test]
2454 fn graph_find_import_span_start_source_out_of_bounds() {
2455 let graph = build_simple_graph();
2456 let span_start = graph.find_import_span_start(FileId(999), FileId(1));
2457 assert!(span_start.is_none());
2458 }
2459
2460 #[test]
2461 fn graph_find_import_span_start_no_edges() {
2462 let graph = build_simple_graph();
2463 let span_start = graph.find_import_span_start(FileId(1), FileId(0));
2464 assert!(span_start.is_none());
2465 }
2466
2467 #[test]
2468 fn graph_reverse_deps_populated() {
2469 let graph = build_simple_graph();
2470 assert!(graph.reverse_deps[1].contains(&FileId(0)));
2471 assert!(graph.reverse_deps[0].is_empty());
2472 }
2473
2474 #[test]
2475 fn graph_type_only_package_usage_tracked() {
2476 let files = vec![DiscoveredFile {
2477 id: FileId(0),
2478 path: PathBuf::from("/project/entry.ts"),
2479 size_bytes: 100,
2480 }];
2481 let entry_points = vec![EntryPoint {
2482 path: PathBuf::from("/project/entry.ts"),
2483 source: EntryPointSource::PackageJsonMain,
2484 }];
2485 let resolved_modules = vec![ResolvedModule {
2486 file_id: FileId(0),
2487 path: PathBuf::from("/project/entry.ts"),
2488 resolved_imports: vec![
2489 ResolvedImport {
2490 info: ImportInfo {
2491 source: "react".to_string(),
2492 imported_name: ImportedName::Named("FC".to_string()),
2493 local_name: "FC".to_string(),
2494 is_type_only: true,
2495 is_type_only_star: false,
2496 from_style: false,
2497 span: oxc_span::Span::new(0, 10),
2498 source_span: oxc_span::Span::default(),
2499 },
2500 target: ResolveResult::NpmPackage("react".to_string()),
2501 },
2502 ResolvedImport {
2503 info: ImportInfo {
2504 source: "react".to_string(),
2505 imported_name: ImportedName::Named("useState".to_string()),
2506 local_name: "useState".to_string(),
2507 is_type_only: false,
2508 is_type_only_star: false,
2509 from_style: false,
2510 span: oxc_span::Span::new(15, 30),
2511 source_span: oxc_span::Span::default(),
2512 },
2513 target: ResolveResult::NpmPackage("react".to_string()),
2514 },
2515 ],
2516 ..Default::default()
2517 }];
2518
2519 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
2520 assert!(graph.package_usage.contains_key("react"));
2521 assert!(graph.type_only_package_usage.contains_key("react"));
2522 }
2523
2524 #[test]
2525 fn graph_default_import_reference() {
2526 let files = vec![
2527 DiscoveredFile {
2528 id: FileId(0),
2529 path: PathBuf::from("/project/entry.ts"),
2530 size_bytes: 100,
2531 },
2532 DiscoveredFile {
2533 id: FileId(1),
2534 path: PathBuf::from("/project/utils.ts"),
2535 size_bytes: 50,
2536 },
2537 ];
2538 let entry_points = vec![EntryPoint {
2539 path: PathBuf::from("/project/entry.ts"),
2540 source: EntryPointSource::PackageJsonMain,
2541 }];
2542 let resolved_modules = vec![
2543 ResolvedModule {
2544 file_id: FileId(0),
2545 path: PathBuf::from("/project/entry.ts"),
2546 resolved_imports: vec![ResolvedImport {
2547 info: ImportInfo {
2548 source: "./utils".to_string(),
2549 imported_name: ImportedName::Default,
2550 local_name: "Utils".to_string(),
2551 is_type_only: false,
2552 is_type_only_star: false,
2553 from_style: false,
2554 span: oxc_span::Span::new(0, 10),
2555 source_span: oxc_span::Span::default(),
2556 },
2557 target: ResolveResult::InternalModule(FileId(1)),
2558 }],
2559 ..Default::default()
2560 },
2561 ResolvedModule {
2562 file_id: FileId(1),
2563 path: PathBuf::from("/project/utils.ts"),
2564 exports: vec![fallow_types::extract::ExportInfo {
2565 name: ExportName::Default,
2566 local_name: None,
2567 is_type_only: false,
2568 visibility: VisibilityTag::None,
2569 expected_unused_reason: None,
2570 span: oxc_span::Span::new(0, 20),
2571 members: vec![],
2572 is_side_effect_used: false,
2573 super_class: None,
2574 deprecated: false,
2575 deprecated_reason: None,
2576 }]
2577 .into(),
2578 ..Default::default()
2579 },
2580 ];
2581
2582 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
2583 let utils = &graph.modules[1];
2584 let default_export = utils
2585 .exports
2586 .iter()
2587 .find(|e| matches!(e.name, ExportName::Default))
2588 .unwrap();
2589 assert!(!default_export.references.is_empty());
2590 assert_eq!(
2591 default_export.references[0].kind,
2592 ReferenceKind::DefaultImport
2593 );
2594 }
2595
2596 #[test]
2597 fn graph_side_effect_import_no_export_reference() {
2598 let files = vec![
2599 DiscoveredFile {
2600 id: FileId(0),
2601 path: PathBuf::from("/project/entry.ts"),
2602 size_bytes: 100,
2603 },
2604 DiscoveredFile {
2605 id: FileId(1),
2606 path: PathBuf::from("/project/styles.ts"),
2607 size_bytes: 50,
2608 },
2609 ];
2610 let entry_points = vec![EntryPoint {
2611 path: PathBuf::from("/project/entry.ts"),
2612 source: EntryPointSource::PackageJsonMain,
2613 }];
2614 let resolved_modules = vec![
2615 ResolvedModule {
2616 file_id: FileId(0),
2617 path: PathBuf::from("/project/entry.ts"),
2618 resolved_imports: vec![ResolvedImport {
2619 info: ImportInfo {
2620 source: "./styles".to_string(),
2621 imported_name: ImportedName::SideEffect,
2622 local_name: String::new(),
2623 is_type_only: false,
2624 is_type_only_star: false,
2625 from_style: false,
2626 span: oxc_span::Span::new(0, 10),
2627 source_span: oxc_span::Span::default(),
2628 },
2629 target: ResolveResult::InternalModule(FileId(1)),
2630 }],
2631 ..Default::default()
2632 },
2633 ResolvedModule {
2634 file_id: FileId(1),
2635 path: PathBuf::from("/project/styles.ts"),
2636 exports: vec![fallow_types::extract::ExportInfo {
2637 name: ExportName::Named("primaryColor".to_string()),
2638 local_name: Some("primaryColor".to_string()),
2639 is_type_only: false,
2640 visibility: VisibilityTag::None,
2641 expected_unused_reason: None,
2642 span: oxc_span::Span::new(0, 20),
2643 members: vec![],
2644 is_side_effect_used: false,
2645 super_class: None,
2646 deprecated: false,
2647 deprecated_reason: None,
2648 }]
2649 .into(),
2650 ..Default::default()
2651 },
2652 ];
2653
2654 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
2655 assert_eq!(graph.edge_count(), 1);
2656 let styles = &graph.modules[1];
2657 assert!(styles.is_reachable());
2658 let export = &styles.exports[0];
2659 assert!(
2660 export.references.is_empty(),
2661 "side-effect import should not reference named exports"
2662 );
2663
2664 let encoded = postcard::to_allocvec(&graph).expect("encode graph");
2665 let decoded: ModuleGraph = postcard::from_bytes(&encoded).expect("decode graph");
2666 assert_eq!(decoded.edge_count(), 1);
2667 assert!(decoded.modules[1].is_reachable());
2668 assert!(decoded.modules[1].exports[0].references.is_empty());
2669 }
2670
2671 #[test]
2672 fn graph_multiple_entry_points() {
2673 let files = vec![
2674 DiscoveredFile {
2675 id: FileId(0),
2676 path: PathBuf::from("/project/main.ts"),
2677 size_bytes: 100,
2678 },
2679 DiscoveredFile {
2680 id: FileId(1),
2681 path: PathBuf::from("/project/worker.ts"),
2682 size_bytes: 100,
2683 },
2684 DiscoveredFile {
2685 id: FileId(2),
2686 path: PathBuf::from("/project/shared.ts"),
2687 size_bytes: 50,
2688 },
2689 ];
2690 let entry_points = vec![
2691 EntryPoint {
2692 path: PathBuf::from("/project/main.ts"),
2693 source: EntryPointSource::PackageJsonMain,
2694 },
2695 EntryPoint {
2696 path: PathBuf::from("/project/worker.ts"),
2697 source: EntryPointSource::PackageJsonMain,
2698 },
2699 ];
2700 let resolved_modules = vec![
2701 ResolvedModule {
2702 file_id: FileId(0),
2703 path: PathBuf::from("/project/main.ts"),
2704 resolved_imports: vec![ResolvedImport {
2705 info: ImportInfo {
2706 source: "./shared".to_string(),
2707 imported_name: ImportedName::Named("helper".to_string()),
2708 local_name: "helper".to_string(),
2709 is_type_only: false,
2710 is_type_only_star: false,
2711 from_style: false,
2712 span: oxc_span::Span::new(0, 10),
2713 source_span: oxc_span::Span::default(),
2714 },
2715 target: ResolveResult::InternalModule(FileId(2)),
2716 }],
2717 ..Default::default()
2718 },
2719 ResolvedModule {
2720 file_id: FileId(1),
2721 path: PathBuf::from("/project/worker.ts"),
2722 ..Default::default()
2723 },
2724 ResolvedModule {
2725 file_id: FileId(2),
2726 path: PathBuf::from("/project/shared.ts"),
2727 exports: vec![fallow_types::extract::ExportInfo {
2728 name: ExportName::Named("helper".to_string()),
2729 local_name: Some("helper".to_string()),
2730 is_type_only: false,
2731 visibility: VisibilityTag::None,
2732 expected_unused_reason: None,
2733 span: oxc_span::Span::new(0, 20),
2734 members: vec![],
2735 is_side_effect_used: false,
2736 super_class: None,
2737 deprecated: false,
2738 deprecated_reason: None,
2739 }]
2740 .into(),
2741 ..Default::default()
2742 },
2743 ];
2744
2745 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
2746 assert!(graph.modules[0].is_entry_point());
2747 assert!(graph.modules[1].is_entry_point());
2748 assert!(!graph.modules[2].is_entry_point());
2749 assert!(graph.modules[0].is_reachable());
2750 assert!(graph.modules[1].is_reachable());
2751 assert!(graph.modules[2].is_reachable());
2752 }
2753}