1mod build;
7mod cycles;
8mod fan_io;
9mod impact_closure;
10mod namespace_aliases;
11mod namespace_indexes;
12mod namespace_re_exports;
13mod narrowing;
14mod partition_order;
15mod public_exports;
16mod re_exports;
17mod reachability;
18pub mod types;
19
20use std::path::Path;
21
22use fixedbitset::FixedBitSet;
23use rustc_hash::{FxHashMap, FxHashSet};
24
25use crate::resolve::ResolvedModule;
26use fallow_types::discover::{DiscoveredFile, EntryPoint, FileId};
27use fallow_types::extract::ImportedName;
28
29pub use fan_io::{FocusFileFacts, FocusFileFactsPaths};
30pub use impact_closure::{
31 CoordinationGap, CoordinationGapPaths, ImpactClosure, ImpactClosurePaths,
32};
33pub use partition_order::{PartitionOrder, PartitionOrderPaths, ReviewUnit, ReviewUnitPaths};
34pub use re_exports::GraphReExportCycle;
35pub use types::{ExportSymbol, ModuleNode, ReExportEdge, ReferenceKind, SymbolReference};
36
37fn is_declaration_file_path(path: &Path) -> bool {
44 path.file_name()
45 .and_then(|n| n.to_str())
46 .is_some_and(|name| {
47 name.ends_with(".d.ts") || name.ends_with(".d.mts") || name.ends_with(".d.cts")
48 })
49}
50
51#[derive(Debug, serde::Serialize, serde::Deserialize)]
59pub struct ModuleGraph {
60 pub modules: Vec<ModuleNode>,
70 edges: Vec<Edge>,
72 pub package_usage: FxHashMap<String, Vec<FileId>>,
74 pub type_only_package_usage: FxHashMap<String, Vec<FileId>>,
78 pub entry_points: FxHashSet<FileId>,
80 pub runtime_entry_points: FxHashSet<FileId>,
82 pub test_entry_points: FxHashSet<FileId>,
84 pub reverse_deps: Vec<Vec<FileId>>,
86 #[serde(skip, default)]
94 namespace_imported: FixedBitSet,
95 pub re_export_cycles: Vec<GraphReExportCycle>,
102}
103
104#[derive(Debug, serde::Serialize, serde::Deserialize)]
111pub struct Edge {
112 source: FileId,
114 target: FileId,
116 symbols: Vec<ImportedSymbol>,
118}
119
120#[derive(Debug, serde::Serialize, serde::Deserialize)]
122pub struct ImportedSymbol {
123 pub imported_name: ImportedName,
126 pub local_name: String,
128 #[serde(with = "crate::cache::span_serde")]
130 pub import_span: oxc_span::Span,
131 pub is_type_only: bool,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct DirectImporterSummary {
139 pub source: FileId,
141 pub symbols: Vec<ImportedSymbolSummary>,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct ImportedSymbolSummary {
148 pub imported: String,
151 pub local: String,
153 pub type_only: bool,
155}
156
157#[cfg(target_pointer_width = "64")]
158const _: () = assert!(std::mem::size_of::<Edge>() == 32);
159#[cfg(target_pointer_width = "64")]
160const _: () = assert!(std::mem::size_of::<ImportedSymbol>() == 64);
161
162#[cold]
163#[inline(never)]
164fn propagate_namespace_references(
165 graph: &mut ModuleGraph,
166 module_by_id: &FxHashMap<FileId, &ResolvedModule>,
167 features: build::NamespaceFeatures,
168) {
169 let indexes = namespace_indexes::NamespacePropagationIndexes::new(graph, module_by_id);
170 if features.has_aliases {
171 namespace_aliases::propagate_cross_package_aliases(graph, module_by_id, &indexes);
172 }
173 if features.has_re_exports {
174 namespace_re_exports::propagate_namespace_re_exports(graph, &indexes);
175 }
176}
177
178impl ModuleGraph {
179 fn resolve_entry_point_ids(
180 entry_points: &[EntryPoint],
181 path_to_id: &FxHashMap<&Path, FileId>,
182 ) -> FxHashSet<FileId> {
183 entry_points
184 .iter()
185 .filter_map(|ep| {
186 path_to_id.get(ep.path.as_path()).copied().or_else(|| {
187 dunce::canonicalize(&ep.path)
188 .ok()
189 .and_then(|path| path_to_id.get(path.as_path()).copied())
190 })
191 })
192 .collect()
193 }
194
195 pub fn build(
197 resolved_modules: &[ResolvedModule],
198 entry_points: &[EntryPoint],
199 files: &[DiscoveredFile],
200 ) -> Self {
201 Self::build_with_reachability_roots(
202 resolved_modules,
203 entry_points,
204 entry_points,
205 &[],
206 files,
207 )
208 }
209
210 pub fn build_with_reachability_roots(
212 resolved_modules: &[ResolvedModule],
213 entry_points: &[EntryPoint],
214 runtime_entry_points: &[EntryPoint],
215 test_entry_points: &[EntryPoint],
216 files: &[DiscoveredFile],
217 ) -> Self {
218 let _span = tracing::info_span!("build_graph").entered();
219
220 let module_count = files.len();
221
222 let max_file_id = files
223 .iter()
224 .map(|f| f.id.0 as usize)
225 .max()
226 .map_or(0, |m| m + 1);
227 let total_capacity = max_file_id.max(module_count);
228
229 let path_to_id: FxHashMap<&Path, FileId> =
230 files.iter().map(|f| (f.path.as_path(), f.id)).collect();
231
232 let module_by_id: FxHashMap<FileId, &ResolvedModule> =
233 resolved_modules.iter().map(|m| (m.file_id, m)).collect();
234
235 let mut entry_point_ids = Self::resolve_entry_point_ids(entry_points, &path_to_id);
236 let runtime_entry_point_ids =
237 Self::resolve_entry_point_ids(runtime_entry_points, &path_to_id);
238 let test_entry_point_ids = Self::resolve_entry_point_ids(test_entry_points, &path_to_id);
239
240 for file in files {
241 if is_declaration_file_path(&file.path) {
242 entry_point_ids.insert(file.id);
243 }
244 }
245
246 let (mut graph, namespace_features) = Self::populate_edges(&build::PopulateEdgesInput {
247 files,
248 module_by_id: &module_by_id,
249 entry_point_ids: &entry_point_ids,
250 runtime_entry_point_ids: &runtime_entry_point_ids,
251 test_entry_point_ids: &test_entry_point_ids,
252 module_count,
253 total_capacity,
254 });
255
256 graph.populate_references(&module_by_id, &entry_point_ids);
257
258 if namespace_features.has_aliases || namespace_features.has_re_exports {
259 propagate_namespace_references(&mut graph, &module_by_id, namespace_features);
260 }
261
262 graph.mark_reachable(
263 &entry_point_ids,
264 &runtime_entry_point_ids,
265 &test_entry_point_ids,
266 total_capacity,
267 );
268
269 graph.re_export_cycles = graph.resolve_re_export_chains(&module_by_id);
270
271 graph
272 }
273
274 #[must_use]
276 pub const fn module_count(&self) -> usize {
277 self.modules.len()
278 }
279
280 #[must_use]
282 pub const fn edge_count(&self) -> usize {
283 self.edges.len()
284 }
285
286 pub(crate) fn reconstruct_namespace_imported(&mut self) {
301 let capacity = self
302 .edges
303 .iter()
304 .map(|edge| edge.target.0 as usize + 1)
305 .max()
306 .unwrap_or(0)
307 .max(self.modules.len());
308 let mut bitset = FixedBitSet::with_capacity(capacity);
309 for edge in &self.edges {
310 if edge
311 .symbols
312 .iter()
313 .any(|sym| matches!(sym.imported_name, ImportedName::Namespace))
314 {
315 let idx = edge.target.0 as usize;
316 if idx < capacity {
317 bitset.insert(idx);
318 }
319 }
320 }
321 self.namespace_imported = bitset;
322 }
323
324 #[must_use]
327 pub fn has_namespace_import(&self, file_id: FileId) -> bool {
328 let idx = file_id.0 as usize;
329 if idx >= self.namespace_imported.len() {
330 return false;
331 }
332 self.namespace_imported.contains(idx)
333 }
334
335 #[must_use]
337 pub fn edges_for(&self, file_id: FileId) -> Vec<FileId> {
338 let idx = file_id.0 as usize;
339 if idx >= self.modules.len() {
340 return Vec::new();
341 }
342 let range = &self.modules[idx].edge_range;
343 self.edges[range.clone()].iter().map(|e| e.target).collect()
344 }
345
346 pub fn outgoing_symbol_edges(
352 &self,
353 file_id: FileId,
354 ) -> impl Iterator<Item = (FileId, &[ImportedSymbol])> + '_ {
355 let idx = file_id.0 as usize;
356 let range = if idx < self.modules.len() {
357 self.modules[idx].edge_range.clone()
358 } else {
359 0..0
360 };
361 self.edges[range]
362 .iter()
363 .map(|edge| (edge.target, edge.symbols.as_slice()))
364 }
365
366 #[must_use]
370 pub fn importers_of(&self, target: FileId) -> &[FileId] {
371 self.reverse_deps
372 .get(target.0 as usize)
373 .map_or(&[], Vec::as_slice)
374 }
375
376 #[must_use]
381 pub fn direct_importer_summaries(&self, target: FileId) -> Vec<DirectImporterSummary> {
382 let Some(importers) = self.reverse_deps.get(target.0 as usize) else {
383 return Vec::new();
384 };
385
386 let mut summaries = Vec::new();
387 for &source in importers {
388 let idx = source.0 as usize;
389 let Some(source_node) = self.modules.get(idx) else {
390 continue;
391 };
392 let mut symbols = Vec::new();
393 for edge in &self.edges[source_node.edge_range.clone()] {
394 if edge.target != target {
395 continue;
396 }
397 symbols.extend(edge.symbols.iter().map(|symbol| ImportedSymbolSummary {
398 imported: imported_name_label(&symbol.imported_name),
399 local: symbol.local_name.clone(),
400 type_only: symbol.is_type_only,
401 }));
402 }
403 symbols.sort_by(|a, b| {
404 a.imported
405 .cmp(&b.imported)
406 .then_with(|| a.local.cmp(&b.local))
407 .then_with(|| a.type_only.cmp(&b.type_only))
408 });
409 symbols.dedup();
410 summaries.push(DirectImporterSummary { source, symbols });
411 }
412 summaries.sort_by_key(|summary| summary.source.0);
413 summaries
414 }
415
416 #[must_use]
423 pub fn find_import_span_start(&self, source: FileId, target: FileId) -> Option<u32> {
424 let idx = source.0 as usize;
425 if idx >= self.modules.len() {
426 return None;
427 }
428 let range = &self.modules[idx].edge_range;
429 for edge in &self.edges[range.clone()] {
430 if edge.target == target {
431 return edge
432 .symbols
433 .iter()
434 .find(|s| !s.is_type_only)
435 .or_else(|| edge.symbols.first())
436 .map(|s| s.import_span.start);
437 }
438 }
439 None
440 }
441
442 pub fn outgoing_edge_summaries(
457 &self,
458 file_id: FileId,
459 ) -> impl Iterator<Item = (FileId, bool, Option<u32>)> + '_ {
460 let idx = file_id.0 as usize;
461 let range = if idx < self.modules.len() {
462 self.modules[idx].edge_range.clone()
463 } else {
464 0..0
465 };
466 self.edges[range].iter().map(|edge| {
467 let all_type_only =
468 !edge.symbols.is_empty() && edge.symbols.iter().all(|s| s.is_type_only);
469 let span = edge
470 .symbols
471 .iter()
472 .find(|s| !s.is_type_only)
473 .or_else(|| edge.symbols.first())
474 .map(|s| s.import_span.start);
475 (edge.target, all_type_only, span)
476 })
477 }
478
479 pub fn outgoing_edge_summaries_with_exclusions<'a>(
490 &'a self,
491 file_id: FileId,
492 excluded_span_starts: &'a FxHashSet<u32>,
493 ) -> impl Iterator<Item = (FileId, bool, Option<u32>, bool)> + 'a {
494 let idx = file_id.0 as usize;
495 let range = if idx < self.modules.len() {
496 self.modules[idx].edge_range.clone()
497 } else {
498 0..0
499 };
500 self.edges[range].iter().map(move |edge| {
501 let all_type_only =
502 !edge.symbols.is_empty() && edge.symbols.iter().all(|s| s.is_type_only);
503 let span = edge
504 .symbols
505 .iter()
506 .find(|s| !s.is_type_only)
507 .or_else(|| edge.symbols.first())
508 .map(|s| s.import_span.start);
509 let mut value_symbols = edge.symbols.iter().filter(|s| !s.is_type_only).peekable();
513 let all_client_only = value_symbols.peek().is_some()
514 && value_symbols.all(|s| excluded_span_starts.contains(&s.import_span.start));
515 (edge.target, all_type_only, span, all_client_only)
516 })
517 }
518}
519
520fn imported_name_label(name: &ImportedName) -> String {
521 match name {
522 ImportedName::Named(name) => name.clone(),
523 ImportedName::Default => "default".to_string(),
524 ImportedName::Namespace => "*".to_string(),
525 ImportedName::SideEffect => "side-effect".to_string(),
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532 use crate::resolve::{ResolveResult, ResolvedImport, ResolvedModule};
533 use fallow_types::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
534 use fallow_types::extract::{ExportName, ImportInfo, ImportedName, VisibilityTag};
535 use std::path::PathBuf;
536
537 fn build_simple_graph() -> ModuleGraph {
538 let files = vec![
539 DiscoveredFile {
540 id: FileId(0),
541 path: PathBuf::from("/project/src/entry.ts"),
542 size_bytes: 100,
543 },
544 DiscoveredFile {
545 id: FileId(1),
546 path: PathBuf::from("/project/src/utils.ts"),
547 size_bytes: 50,
548 },
549 ];
550
551 let entry_points = vec![EntryPoint {
552 path: PathBuf::from("/project/src/entry.ts"),
553 source: EntryPointSource::PackageJsonMain,
554 }];
555
556 let resolved_modules = vec![
557 ResolvedModule {
558 file_id: FileId(0),
559 path: PathBuf::from("/project/src/entry.ts"),
560 resolved_imports: vec![ResolvedImport {
561 info: ImportInfo {
562 source: "./utils".to_string(),
563 imported_name: ImportedName::Named("foo".to_string()),
564 local_name: "foo".to_string(),
565 is_type_only: false,
566 from_style: false,
567 span: oxc_span::Span::new(0, 10),
568 source_span: oxc_span::Span::default(),
569 },
570 target: ResolveResult::InternalModule(FileId(1)),
571 }],
572 ..Default::default()
573 },
574 ResolvedModule {
575 file_id: FileId(1),
576 path: PathBuf::from("/project/src/utils.ts"),
577 exports: vec![
578 fallow_types::extract::ExportInfo {
579 name: ExportName::Named("foo".to_string()),
580 local_name: Some("foo".to_string()),
581 is_type_only: false,
582 visibility: VisibilityTag::None,
583 expected_unused_reason: None,
584 span: oxc_span::Span::new(0, 20),
585 members: vec![],
586 is_side_effect_used: false,
587 super_class: None,
588 },
589 fallow_types::extract::ExportInfo {
590 name: ExportName::Named("bar".to_string()),
591 local_name: Some("bar".to_string()),
592 is_type_only: false,
593 visibility: VisibilityTag::None,
594 expected_unused_reason: None,
595 span: oxc_span::Span::new(25, 45),
596 members: vec![],
597 is_side_effect_used: false,
598 super_class: None,
599 },
600 ],
601 ..Default::default()
602 },
603 ];
604
605 ModuleGraph::build(&resolved_modules, &entry_points, &files)
606 }
607
608 #[test]
609 fn graph_module_count() {
610 let graph = build_simple_graph();
611 assert_eq!(graph.module_count(), 2);
612 }
613
614 #[test]
615 fn graph_edge_count() {
616 let graph = build_simple_graph();
617 assert_eq!(graph.edge_count(), 1);
618 }
619
620 #[test]
621 fn graph_entry_point_is_reachable() {
622 let graph = build_simple_graph();
623 assert!(graph.modules[0].is_entry_point());
624 assert!(graph.modules[0].is_reachable());
625 }
626
627 #[test]
628 fn graph_imported_module_is_reachable() {
629 let graph = build_simple_graph();
630 assert!(!graph.modules[1].is_entry_point());
631 assert!(graph.modules[1].is_reachable());
632 }
633
634 #[test]
635 #[expect(
636 clippy::too_many_lines,
637 reason = "this test fixture exercises four reachability roles end-to-end; splitting it \
638 would obscure the cross-role assertions"
639 )]
640 fn graph_distinguishes_runtime_test_and_support_reachability() {
641 let files = vec![
642 DiscoveredFile {
643 id: FileId(0),
644 path: PathBuf::from("/project/src/main.ts"),
645 size_bytes: 100,
646 },
647 DiscoveredFile {
648 id: FileId(1),
649 path: PathBuf::from("/project/src/runtime-only.ts"),
650 size_bytes: 50,
651 },
652 DiscoveredFile {
653 id: FileId(2),
654 path: PathBuf::from("/project/tests/app.test.ts"),
655 size_bytes: 50,
656 },
657 DiscoveredFile {
658 id: FileId(3),
659 path: PathBuf::from("/project/tests/setup.ts"),
660 size_bytes: 50,
661 },
662 DiscoveredFile {
663 id: FileId(4),
664 path: PathBuf::from("/project/src/covered.ts"),
665 size_bytes: 50,
666 },
667 ];
668
669 let all_entry_points = vec![
670 EntryPoint {
671 path: PathBuf::from("/project/src/main.ts"),
672 source: EntryPointSource::PackageJsonMain,
673 },
674 EntryPoint {
675 path: PathBuf::from("/project/tests/app.test.ts"),
676 source: EntryPointSource::TestFile,
677 },
678 EntryPoint {
679 path: PathBuf::from("/project/tests/setup.ts"),
680 source: EntryPointSource::Plugin {
681 name: "vitest".to_string(),
682 },
683 },
684 ];
685 let runtime_entry_points = vec![EntryPoint {
686 path: PathBuf::from("/project/src/main.ts"),
687 source: EntryPointSource::PackageJsonMain,
688 }];
689 let test_entry_points = vec![EntryPoint {
690 path: PathBuf::from("/project/tests/app.test.ts"),
691 source: EntryPointSource::TestFile,
692 }];
693
694 let resolved_modules = vec![
695 ResolvedModule {
696 file_id: FileId(0),
697 path: PathBuf::from("/project/src/main.ts"),
698 resolved_imports: vec![ResolvedImport {
699 info: ImportInfo {
700 source: "./runtime-only".to_string(),
701 imported_name: ImportedName::Named("runtimeOnly".to_string()),
702 local_name: "runtimeOnly".to_string(),
703 is_type_only: false,
704 from_style: false,
705 span: oxc_span::Span::new(0, 10),
706 source_span: oxc_span::Span::default(),
707 },
708 target: ResolveResult::InternalModule(FileId(1)),
709 }],
710 ..Default::default()
711 },
712 ResolvedModule {
713 file_id: FileId(1),
714 path: PathBuf::from("/project/src/runtime-only.ts"),
715 exports: vec![fallow_types::extract::ExportInfo {
716 name: ExportName::Named("runtimeOnly".to_string()),
717 local_name: Some("runtimeOnly".to_string()),
718 is_type_only: false,
719 visibility: VisibilityTag::None,
720 expected_unused_reason: None,
721 span: oxc_span::Span::new(0, 20),
722 members: vec![],
723 is_side_effect_used: false,
724 super_class: None,
725 }],
726 ..Default::default()
727 },
728 ResolvedModule {
729 file_id: FileId(2),
730 path: PathBuf::from("/project/tests/app.test.ts"),
731 resolved_imports: vec![ResolvedImport {
732 info: ImportInfo {
733 source: "../src/covered".to_string(),
734 imported_name: ImportedName::Named("covered".to_string()),
735 local_name: "covered".to_string(),
736 is_type_only: false,
737 from_style: false,
738 span: oxc_span::Span::new(0, 10),
739 source_span: oxc_span::Span::default(),
740 },
741 target: ResolveResult::InternalModule(FileId(4)),
742 }],
743 ..Default::default()
744 },
745 ResolvedModule {
746 file_id: FileId(3),
747 path: PathBuf::from("/project/tests/setup.ts"),
748 resolved_imports: vec![ResolvedImport {
749 info: ImportInfo {
750 source: "../src/runtime-only".to_string(),
751 imported_name: ImportedName::Named("runtimeOnly".to_string()),
752 local_name: "runtimeOnly".to_string(),
753 is_type_only: false,
754 from_style: false,
755 span: oxc_span::Span::new(0, 10),
756 source_span: oxc_span::Span::default(),
757 },
758 target: ResolveResult::InternalModule(FileId(1)),
759 }],
760 ..Default::default()
761 },
762 ResolvedModule {
763 file_id: FileId(4),
764 path: PathBuf::from("/project/src/covered.ts"),
765 exports: vec![fallow_types::extract::ExportInfo {
766 name: ExportName::Named("covered".to_string()),
767 local_name: Some("covered".to_string()),
768 is_type_only: false,
769 visibility: VisibilityTag::None,
770 expected_unused_reason: None,
771 span: oxc_span::Span::new(0, 20),
772 members: vec![],
773 is_side_effect_used: false,
774 super_class: None,
775 }],
776 ..Default::default()
777 },
778 ];
779
780 let graph = ModuleGraph::build_with_reachability_roots(
781 &resolved_modules,
782 &all_entry_points,
783 &runtime_entry_points,
784 &test_entry_points,
785 &files,
786 );
787
788 assert!(graph.modules[1].is_reachable());
789 assert!(graph.modules[1].is_runtime_reachable());
790 assert!(
791 !graph.modules[1].is_test_reachable(),
792 "support roots should not make runtime-only modules test reachable"
793 );
794
795 assert!(graph.modules[4].is_reachable());
796 assert!(graph.modules[4].is_test_reachable());
797 assert!(
798 !graph.modules[4].is_runtime_reachable(),
799 "test-only reachability should stay separate from runtime roots"
800 );
801 }
802
803 #[test]
804 fn graph_export_has_reference() {
805 let graph = build_simple_graph();
806 let utils = &graph.modules[1];
807 let foo_export = utils
808 .exports
809 .iter()
810 .find(|e| e.name.to_string() == "foo")
811 .unwrap();
812 assert!(
813 !foo_export.references.is_empty(),
814 "foo should have references"
815 );
816 }
817
818 #[test]
819 fn graph_unused_export_no_reference() {
820 let graph = build_simple_graph();
821 let utils = &graph.modules[1];
822 let bar_export = utils
823 .exports
824 .iter()
825 .find(|e| e.name.to_string() == "bar")
826 .unwrap();
827 assert!(
828 bar_export.references.is_empty(),
829 "bar should have no references"
830 );
831 }
832
833 #[test]
834 fn graph_no_namespace_import() {
835 let graph = build_simple_graph();
836 assert!(!graph.has_namespace_import(FileId(0)));
837 assert!(!graph.has_namespace_import(FileId(1)));
838 }
839
840 #[test]
841 fn graph_has_namespace_import() {
842 let files = vec![
843 DiscoveredFile {
844 id: FileId(0),
845 path: PathBuf::from("/project/entry.ts"),
846 size_bytes: 100,
847 },
848 DiscoveredFile {
849 id: FileId(1),
850 path: PathBuf::from("/project/utils.ts"),
851 size_bytes: 50,
852 },
853 ];
854
855 let entry_points = vec![EntryPoint {
856 path: PathBuf::from("/project/entry.ts"),
857 source: EntryPointSource::PackageJsonMain,
858 }];
859
860 let resolved_modules = vec![
861 ResolvedModule {
862 file_id: FileId(0),
863 path: PathBuf::from("/project/entry.ts"),
864 resolved_imports: vec![ResolvedImport {
865 info: ImportInfo {
866 source: "./utils".to_string(),
867 imported_name: ImportedName::Namespace,
868 local_name: "utils".to_string(),
869 is_type_only: false,
870 from_style: false,
871 span: oxc_span::Span::new(0, 10),
872 source_span: oxc_span::Span::default(),
873 },
874 target: ResolveResult::InternalModule(FileId(1)),
875 }],
876 ..Default::default()
877 },
878 ResolvedModule {
879 file_id: FileId(1),
880 path: PathBuf::from("/project/utils.ts"),
881 exports: vec![fallow_types::extract::ExportInfo {
882 name: ExportName::Named("foo".to_string()),
883 local_name: Some("foo".to_string()),
884 is_type_only: false,
885 visibility: VisibilityTag::None,
886 expected_unused_reason: None,
887 span: oxc_span::Span::new(0, 20),
888 members: vec![],
889 is_side_effect_used: false,
890 super_class: None,
891 }],
892 ..Default::default()
893 },
894 ];
895
896 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
897 assert!(
898 graph.has_namespace_import(FileId(1)),
899 "utils should have namespace import"
900 );
901 }
902
903 #[test]
904 fn graph_has_namespace_import_out_of_bounds() {
905 let graph = build_simple_graph();
906 assert!(!graph.has_namespace_import(FileId(999)));
907 }
908
909 #[test]
914 fn reconstruct_namespace_imported_matches_fresh_build() {
915 let files = vec![
916 DiscoveredFile {
917 id: FileId(0),
918 path: PathBuf::from("/project/entry.ts"),
919 size_bytes: 100,
920 },
921 DiscoveredFile {
922 id: FileId(1),
923 path: PathBuf::from("/project/utils.ts"),
924 size_bytes: 50,
925 },
926 DiscoveredFile {
927 id: FileId(2),
928 path: PathBuf::from("/project/named-only.ts"),
929 size_bytes: 50,
930 },
931 ];
932 let entry_points = vec![EntryPoint {
933 path: PathBuf::from("/project/entry.ts"),
934 source: EntryPointSource::PackageJsonMain,
935 }];
936 let resolved_modules = vec![
937 ResolvedModule {
938 file_id: FileId(0),
939 path: PathBuf::from("/project/entry.ts"),
940 resolved_imports: vec![
941 ResolvedImport {
942 info: ImportInfo {
943 source: "./utils".to_string(),
944 imported_name: ImportedName::Namespace,
945 local_name: "utils".to_string(),
946 is_type_only: false,
947 from_style: false,
948 span: oxc_span::Span::new(0, 10),
949 source_span: oxc_span::Span::default(),
950 },
951 target: ResolveResult::InternalModule(FileId(1)),
952 },
953 ResolvedImport {
954 info: ImportInfo {
955 source: "./named-only".to_string(),
956 imported_name: ImportedName::Named("foo".to_string()),
957 local_name: "foo".to_string(),
958 is_type_only: false,
959 from_style: false,
960 span: oxc_span::Span::new(11, 20),
961 source_span: oxc_span::Span::default(),
962 },
963 target: ResolveResult::InternalModule(FileId(2)),
964 },
965 ],
966 ..Default::default()
967 },
968 ResolvedModule {
969 file_id: FileId(1),
970 path: PathBuf::from("/project/utils.ts"),
971 ..Default::default()
972 },
973 ResolvedModule {
974 file_id: FileId(2),
975 path: PathBuf::from("/project/named-only.ts"),
976 exports: vec![fallow_types::extract::ExportInfo {
977 name: ExportName::Named("foo".to_string()),
978 local_name: Some("foo".to_string()),
979 is_type_only: false,
980 visibility: VisibilityTag::None,
981 expected_unused_reason: None,
982 span: oxc_span::Span::new(0, 20),
983 members: vec![],
984 is_side_effect_used: false,
985 super_class: None,
986 }],
987 ..Default::default()
988 },
989 ];
990
991 let mut graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
992 let fresh = graph.namespace_imported.clone();
993
994 assert!(graph.has_namespace_import(FileId(1)));
996 assert!(!graph.has_namespace_import(FileId(2)));
997
998 graph.namespace_imported = FixedBitSet::default();
1001 graph.reconstruct_namespace_imported();
1002
1003 assert_eq!(
1004 graph.namespace_imported, fresh,
1005 "reconstructed namespace_imported must equal the fresh-built bitset"
1006 );
1007 assert!(graph.has_namespace_import(FileId(1)));
1008 assert!(!graph.has_namespace_import(FileId(2)));
1009 }
1010
1011 #[test]
1012 fn graph_unreachable_module() {
1013 let files = vec![
1014 DiscoveredFile {
1015 id: FileId(0),
1016 path: PathBuf::from("/project/entry.ts"),
1017 size_bytes: 100,
1018 },
1019 DiscoveredFile {
1020 id: FileId(1),
1021 path: PathBuf::from("/project/utils.ts"),
1022 size_bytes: 50,
1023 },
1024 DiscoveredFile {
1025 id: FileId(2),
1026 path: PathBuf::from("/project/orphan.ts"),
1027 size_bytes: 30,
1028 },
1029 ];
1030
1031 let entry_points = vec![EntryPoint {
1032 path: PathBuf::from("/project/entry.ts"),
1033 source: EntryPointSource::PackageJsonMain,
1034 }];
1035
1036 let resolved_modules = vec![
1037 ResolvedModule {
1038 file_id: FileId(0),
1039 path: PathBuf::from("/project/entry.ts"),
1040 resolved_imports: vec![ResolvedImport {
1041 info: ImportInfo {
1042 source: "./utils".to_string(),
1043 imported_name: ImportedName::Named("foo".to_string()),
1044 local_name: "foo".to_string(),
1045 is_type_only: false,
1046 from_style: false,
1047 span: oxc_span::Span::new(0, 10),
1048 source_span: oxc_span::Span::default(),
1049 },
1050 target: ResolveResult::InternalModule(FileId(1)),
1051 }],
1052 ..Default::default()
1053 },
1054 ResolvedModule {
1055 file_id: FileId(1),
1056 path: PathBuf::from("/project/utils.ts"),
1057 exports: vec![fallow_types::extract::ExportInfo {
1058 name: ExportName::Named("foo".to_string()),
1059 local_name: Some("foo".to_string()),
1060 is_type_only: false,
1061 visibility: VisibilityTag::None,
1062 expected_unused_reason: None,
1063 span: oxc_span::Span::new(0, 20),
1064 members: vec![],
1065 is_side_effect_used: false,
1066 super_class: None,
1067 }],
1068 ..Default::default()
1069 },
1070 ResolvedModule {
1071 file_id: FileId(2),
1072 path: PathBuf::from("/project/orphan.ts"),
1073 exports: vec![fallow_types::extract::ExportInfo {
1074 name: ExportName::Named("orphan".to_string()),
1075 local_name: Some("orphan".to_string()),
1076 is_type_only: false,
1077 visibility: VisibilityTag::None,
1078 expected_unused_reason: None,
1079 span: oxc_span::Span::new(0, 20),
1080 members: vec![],
1081 is_side_effect_used: false,
1082 super_class: None,
1083 }],
1084 ..Default::default()
1085 },
1086 ];
1087
1088 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1089
1090 assert!(graph.modules[0].is_reachable(), "entry should be reachable");
1091 assert!(graph.modules[1].is_reachable(), "utils should be reachable");
1092 assert!(
1093 !graph.modules[2].is_reachable(),
1094 "orphan should NOT be reachable"
1095 );
1096 }
1097
1098 #[test]
1099 fn graph_package_usage_tracked() {
1100 let files = vec![DiscoveredFile {
1101 id: FileId(0),
1102 path: PathBuf::from("/project/entry.ts"),
1103 size_bytes: 100,
1104 }];
1105
1106 let entry_points = vec![EntryPoint {
1107 path: PathBuf::from("/project/entry.ts"),
1108 source: EntryPointSource::PackageJsonMain,
1109 }];
1110
1111 let resolved_modules = vec![ResolvedModule {
1112 file_id: FileId(0),
1113 path: PathBuf::from("/project/entry.ts"),
1114 exports: vec![],
1115 re_exports: vec![],
1116 resolved_imports: vec![
1117 ResolvedImport {
1118 info: ImportInfo {
1119 source: "react".to_string(),
1120 imported_name: ImportedName::Default,
1121 local_name: "React".to_string(),
1122 is_type_only: false,
1123 from_style: false,
1124 span: oxc_span::Span::new(0, 10),
1125 source_span: oxc_span::Span::default(),
1126 },
1127 target: ResolveResult::NpmPackage("react".to_string()),
1128 },
1129 ResolvedImport {
1130 info: ImportInfo {
1131 source: "lodash".to_string(),
1132 imported_name: ImportedName::Named("merge".to_string()),
1133 local_name: "merge".to_string(),
1134 is_type_only: false,
1135 from_style: false,
1136 span: oxc_span::Span::new(15, 30),
1137 source_span: oxc_span::Span::default(),
1138 },
1139 target: ResolveResult::NpmPackage("lodash".to_string()),
1140 },
1141 ],
1142 ..Default::default()
1143 }];
1144
1145 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1146 assert!(graph.package_usage.contains_key("react"));
1147 assert!(graph.package_usage.contains_key("lodash"));
1148 assert!(!graph.package_usage.contains_key("express"));
1149 }
1150
1151 #[test]
1152 fn graph_empty() {
1153 let graph = ModuleGraph::build(&[], &[], &[]);
1154 assert_eq!(graph.module_count(), 0);
1155 assert_eq!(graph.edge_count(), 0);
1156 }
1157
1158 #[test]
1164 fn graph_postcard_round_trip_is_lossless() {
1165 let graph = build_simple_graph();
1166
1167 let encoded = postcard::to_allocvec(&graph).expect("encode graph");
1168 let mut decoded: ModuleGraph = postcard::from_bytes(&encoded).expect("decode graph");
1169 decoded.reconstruct_namespace_imported();
1171
1172 assert_eq!(decoded.module_count(), graph.module_count());
1173 assert_eq!(decoded.edge_count(), graph.edge_count());
1174 assert_eq!(decoded.namespace_imported, graph.namespace_imported);
1175
1176 let utils = &decoded.modules[1];
1178 let foo = utils
1179 .exports
1180 .iter()
1181 .find(|e| e.name.to_string() == "foo")
1182 .expect("foo export survives round-trip");
1183 assert!(!foo.references.is_empty());
1184 let bar = utils
1185 .exports
1186 .iter()
1187 .find(|e| e.name.to_string() == "bar")
1188 .expect("bar export survives round-trip");
1189 assert!(bar.references.is_empty());
1190
1191 assert!(decoded.modules[0].is_entry_point());
1193 assert!(decoded.modules[0].is_reachable());
1194 assert!(decoded.modules[1].is_reachable());
1195 assert_eq!(decoded.entry_points, graph.entry_points);
1196 }
1197
1198 #[test]
1199 fn graph_cjs_exports_tracked() {
1200 let files = vec![DiscoveredFile {
1201 id: FileId(0),
1202 path: PathBuf::from("/project/entry.ts"),
1203 size_bytes: 100,
1204 }];
1205
1206 let entry_points = vec![EntryPoint {
1207 path: PathBuf::from("/project/entry.ts"),
1208 source: EntryPointSource::PackageJsonMain,
1209 }];
1210
1211 let resolved_modules = vec![ResolvedModule {
1212 file_id: FileId(0),
1213 path: PathBuf::from("/project/entry.ts"),
1214 has_cjs_exports: true,
1215 has_angular_component_template_url: false,
1216 ..Default::default()
1217 }];
1218
1219 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1220 assert!(graph.modules[0].has_cjs_exports());
1221 }
1222
1223 #[test]
1224 fn graph_edges_for_returns_targets() {
1225 let graph = build_simple_graph();
1226 let targets = graph.edges_for(FileId(0));
1227 assert_eq!(targets, vec![FileId(1)]);
1228 }
1229
1230 #[test]
1231 fn graph_edges_for_no_imports() {
1232 let graph = build_simple_graph();
1233 let targets = graph.edges_for(FileId(1));
1234 assert!(targets.is_empty());
1235 }
1236
1237 #[test]
1238 fn graph_edges_for_out_of_bounds() {
1239 let graph = build_simple_graph();
1240 let targets = graph.edges_for(FileId(999));
1241 assert!(targets.is_empty());
1242 }
1243
1244 #[test]
1245 fn graph_direct_importer_summaries_include_symbols() {
1246 let graph = build_simple_graph();
1247 let summaries = graph.direct_importer_summaries(FileId(1));
1248
1249 assert_eq!(
1250 summaries,
1251 vec![DirectImporterSummary {
1252 source: FileId(0),
1253 symbols: vec![ImportedSymbolSummary {
1254 imported: "foo".to_string(),
1255 local: "foo".to_string(),
1256 type_only: false,
1257 }],
1258 }]
1259 );
1260 }
1261
1262 #[test]
1263 fn graph_find_import_span_start_found() {
1264 let graph = build_simple_graph();
1265 let span_start = graph.find_import_span_start(FileId(0), FileId(1));
1266 assert!(span_start.is_some());
1267 assert_eq!(span_start.unwrap(), 0);
1268 }
1269
1270 #[test]
1271 fn graph_find_import_span_start_prefers_value_import_on_mixed_edge() {
1272 let files = vec![
1273 DiscoveredFile {
1274 id: FileId(0),
1275 path: PathBuf::from("/project/entry.ts"),
1276 size_bytes: 100,
1277 },
1278 DiscoveredFile {
1279 id: FileId(1),
1280 path: PathBuf::from("/project/utils.ts"),
1281 size_bytes: 50,
1282 },
1283 ];
1284 let entry_points = vec![EntryPoint {
1285 path: PathBuf::from("/project/entry.ts"),
1286 source: EntryPointSource::PackageJsonMain,
1287 }];
1288 let resolved_modules = vec![
1289 ResolvedModule {
1290 file_id: FileId(0),
1291 path: PathBuf::from("/project/entry.ts"),
1292 resolved_imports: vec![
1293 ResolvedImport {
1294 info: ImportInfo {
1295 source: "./utils".to_string(),
1296 imported_name: ImportedName::Named("Foo".to_string()),
1297 local_name: "Foo".to_string(),
1298 is_type_only: true,
1299 from_style: false,
1300 span: oxc_span::Span::new(10, 20),
1301 source_span: oxc_span::Span::default(),
1302 },
1303 target: ResolveResult::InternalModule(FileId(1)),
1304 },
1305 ResolvedImport {
1306 info: ImportInfo {
1307 source: "./utils".to_string(),
1308 imported_name: ImportedName::Named("foo".to_string()),
1309 local_name: "foo".to_string(),
1310 is_type_only: false,
1311 from_style: false,
1312 span: oxc_span::Span::new(50, 60),
1313 source_span: oxc_span::Span::default(),
1314 },
1315 target: ResolveResult::InternalModule(FileId(1)),
1316 },
1317 ],
1318 ..Default::default()
1319 },
1320 ResolvedModule {
1321 file_id: FileId(1),
1322 path: PathBuf::from("/project/utils.ts"),
1323 ..Default::default()
1324 },
1325 ];
1326
1327 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1328 assert_eq!(graph.find_import_span_start(FileId(0), FileId(1)), Some(50));
1329 }
1330
1331 #[test]
1332 fn graph_find_import_span_start_wrong_target() {
1333 let graph = build_simple_graph();
1334 let span_start = graph.find_import_span_start(FileId(0), FileId(0));
1335 assert!(span_start.is_none());
1336 }
1337
1338 #[test]
1339 fn graph_find_import_span_start_source_out_of_bounds() {
1340 let graph = build_simple_graph();
1341 let span_start = graph.find_import_span_start(FileId(999), FileId(1));
1342 assert!(span_start.is_none());
1343 }
1344
1345 #[test]
1346 fn graph_find_import_span_start_no_edges() {
1347 let graph = build_simple_graph();
1348 let span_start = graph.find_import_span_start(FileId(1), FileId(0));
1349 assert!(span_start.is_none());
1350 }
1351
1352 #[test]
1353 fn graph_reverse_deps_populated() {
1354 let graph = build_simple_graph();
1355 assert!(graph.reverse_deps[1].contains(&FileId(0)));
1356 assert!(graph.reverse_deps[0].is_empty());
1357 }
1358
1359 #[test]
1360 fn graph_type_only_package_usage_tracked() {
1361 let files = vec![DiscoveredFile {
1362 id: FileId(0),
1363 path: PathBuf::from("/project/entry.ts"),
1364 size_bytes: 100,
1365 }];
1366 let entry_points = vec![EntryPoint {
1367 path: PathBuf::from("/project/entry.ts"),
1368 source: EntryPointSource::PackageJsonMain,
1369 }];
1370 let resolved_modules = vec![ResolvedModule {
1371 file_id: FileId(0),
1372 path: PathBuf::from("/project/entry.ts"),
1373 resolved_imports: vec![
1374 ResolvedImport {
1375 info: ImportInfo {
1376 source: "react".to_string(),
1377 imported_name: ImportedName::Named("FC".to_string()),
1378 local_name: "FC".to_string(),
1379 is_type_only: true,
1380 from_style: false,
1381 span: oxc_span::Span::new(0, 10),
1382 source_span: oxc_span::Span::default(),
1383 },
1384 target: ResolveResult::NpmPackage("react".to_string()),
1385 },
1386 ResolvedImport {
1387 info: ImportInfo {
1388 source: "react".to_string(),
1389 imported_name: ImportedName::Named("useState".to_string()),
1390 local_name: "useState".to_string(),
1391 is_type_only: false,
1392 from_style: false,
1393 span: oxc_span::Span::new(15, 30),
1394 source_span: oxc_span::Span::default(),
1395 },
1396 target: ResolveResult::NpmPackage("react".to_string()),
1397 },
1398 ],
1399 ..Default::default()
1400 }];
1401
1402 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1403 assert!(graph.package_usage.contains_key("react"));
1404 assert!(graph.type_only_package_usage.contains_key("react"));
1405 }
1406
1407 #[test]
1408 fn graph_default_import_reference() {
1409 let files = vec![
1410 DiscoveredFile {
1411 id: FileId(0),
1412 path: PathBuf::from("/project/entry.ts"),
1413 size_bytes: 100,
1414 },
1415 DiscoveredFile {
1416 id: FileId(1),
1417 path: PathBuf::from("/project/utils.ts"),
1418 size_bytes: 50,
1419 },
1420 ];
1421 let entry_points = vec![EntryPoint {
1422 path: PathBuf::from("/project/entry.ts"),
1423 source: EntryPointSource::PackageJsonMain,
1424 }];
1425 let resolved_modules = vec![
1426 ResolvedModule {
1427 file_id: FileId(0),
1428 path: PathBuf::from("/project/entry.ts"),
1429 resolved_imports: vec![ResolvedImport {
1430 info: ImportInfo {
1431 source: "./utils".to_string(),
1432 imported_name: ImportedName::Default,
1433 local_name: "Utils".to_string(),
1434 is_type_only: false,
1435 from_style: false,
1436 span: oxc_span::Span::new(0, 10),
1437 source_span: oxc_span::Span::default(),
1438 },
1439 target: ResolveResult::InternalModule(FileId(1)),
1440 }],
1441 ..Default::default()
1442 },
1443 ResolvedModule {
1444 file_id: FileId(1),
1445 path: PathBuf::from("/project/utils.ts"),
1446 exports: vec![fallow_types::extract::ExportInfo {
1447 name: ExportName::Default,
1448 local_name: None,
1449 is_type_only: false,
1450 visibility: VisibilityTag::None,
1451 expected_unused_reason: None,
1452 span: oxc_span::Span::new(0, 20),
1453 members: vec![],
1454 is_side_effect_used: false,
1455 super_class: None,
1456 }],
1457 ..Default::default()
1458 },
1459 ];
1460
1461 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1462 let utils = &graph.modules[1];
1463 let default_export = utils
1464 .exports
1465 .iter()
1466 .find(|e| matches!(e.name, ExportName::Default))
1467 .unwrap();
1468 assert!(!default_export.references.is_empty());
1469 assert_eq!(
1470 default_export.references[0].kind,
1471 ReferenceKind::DefaultImport
1472 );
1473 }
1474
1475 #[test]
1476 fn graph_side_effect_import_no_export_reference() {
1477 let files = vec![
1478 DiscoveredFile {
1479 id: FileId(0),
1480 path: PathBuf::from("/project/entry.ts"),
1481 size_bytes: 100,
1482 },
1483 DiscoveredFile {
1484 id: FileId(1),
1485 path: PathBuf::from("/project/styles.ts"),
1486 size_bytes: 50,
1487 },
1488 ];
1489 let entry_points = vec![EntryPoint {
1490 path: PathBuf::from("/project/entry.ts"),
1491 source: EntryPointSource::PackageJsonMain,
1492 }];
1493 let resolved_modules = vec![
1494 ResolvedModule {
1495 file_id: FileId(0),
1496 path: PathBuf::from("/project/entry.ts"),
1497 resolved_imports: vec![ResolvedImport {
1498 info: ImportInfo {
1499 source: "./styles".to_string(),
1500 imported_name: ImportedName::SideEffect,
1501 local_name: String::new(),
1502 is_type_only: false,
1503 from_style: false,
1504 span: oxc_span::Span::new(0, 10),
1505 source_span: oxc_span::Span::default(),
1506 },
1507 target: ResolveResult::InternalModule(FileId(1)),
1508 }],
1509 ..Default::default()
1510 },
1511 ResolvedModule {
1512 file_id: FileId(1),
1513 path: PathBuf::from("/project/styles.ts"),
1514 exports: vec![fallow_types::extract::ExportInfo {
1515 name: ExportName::Named("primaryColor".to_string()),
1516 local_name: Some("primaryColor".to_string()),
1517 is_type_only: false,
1518 visibility: VisibilityTag::None,
1519 expected_unused_reason: None,
1520 span: oxc_span::Span::new(0, 20),
1521 members: vec![],
1522 is_side_effect_used: false,
1523 super_class: None,
1524 }],
1525 ..Default::default()
1526 },
1527 ];
1528
1529 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1530 assert_eq!(graph.edge_count(), 1);
1531 let styles = &graph.modules[1];
1532 let export = &styles.exports[0];
1533 assert!(
1534 export.references.is_empty(),
1535 "side-effect import should not reference named exports"
1536 );
1537 }
1538
1539 #[test]
1540 fn graph_multiple_entry_points() {
1541 let files = vec![
1542 DiscoveredFile {
1543 id: FileId(0),
1544 path: PathBuf::from("/project/main.ts"),
1545 size_bytes: 100,
1546 },
1547 DiscoveredFile {
1548 id: FileId(1),
1549 path: PathBuf::from("/project/worker.ts"),
1550 size_bytes: 100,
1551 },
1552 DiscoveredFile {
1553 id: FileId(2),
1554 path: PathBuf::from("/project/shared.ts"),
1555 size_bytes: 50,
1556 },
1557 ];
1558 let entry_points = vec![
1559 EntryPoint {
1560 path: PathBuf::from("/project/main.ts"),
1561 source: EntryPointSource::PackageJsonMain,
1562 },
1563 EntryPoint {
1564 path: PathBuf::from("/project/worker.ts"),
1565 source: EntryPointSource::PackageJsonMain,
1566 },
1567 ];
1568 let resolved_modules = vec![
1569 ResolvedModule {
1570 file_id: FileId(0),
1571 path: PathBuf::from("/project/main.ts"),
1572 resolved_imports: vec![ResolvedImport {
1573 info: ImportInfo {
1574 source: "./shared".to_string(),
1575 imported_name: ImportedName::Named("helper".to_string()),
1576 local_name: "helper".to_string(),
1577 is_type_only: false,
1578 from_style: false,
1579 span: oxc_span::Span::new(0, 10),
1580 source_span: oxc_span::Span::default(),
1581 },
1582 target: ResolveResult::InternalModule(FileId(2)),
1583 }],
1584 ..Default::default()
1585 },
1586 ResolvedModule {
1587 file_id: FileId(1),
1588 path: PathBuf::from("/project/worker.ts"),
1589 ..Default::default()
1590 },
1591 ResolvedModule {
1592 file_id: FileId(2),
1593 path: PathBuf::from("/project/shared.ts"),
1594 exports: vec![fallow_types::extract::ExportInfo {
1595 name: ExportName::Named("helper".to_string()),
1596 local_name: Some("helper".to_string()),
1597 is_type_only: false,
1598 visibility: VisibilityTag::None,
1599 expected_unused_reason: None,
1600 span: oxc_span::Span::new(0, 20),
1601 members: vec![],
1602 is_side_effect_used: false,
1603 super_class: None,
1604 }],
1605 ..Default::default()
1606 },
1607 ];
1608
1609 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1610 assert!(graph.modules[0].is_entry_point());
1611 assert!(graph.modules[1].is_entry_point());
1612 assert!(!graph.modules[2].is_entry_point());
1613 assert!(graph.modules[0].is_reachable());
1614 assert!(graph.modules[1].is_reachable());
1615 assert!(graph.modules[2].is_reachable());
1616 }
1617}