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