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