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