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