1mod build;
7mod cycles;
8mod narrowing;
9mod re_exports;
10mod reachability;
11pub mod types;
12
13use std::path::Path;
14
15use fixedbitset::FixedBitSet;
16use rustc_hash::{FxHashMap, FxHashSet};
17
18use crate::resolve::ResolvedModule;
19use fallow_types::discover::{DiscoveredFile, EntryPoint, FileId};
20use fallow_types::extract::ImportedName;
21
22pub use types::{ExportSymbol, ModuleNode, ReExportEdge, ReferenceKind, SymbolReference};
24
25#[derive(Debug)]
27pub struct ModuleGraph {
28 pub modules: Vec<ModuleNode>,
30 edges: Vec<Edge>,
32 pub package_usage: FxHashMap<String, Vec<FileId>>,
34 pub type_only_package_usage: FxHashMap<String, Vec<FileId>>,
38 pub entry_points: FxHashSet<FileId>,
40 pub runtime_entry_points: FxHashSet<FileId>,
42 pub test_entry_points: FxHashSet<FileId>,
44 pub reverse_deps: Vec<Vec<FileId>>,
46 namespace_imported: FixedBitSet,
48}
49
50#[derive(Debug)]
52pub(super) struct Edge {
53 pub(super) source: FileId,
54 pub(super) target: FileId,
55 pub(super) symbols: Vec<ImportedSymbol>,
56}
57
58#[derive(Debug)]
60pub(super) struct ImportedSymbol {
61 pub(super) imported_name: ImportedName,
62 pub(super) local_name: String,
63 pub(super) import_span: oxc_span::Span,
65 pub(super) is_type_only: bool,
68}
69
70#[cfg(target_pointer_width = "64")]
74const _: () = assert!(std::mem::size_of::<Edge>() == 32);
75#[cfg(target_pointer_width = "64")]
76const _: () = assert!(std::mem::size_of::<ImportedSymbol>() == 64);
77
78impl ModuleGraph {
79 fn resolve_entry_point_ids(
80 entry_points: &[EntryPoint],
81 path_to_id: &FxHashMap<&Path, FileId>,
82 ) -> FxHashSet<FileId> {
83 entry_points
84 .iter()
85 .filter_map(|ep| {
86 path_to_id.get(ep.path.as_path()).copied().or_else(|| {
87 dunce::canonicalize(&ep.path)
88 .ok()
89 .and_then(|path| path_to_id.get(path.as_path()).copied())
90 })
91 })
92 .collect()
93 }
94
95 pub fn build(
97 resolved_modules: &[ResolvedModule],
98 entry_points: &[EntryPoint],
99 files: &[DiscoveredFile],
100 ) -> Self {
101 Self::build_with_reachability_roots(
102 resolved_modules,
103 entry_points,
104 entry_points,
105 &[],
106 files,
107 )
108 }
109
110 pub fn build_with_reachability_roots(
112 resolved_modules: &[ResolvedModule],
113 entry_points: &[EntryPoint],
114 runtime_entry_points: &[EntryPoint],
115 test_entry_points: &[EntryPoint],
116 files: &[DiscoveredFile],
117 ) -> Self {
118 let _span = tracing::info_span!("build_graph").entered();
119
120 let module_count = files.len();
121
122 let max_file_id = files
125 .iter()
126 .map(|f| f.id.0 as usize)
127 .max()
128 .map_or(0, |m| m + 1);
129 let total_capacity = max_file_id.max(module_count);
130
131 let path_to_id: FxHashMap<&Path, FileId> =
133 files.iter().map(|f| (f.path.as_path(), f.id)).collect();
134
135 let module_by_id: FxHashMap<FileId, &ResolvedModule> =
137 resolved_modules.iter().map(|m| (m.file_id, m)).collect();
138
139 let entry_point_ids = Self::resolve_entry_point_ids(entry_points, &path_to_id);
141 let runtime_entry_point_ids =
142 Self::resolve_entry_point_ids(runtime_entry_points, &path_to_id);
143 let test_entry_point_ids = Self::resolve_entry_point_ids(test_entry_points, &path_to_id);
144
145 let mut graph = Self::populate_edges(
147 files,
148 &module_by_id,
149 &entry_point_ids,
150 &runtime_entry_point_ids,
151 &test_entry_point_ids,
152 module_count,
153 total_capacity,
154 );
155
156 graph.populate_references(&module_by_id, &entry_point_ids);
158
159 graph.mark_reachable(
161 &entry_point_ids,
162 &runtime_entry_point_ids,
163 &test_entry_point_ids,
164 total_capacity,
165 );
166
167 graph.resolve_re_export_chains();
169
170 graph
171 }
172
173 #[must_use]
175 pub const fn module_count(&self) -> usize {
176 self.modules.len()
177 }
178
179 #[must_use]
181 pub const fn edge_count(&self) -> usize {
182 self.edges.len()
183 }
184
185 #[must_use]
188 pub fn has_namespace_import(&self, file_id: FileId) -> bool {
189 let idx = file_id.0 as usize;
190 if idx >= self.namespace_imported.len() {
191 return false;
192 }
193 self.namespace_imported.contains(idx)
194 }
195
196 #[must_use]
198 pub fn edges_for(&self, file_id: FileId) -> Vec<FileId> {
199 let idx = file_id.0 as usize;
200 if idx >= self.modules.len() {
201 return Vec::new();
202 }
203 let range = &self.modules[idx].edge_range;
204 self.edges[range.clone()].iter().map(|e| e.target).collect()
205 }
206
207 #[must_use]
210 pub fn find_import_span_start(&self, source: FileId, target: FileId) -> Option<u32> {
211 let idx = source.0 as usize;
212 if idx >= self.modules.len() {
213 return None;
214 }
215 let range = &self.modules[idx].edge_range;
216 for edge in &self.edges[range.clone()] {
217 if edge.target == target {
218 return edge.symbols.first().map(|s| s.import_span.start);
219 }
220 }
221 None
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use crate::resolve::{ResolveResult, ResolvedImport, ResolvedModule};
229 use fallow_types::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
230 use fallow_types::extract::{ExportName, ImportInfo, ImportedName};
231 use std::path::PathBuf;
232
233 fn build_simple_graph() -> ModuleGraph {
235 let files = vec![
237 DiscoveredFile {
238 id: FileId(0),
239 path: PathBuf::from("/project/src/entry.ts"),
240 size_bytes: 100,
241 },
242 DiscoveredFile {
243 id: FileId(1),
244 path: PathBuf::from("/project/src/utils.ts"),
245 size_bytes: 50,
246 },
247 ];
248
249 let entry_points = vec![EntryPoint {
250 path: PathBuf::from("/project/src/entry.ts"),
251 source: EntryPointSource::PackageJsonMain,
252 }];
253
254 let resolved_modules = vec![
255 ResolvedModule {
256 file_id: FileId(0),
257 path: PathBuf::from("/project/src/entry.ts"),
258 resolved_imports: vec![ResolvedImport {
259 info: ImportInfo {
260 source: "./utils".to_string(),
261 imported_name: ImportedName::Named("foo".to_string()),
262 local_name: "foo".to_string(),
263 is_type_only: false,
264 span: oxc_span::Span::new(0, 10),
265 source_span: oxc_span::Span::default(),
266 },
267 target: ResolveResult::InternalModule(FileId(1)),
268 }],
269 ..Default::default()
270 },
271 ResolvedModule {
272 file_id: FileId(1),
273 path: PathBuf::from("/project/src/utils.ts"),
274 exports: vec![
275 fallow_types::extract::ExportInfo {
276 name: ExportName::Named("foo".to_string()),
277 local_name: Some("foo".to_string()),
278 is_type_only: false,
279 is_public: false,
280 span: oxc_span::Span::new(0, 20),
281 members: vec![],
282 },
283 fallow_types::extract::ExportInfo {
284 name: ExportName::Named("bar".to_string()),
285 local_name: Some("bar".to_string()),
286 is_type_only: false,
287 is_public: false,
288 span: oxc_span::Span::new(25, 45),
289 members: vec![],
290 },
291 ],
292 ..Default::default()
293 },
294 ];
295
296 ModuleGraph::build(&resolved_modules, &entry_points, &files)
297 }
298
299 #[test]
300 fn graph_module_count() {
301 let graph = build_simple_graph();
302 assert_eq!(graph.module_count(), 2);
303 }
304
305 #[test]
306 fn graph_edge_count() {
307 let graph = build_simple_graph();
308 assert_eq!(graph.edge_count(), 1);
309 }
310
311 #[test]
312 fn graph_entry_point_is_reachable() {
313 let graph = build_simple_graph();
314 assert!(graph.modules[0].is_entry_point());
315 assert!(graph.modules[0].is_reachable());
316 }
317
318 #[test]
319 fn graph_imported_module_is_reachable() {
320 let graph = build_simple_graph();
321 assert!(!graph.modules[1].is_entry_point());
322 assert!(graph.modules[1].is_reachable());
323 }
324
325 #[test]
326 fn graph_distinguishes_runtime_test_and_support_reachability() {
327 let files = vec![
328 DiscoveredFile {
329 id: FileId(0),
330 path: PathBuf::from("/project/src/main.ts"),
331 size_bytes: 100,
332 },
333 DiscoveredFile {
334 id: FileId(1),
335 path: PathBuf::from("/project/src/runtime-only.ts"),
336 size_bytes: 50,
337 },
338 DiscoveredFile {
339 id: FileId(2),
340 path: PathBuf::from("/project/tests/app.test.ts"),
341 size_bytes: 50,
342 },
343 DiscoveredFile {
344 id: FileId(3),
345 path: PathBuf::from("/project/tests/setup.ts"),
346 size_bytes: 50,
347 },
348 DiscoveredFile {
349 id: FileId(4),
350 path: PathBuf::from("/project/src/covered.ts"),
351 size_bytes: 50,
352 },
353 ];
354
355 let all_entry_points = vec![
356 EntryPoint {
357 path: PathBuf::from("/project/src/main.ts"),
358 source: EntryPointSource::PackageJsonMain,
359 },
360 EntryPoint {
361 path: PathBuf::from("/project/tests/app.test.ts"),
362 source: EntryPointSource::TestFile,
363 },
364 EntryPoint {
365 path: PathBuf::from("/project/tests/setup.ts"),
366 source: EntryPointSource::Plugin {
367 name: "vitest".to_string(),
368 },
369 },
370 ];
371 let runtime_entry_points = vec![EntryPoint {
372 path: PathBuf::from("/project/src/main.ts"),
373 source: EntryPointSource::PackageJsonMain,
374 }];
375 let test_entry_points = vec![EntryPoint {
376 path: PathBuf::from("/project/tests/app.test.ts"),
377 source: EntryPointSource::TestFile,
378 }];
379
380 let resolved_modules = vec![
381 ResolvedModule {
382 file_id: FileId(0),
383 path: PathBuf::from("/project/src/main.ts"),
384 resolved_imports: vec![ResolvedImport {
385 info: ImportInfo {
386 source: "./runtime-only".to_string(),
387 imported_name: ImportedName::Named("runtimeOnly".to_string()),
388 local_name: "runtimeOnly".to_string(),
389 is_type_only: false,
390 span: oxc_span::Span::new(0, 10),
391 source_span: oxc_span::Span::default(),
392 },
393 target: ResolveResult::InternalModule(FileId(1)),
394 }],
395 ..Default::default()
396 },
397 ResolvedModule {
398 file_id: FileId(1),
399 path: PathBuf::from("/project/src/runtime-only.ts"),
400 exports: vec![fallow_types::extract::ExportInfo {
401 name: ExportName::Named("runtimeOnly".to_string()),
402 local_name: Some("runtimeOnly".to_string()),
403 is_type_only: false,
404 is_public: false,
405 span: oxc_span::Span::new(0, 20),
406 members: vec![],
407 }],
408 ..Default::default()
409 },
410 ResolvedModule {
411 file_id: FileId(2),
412 path: PathBuf::from("/project/tests/app.test.ts"),
413 resolved_imports: vec![ResolvedImport {
414 info: ImportInfo {
415 source: "../src/covered".to_string(),
416 imported_name: ImportedName::Named("covered".to_string()),
417 local_name: "covered".to_string(),
418 is_type_only: false,
419 span: oxc_span::Span::new(0, 10),
420 source_span: oxc_span::Span::default(),
421 },
422 target: ResolveResult::InternalModule(FileId(4)),
423 }],
424 ..Default::default()
425 },
426 ResolvedModule {
427 file_id: FileId(3),
428 path: PathBuf::from("/project/tests/setup.ts"),
429 resolved_imports: vec![ResolvedImport {
430 info: ImportInfo {
431 source: "../src/runtime-only".to_string(),
432 imported_name: ImportedName::Named("runtimeOnly".to_string()),
433 local_name: "runtimeOnly".to_string(),
434 is_type_only: false,
435 span: oxc_span::Span::new(0, 10),
436 source_span: oxc_span::Span::default(),
437 },
438 target: ResolveResult::InternalModule(FileId(1)),
439 }],
440 ..Default::default()
441 },
442 ResolvedModule {
443 file_id: FileId(4),
444 path: PathBuf::from("/project/src/covered.ts"),
445 exports: vec![fallow_types::extract::ExportInfo {
446 name: ExportName::Named("covered".to_string()),
447 local_name: Some("covered".to_string()),
448 is_type_only: false,
449 is_public: false,
450 span: oxc_span::Span::new(0, 20),
451 members: vec![],
452 }],
453 ..Default::default()
454 },
455 ];
456
457 let graph = ModuleGraph::build_with_reachability_roots(
458 &resolved_modules,
459 &all_entry_points,
460 &runtime_entry_points,
461 &test_entry_points,
462 &files,
463 );
464
465 assert!(graph.modules[1].is_reachable());
466 assert!(graph.modules[1].is_runtime_reachable());
467 assert!(
468 !graph.modules[1].is_test_reachable(),
469 "support roots should not make runtime-only modules test reachable"
470 );
471
472 assert!(graph.modules[4].is_reachable());
473 assert!(graph.modules[4].is_test_reachable());
474 assert!(
475 !graph.modules[4].is_runtime_reachable(),
476 "test-only reachability should stay separate from runtime roots"
477 );
478 }
479
480 #[test]
481 fn graph_export_has_reference() {
482 let graph = build_simple_graph();
483 let utils = &graph.modules[1];
484 let foo_export = utils
485 .exports
486 .iter()
487 .find(|e| e.name.to_string() == "foo")
488 .unwrap();
489 assert!(
490 !foo_export.references.is_empty(),
491 "foo should have references"
492 );
493 }
494
495 #[test]
496 fn graph_unused_export_no_reference() {
497 let graph = build_simple_graph();
498 let utils = &graph.modules[1];
499 let bar_export = utils
500 .exports
501 .iter()
502 .find(|e| e.name.to_string() == "bar")
503 .unwrap();
504 assert!(
505 bar_export.references.is_empty(),
506 "bar should have no references"
507 );
508 }
509
510 #[test]
511 fn graph_no_namespace_import() {
512 let graph = build_simple_graph();
513 assert!(!graph.has_namespace_import(FileId(0)));
514 assert!(!graph.has_namespace_import(FileId(1)));
515 }
516
517 #[test]
518 fn graph_has_namespace_import() {
519 let files = vec![
520 DiscoveredFile {
521 id: FileId(0),
522 path: PathBuf::from("/project/entry.ts"),
523 size_bytes: 100,
524 },
525 DiscoveredFile {
526 id: FileId(1),
527 path: PathBuf::from("/project/utils.ts"),
528 size_bytes: 50,
529 },
530 ];
531
532 let entry_points = vec![EntryPoint {
533 path: PathBuf::from("/project/entry.ts"),
534 source: EntryPointSource::PackageJsonMain,
535 }];
536
537 let resolved_modules = vec![
538 ResolvedModule {
539 file_id: FileId(0),
540 path: PathBuf::from("/project/entry.ts"),
541 resolved_imports: vec![ResolvedImport {
542 info: ImportInfo {
543 source: "./utils".to_string(),
544 imported_name: ImportedName::Namespace,
545 local_name: "utils".to_string(),
546 is_type_only: false,
547 span: oxc_span::Span::new(0, 10),
548 source_span: oxc_span::Span::default(),
549 },
550 target: ResolveResult::InternalModule(FileId(1)),
551 }],
552 ..Default::default()
553 },
554 ResolvedModule {
555 file_id: FileId(1),
556 path: PathBuf::from("/project/utils.ts"),
557 exports: vec![fallow_types::extract::ExportInfo {
558 name: ExportName::Named("foo".to_string()),
559 local_name: Some("foo".to_string()),
560 is_type_only: false,
561 is_public: false,
562 span: oxc_span::Span::new(0, 20),
563 members: vec![],
564 }],
565 ..Default::default()
566 },
567 ];
568
569 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
570 assert!(
571 graph.has_namespace_import(FileId(1)),
572 "utils should have namespace import"
573 );
574 }
575
576 #[test]
577 fn graph_has_namespace_import_out_of_bounds() {
578 let graph = build_simple_graph();
579 assert!(!graph.has_namespace_import(FileId(999)));
580 }
581
582 #[test]
583 fn graph_unreachable_module() {
584 let files = vec![
586 DiscoveredFile {
587 id: FileId(0),
588 path: PathBuf::from("/project/entry.ts"),
589 size_bytes: 100,
590 },
591 DiscoveredFile {
592 id: FileId(1),
593 path: PathBuf::from("/project/utils.ts"),
594 size_bytes: 50,
595 },
596 DiscoveredFile {
597 id: FileId(2),
598 path: PathBuf::from("/project/orphan.ts"),
599 size_bytes: 30,
600 },
601 ];
602
603 let entry_points = vec![EntryPoint {
604 path: PathBuf::from("/project/entry.ts"),
605 source: EntryPointSource::PackageJsonMain,
606 }];
607
608 let resolved_modules = vec![
609 ResolvedModule {
610 file_id: FileId(0),
611 path: PathBuf::from("/project/entry.ts"),
612 resolved_imports: vec![ResolvedImport {
613 info: ImportInfo {
614 source: "./utils".to_string(),
615 imported_name: ImportedName::Named("foo".to_string()),
616 local_name: "foo".to_string(),
617 is_type_only: false,
618 span: oxc_span::Span::new(0, 10),
619 source_span: oxc_span::Span::default(),
620 },
621 target: ResolveResult::InternalModule(FileId(1)),
622 }],
623 ..Default::default()
624 },
625 ResolvedModule {
626 file_id: FileId(1),
627 path: PathBuf::from("/project/utils.ts"),
628 exports: vec![fallow_types::extract::ExportInfo {
629 name: ExportName::Named("foo".to_string()),
630 local_name: Some("foo".to_string()),
631 is_type_only: false,
632 is_public: false,
633 span: oxc_span::Span::new(0, 20),
634 members: vec![],
635 }],
636 ..Default::default()
637 },
638 ResolvedModule {
639 file_id: FileId(2),
640 path: PathBuf::from("/project/orphan.ts"),
641 exports: vec![fallow_types::extract::ExportInfo {
642 name: ExportName::Named("orphan".to_string()),
643 local_name: Some("orphan".to_string()),
644 is_type_only: false,
645 is_public: false,
646 span: oxc_span::Span::new(0, 20),
647 members: vec![],
648 }],
649 ..Default::default()
650 },
651 ];
652
653 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
654
655 assert!(graph.modules[0].is_reachable(), "entry should be reachable");
656 assert!(graph.modules[1].is_reachable(), "utils should be reachable");
657 assert!(
658 !graph.modules[2].is_reachable(),
659 "orphan should NOT be reachable"
660 );
661 }
662
663 #[test]
664 fn graph_package_usage_tracked() {
665 let files = vec![DiscoveredFile {
666 id: FileId(0),
667 path: PathBuf::from("/project/entry.ts"),
668 size_bytes: 100,
669 }];
670
671 let entry_points = vec![EntryPoint {
672 path: PathBuf::from("/project/entry.ts"),
673 source: EntryPointSource::PackageJsonMain,
674 }];
675
676 let resolved_modules = vec![ResolvedModule {
677 file_id: FileId(0),
678 path: PathBuf::from("/project/entry.ts"),
679 exports: vec![],
680 re_exports: vec![],
681 resolved_imports: vec![
682 ResolvedImport {
683 info: ImportInfo {
684 source: "react".to_string(),
685 imported_name: ImportedName::Default,
686 local_name: "React".to_string(),
687 is_type_only: false,
688 span: oxc_span::Span::new(0, 10),
689 source_span: oxc_span::Span::default(),
690 },
691 target: ResolveResult::NpmPackage("react".to_string()),
692 },
693 ResolvedImport {
694 info: ImportInfo {
695 source: "lodash".to_string(),
696 imported_name: ImportedName::Named("merge".to_string()),
697 local_name: "merge".to_string(),
698 is_type_only: false,
699 span: oxc_span::Span::new(15, 30),
700 source_span: oxc_span::Span::default(),
701 },
702 target: ResolveResult::NpmPackage("lodash".to_string()),
703 },
704 ],
705 ..Default::default()
706 }];
707
708 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
709 assert!(graph.package_usage.contains_key("react"));
710 assert!(graph.package_usage.contains_key("lodash"));
711 assert!(!graph.package_usage.contains_key("express"));
712 }
713
714 #[test]
715 fn graph_empty() {
716 let graph = ModuleGraph::build(&[], &[], &[]);
717 assert_eq!(graph.module_count(), 0);
718 assert_eq!(graph.edge_count(), 0);
719 }
720
721 #[test]
722 fn graph_cjs_exports_tracked() {
723 let files = vec![DiscoveredFile {
724 id: FileId(0),
725 path: PathBuf::from("/project/entry.ts"),
726 size_bytes: 100,
727 }];
728
729 let entry_points = vec![EntryPoint {
730 path: PathBuf::from("/project/entry.ts"),
731 source: EntryPointSource::PackageJsonMain,
732 }];
733
734 let resolved_modules = vec![ResolvedModule {
735 file_id: FileId(0),
736 path: PathBuf::from("/project/entry.ts"),
737 has_cjs_exports: true,
738 ..Default::default()
739 }];
740
741 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
742 assert!(graph.modules[0].has_cjs_exports());
743 }
744
745 #[test]
746 fn graph_edges_for_returns_targets() {
747 let graph = build_simple_graph();
748 let targets = graph.edges_for(FileId(0));
749 assert_eq!(targets, vec![FileId(1)]);
750 }
751
752 #[test]
753 fn graph_edges_for_no_imports() {
754 let graph = build_simple_graph();
755 let targets = graph.edges_for(FileId(1));
757 assert!(targets.is_empty());
758 }
759
760 #[test]
761 fn graph_edges_for_out_of_bounds() {
762 let graph = build_simple_graph();
763 let targets = graph.edges_for(FileId(999));
764 assert!(targets.is_empty());
765 }
766
767 #[test]
768 fn graph_find_import_span_start_found() {
769 let graph = build_simple_graph();
770 let span_start = graph.find_import_span_start(FileId(0), FileId(1));
771 assert!(span_start.is_some());
772 assert_eq!(span_start.unwrap(), 0);
773 }
774
775 #[test]
776 fn graph_find_import_span_start_wrong_target() {
777 let graph = build_simple_graph();
778 let span_start = graph.find_import_span_start(FileId(0), FileId(0));
780 assert!(span_start.is_none());
781 }
782
783 #[test]
784 fn graph_find_import_span_start_source_out_of_bounds() {
785 let graph = build_simple_graph();
786 let span_start = graph.find_import_span_start(FileId(999), FileId(1));
787 assert!(span_start.is_none());
788 }
789
790 #[test]
791 fn graph_find_import_span_start_no_edges() {
792 let graph = build_simple_graph();
793 let span_start = graph.find_import_span_start(FileId(1), FileId(0));
795 assert!(span_start.is_none());
796 }
797
798 #[test]
799 fn graph_reverse_deps_populated() {
800 let graph = build_simple_graph();
801 assert!(graph.reverse_deps[1].contains(&FileId(0)));
803 assert!(graph.reverse_deps[0].is_empty());
805 }
806
807 #[test]
808 fn graph_type_only_package_usage_tracked() {
809 let files = vec![DiscoveredFile {
810 id: FileId(0),
811 path: PathBuf::from("/project/entry.ts"),
812 size_bytes: 100,
813 }];
814 let entry_points = vec![EntryPoint {
815 path: PathBuf::from("/project/entry.ts"),
816 source: EntryPointSource::PackageJsonMain,
817 }];
818 let resolved_modules = vec![ResolvedModule {
819 file_id: FileId(0),
820 path: PathBuf::from("/project/entry.ts"),
821 resolved_imports: vec![
822 ResolvedImport {
823 info: ImportInfo {
824 source: "react".to_string(),
825 imported_name: ImportedName::Named("FC".to_string()),
826 local_name: "FC".to_string(),
827 is_type_only: true,
828 span: oxc_span::Span::new(0, 10),
829 source_span: oxc_span::Span::default(),
830 },
831 target: ResolveResult::NpmPackage("react".to_string()),
832 },
833 ResolvedImport {
834 info: ImportInfo {
835 source: "react".to_string(),
836 imported_name: ImportedName::Named("useState".to_string()),
837 local_name: "useState".to_string(),
838 is_type_only: false,
839 span: oxc_span::Span::new(15, 30),
840 source_span: oxc_span::Span::default(),
841 },
842 target: ResolveResult::NpmPackage("react".to_string()),
843 },
844 ],
845 ..Default::default()
846 }];
847
848 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
849 assert!(graph.package_usage.contains_key("react"));
850 assert!(graph.type_only_package_usage.contains_key("react"));
851 }
852
853 #[test]
854 fn graph_default_import_reference() {
855 let files = vec![
856 DiscoveredFile {
857 id: FileId(0),
858 path: PathBuf::from("/project/entry.ts"),
859 size_bytes: 100,
860 },
861 DiscoveredFile {
862 id: FileId(1),
863 path: PathBuf::from("/project/utils.ts"),
864 size_bytes: 50,
865 },
866 ];
867 let entry_points = vec![EntryPoint {
868 path: PathBuf::from("/project/entry.ts"),
869 source: EntryPointSource::PackageJsonMain,
870 }];
871 let resolved_modules = vec![
872 ResolvedModule {
873 file_id: FileId(0),
874 path: PathBuf::from("/project/entry.ts"),
875 resolved_imports: vec![ResolvedImport {
876 info: ImportInfo {
877 source: "./utils".to_string(),
878 imported_name: ImportedName::Default,
879 local_name: "Utils".to_string(),
880 is_type_only: false,
881 span: oxc_span::Span::new(0, 10),
882 source_span: oxc_span::Span::default(),
883 },
884 target: ResolveResult::InternalModule(FileId(1)),
885 }],
886 ..Default::default()
887 },
888 ResolvedModule {
889 file_id: FileId(1),
890 path: PathBuf::from("/project/utils.ts"),
891 exports: vec![fallow_types::extract::ExportInfo {
892 name: ExportName::Default,
893 local_name: None,
894 is_type_only: false,
895 is_public: false,
896 span: oxc_span::Span::new(0, 20),
897 members: vec![],
898 }],
899 ..Default::default()
900 },
901 ];
902
903 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
904 let utils = &graph.modules[1];
905 let default_export = utils
906 .exports
907 .iter()
908 .find(|e| matches!(e.name, ExportName::Default))
909 .unwrap();
910 assert!(!default_export.references.is_empty());
911 assert_eq!(
912 default_export.references[0].kind,
913 ReferenceKind::DefaultImport
914 );
915 }
916
917 #[test]
918 fn graph_side_effect_import_no_export_reference() {
919 let files = vec![
920 DiscoveredFile {
921 id: FileId(0),
922 path: PathBuf::from("/project/entry.ts"),
923 size_bytes: 100,
924 },
925 DiscoveredFile {
926 id: FileId(1),
927 path: PathBuf::from("/project/styles.ts"),
928 size_bytes: 50,
929 },
930 ];
931 let entry_points = vec![EntryPoint {
932 path: PathBuf::from("/project/entry.ts"),
933 source: EntryPointSource::PackageJsonMain,
934 }];
935 let resolved_modules = vec![
936 ResolvedModule {
937 file_id: FileId(0),
938 path: PathBuf::from("/project/entry.ts"),
939 resolved_imports: vec![ResolvedImport {
940 info: ImportInfo {
941 source: "./styles".to_string(),
942 imported_name: ImportedName::SideEffect,
943 local_name: String::new(),
944 is_type_only: false,
945 span: oxc_span::Span::new(0, 10),
946 source_span: oxc_span::Span::default(),
947 },
948 target: ResolveResult::InternalModule(FileId(1)),
949 }],
950 ..Default::default()
951 },
952 ResolvedModule {
953 file_id: FileId(1),
954 path: PathBuf::from("/project/styles.ts"),
955 exports: vec![fallow_types::extract::ExportInfo {
956 name: ExportName::Named("primaryColor".to_string()),
957 local_name: Some("primaryColor".to_string()),
958 is_type_only: false,
959 is_public: false,
960 span: oxc_span::Span::new(0, 20),
961 members: vec![],
962 }],
963 ..Default::default()
964 },
965 ];
966
967 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
968 assert_eq!(graph.edge_count(), 1);
970 let styles = &graph.modules[1];
971 let export = &styles.exports[0];
972 assert!(
974 export.references.is_empty(),
975 "side-effect import should not reference named exports"
976 );
977 }
978
979 #[test]
980 fn graph_multiple_entry_points() {
981 let files = vec![
982 DiscoveredFile {
983 id: FileId(0),
984 path: PathBuf::from("/project/main.ts"),
985 size_bytes: 100,
986 },
987 DiscoveredFile {
988 id: FileId(1),
989 path: PathBuf::from("/project/worker.ts"),
990 size_bytes: 100,
991 },
992 DiscoveredFile {
993 id: FileId(2),
994 path: PathBuf::from("/project/shared.ts"),
995 size_bytes: 50,
996 },
997 ];
998 let entry_points = vec![
999 EntryPoint {
1000 path: PathBuf::from("/project/main.ts"),
1001 source: EntryPointSource::PackageJsonMain,
1002 },
1003 EntryPoint {
1004 path: PathBuf::from("/project/worker.ts"),
1005 source: EntryPointSource::PackageJsonMain,
1006 },
1007 ];
1008 let resolved_modules = vec![
1009 ResolvedModule {
1010 file_id: FileId(0),
1011 path: PathBuf::from("/project/main.ts"),
1012 resolved_imports: vec![ResolvedImport {
1013 info: ImportInfo {
1014 source: "./shared".to_string(),
1015 imported_name: ImportedName::Named("helper".to_string()),
1016 local_name: "helper".to_string(),
1017 is_type_only: false,
1018 span: oxc_span::Span::new(0, 10),
1019 source_span: oxc_span::Span::default(),
1020 },
1021 target: ResolveResult::InternalModule(FileId(2)),
1022 }],
1023 ..Default::default()
1024 },
1025 ResolvedModule {
1026 file_id: FileId(1),
1027 path: PathBuf::from("/project/worker.ts"),
1028 ..Default::default()
1029 },
1030 ResolvedModule {
1031 file_id: FileId(2),
1032 path: PathBuf::from("/project/shared.ts"),
1033 exports: vec![fallow_types::extract::ExportInfo {
1034 name: ExportName::Named("helper".to_string()),
1035 local_name: Some("helper".to_string()),
1036 is_type_only: false,
1037 is_public: false,
1038 span: oxc_span::Span::new(0, 20),
1039 members: vec![],
1040 }],
1041 ..Default::default()
1042 },
1043 ];
1044
1045 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1046 assert!(graph.modules[0].is_entry_point());
1047 assert!(graph.modules[1].is_entry_point());
1048 assert!(!graph.modules[2].is_entry_point());
1049 assert!(graph.modules[0].is_reachable());
1051 assert!(graph.modules[1].is_reachable());
1052 assert!(graph.modules[2].is_reachable());
1053 }
1054}