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