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