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