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