1mod build;
7mod cycles;
8mod re_exports;
9mod reachability;
10pub mod types;
11
12use std::path::PathBuf;
13
14use fixedbitset::FixedBitSet;
15use rustc_hash::{FxHashMap, FxHashSet};
16
17use crate::resolve::ResolvedModule;
18use fallow_types::discover::{DiscoveredFile, EntryPoint, FileId};
19use fallow_types::extract::ImportedName;
20
21pub use types::{ExportSymbol, ModuleNode, ReExportEdge, ReferenceKind, SymbolReference};
23
24#[derive(Debug)]
26pub struct ModuleGraph {
27 pub modules: Vec<ModuleNode>,
29 edges: Vec<Edge>,
31 pub package_usage: FxHashMap<String, Vec<FileId>>,
33 pub type_only_package_usage: FxHashMap<String, Vec<FileId>>,
37 pub entry_points: FxHashSet<FileId>,
39 pub reverse_deps: Vec<Vec<FileId>>,
41 namespace_imported: FixedBitSet,
43}
44
45#[derive(Debug)]
47pub(super) struct Edge {
48 pub(super) source: FileId,
49 pub(super) target: FileId,
50 pub(super) symbols: Vec<ImportedSymbol>,
51}
52
53#[derive(Debug)]
55pub(super) struct ImportedSymbol {
56 pub(super) imported_name: ImportedName,
57 pub(super) local_name: String,
58 pub(super) import_span: oxc_span::Span,
60}
61
62#[cfg(target_pointer_width = "64")]
66const _: () = assert!(std::mem::size_of::<Edge>() == 32);
67#[cfg(target_pointer_width = "64")]
68const _: () = assert!(std::mem::size_of::<ImportedSymbol>() == 56);
69
70impl ModuleGraph {
71 pub fn build(
73 resolved_modules: &[ResolvedModule],
74 entry_points: &[EntryPoint],
75 files: &[DiscoveredFile],
76 ) -> Self {
77 let _span = tracing::info_span!("build_graph").entered();
78
79 let module_count = files.len();
80
81 let max_file_id = files
84 .iter()
85 .map(|f| f.id.0 as usize)
86 .max()
87 .map_or(0, |m| m + 1);
88 let total_capacity = max_file_id.max(module_count);
89
90 let path_to_id: FxHashMap<PathBuf, FileId> =
92 files.iter().map(|f| (f.path.clone(), f.id)).collect();
93
94 let module_by_id: FxHashMap<FileId, &ResolvedModule> =
96 resolved_modules.iter().map(|m| (m.file_id, m)).collect();
97
98 let entry_point_ids: FxHashSet<FileId> = entry_points
100 .iter()
101 .filter_map(|ep| {
102 path_to_id.get(&ep.path).copied().or_else(|| {
104 ep.path
106 .canonicalize()
107 .ok()
108 .and_then(|c| path_to_id.get(&c).copied())
109 })
110 })
111 .collect();
112
113 let mut graph = Self::populate_edges(
115 files,
116 &module_by_id,
117 &entry_point_ids,
118 module_count,
119 total_capacity,
120 );
121
122 graph.populate_references(&module_by_id, &entry_point_ids);
124
125 graph.mark_reachable(total_capacity);
127
128 graph.resolve_re_export_chains();
130
131 graph
132 }
133
134 pub const fn module_count(&self) -> usize {
136 self.modules.len()
137 }
138
139 pub const fn edge_count(&self) -> usize {
141 self.edges.len()
142 }
143
144 pub fn has_namespace_import(&self, file_id: FileId) -> bool {
147 let idx = file_id.0 as usize;
148 if idx >= self.namespace_imported.len() {
149 return false;
150 }
151 self.namespace_imported.contains(idx)
152 }
153
154 pub fn edges_for(&self, file_id: FileId) -> Vec<FileId> {
156 let idx = file_id.0 as usize;
157 if idx >= self.modules.len() {
158 return Vec::new();
159 }
160 let range = &self.modules[idx].edge_range;
161 self.edges[range.clone()].iter().map(|e| e.target).collect()
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::resolve::{ResolveResult, ResolvedImport, ResolvedModule};
169 use fallow_types::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
170 use fallow_types::extract::{ExportName, ImportInfo, ImportedName};
171 use std::path::PathBuf;
172
173 fn build_simple_graph() -> ModuleGraph {
175 let files = vec![
177 DiscoveredFile {
178 id: FileId(0),
179 path: PathBuf::from("/project/src/entry.ts"),
180 size_bytes: 100,
181 },
182 DiscoveredFile {
183 id: FileId(1),
184 path: PathBuf::from("/project/src/utils.ts"),
185 size_bytes: 50,
186 },
187 ];
188
189 let entry_points = vec![EntryPoint {
190 path: PathBuf::from("/project/src/entry.ts"),
191 source: EntryPointSource::PackageJsonMain,
192 }];
193
194 let resolved_modules = vec![
195 ResolvedModule {
196 file_id: FileId(0),
197 path: PathBuf::from("/project/src/entry.ts"),
198 exports: vec![],
199 re_exports: vec![],
200 resolved_imports: vec![ResolvedImport {
201 info: ImportInfo {
202 source: "./utils".to_string(),
203 imported_name: ImportedName::Named("foo".to_string()),
204 local_name: "foo".to_string(),
205 is_type_only: false,
206 span: oxc_span::Span::new(0, 10),
207 },
208 target: ResolveResult::InternalModule(FileId(1)),
209 }],
210 resolved_dynamic_imports: vec![],
211 resolved_dynamic_patterns: vec![],
212 member_accesses: vec![],
213 whole_object_uses: vec![],
214 has_cjs_exports: false,
215 unused_import_bindings: vec![],
216 },
217 ResolvedModule {
218 file_id: FileId(1),
219 path: PathBuf::from("/project/src/utils.ts"),
220 exports: vec![
221 fallow_types::extract::ExportInfo {
222 name: ExportName::Named("foo".to_string()),
223 local_name: Some("foo".to_string()),
224 is_type_only: false,
225 span: oxc_span::Span::new(0, 20),
226 members: vec![],
227 },
228 fallow_types::extract::ExportInfo {
229 name: ExportName::Named("bar".to_string()),
230 local_name: Some("bar".to_string()),
231 is_type_only: false,
232 span: oxc_span::Span::new(25, 45),
233 members: vec![],
234 },
235 ],
236 re_exports: vec![],
237 resolved_imports: vec![],
238 resolved_dynamic_imports: vec![],
239 resolved_dynamic_patterns: vec![],
240 member_accesses: vec![],
241 whole_object_uses: vec![],
242 has_cjs_exports: false,
243 unused_import_bindings: vec![],
244 },
245 ];
246
247 ModuleGraph::build(&resolved_modules, &entry_points, &files)
248 }
249
250 #[test]
251 fn graph_module_count() {
252 let graph = build_simple_graph();
253 assert_eq!(graph.module_count(), 2);
254 }
255
256 #[test]
257 fn graph_edge_count() {
258 let graph = build_simple_graph();
259 assert_eq!(graph.edge_count(), 1);
260 }
261
262 #[test]
263 fn graph_entry_point_is_reachable() {
264 let graph = build_simple_graph();
265 assert!(graph.modules[0].is_entry_point);
266 assert!(graph.modules[0].is_reachable);
267 }
268
269 #[test]
270 fn graph_imported_module_is_reachable() {
271 let graph = build_simple_graph();
272 assert!(!graph.modules[1].is_entry_point);
273 assert!(graph.modules[1].is_reachable);
274 }
275
276 #[test]
277 fn graph_export_has_reference() {
278 let graph = build_simple_graph();
279 let utils = &graph.modules[1];
280 let foo_export = utils
281 .exports
282 .iter()
283 .find(|e| e.name.to_string() == "foo")
284 .unwrap();
285 assert!(
286 !foo_export.references.is_empty(),
287 "foo should have references"
288 );
289 }
290
291 #[test]
292 fn graph_unused_export_no_reference() {
293 let graph = build_simple_graph();
294 let utils = &graph.modules[1];
295 let bar_export = utils
296 .exports
297 .iter()
298 .find(|e| e.name.to_string() == "bar")
299 .unwrap();
300 assert!(
301 bar_export.references.is_empty(),
302 "bar should have no references"
303 );
304 }
305
306 #[test]
307 fn graph_no_namespace_import() {
308 let graph = build_simple_graph();
309 assert!(!graph.has_namespace_import(FileId(0)));
310 assert!(!graph.has_namespace_import(FileId(1)));
311 }
312
313 #[test]
314 fn graph_has_namespace_import() {
315 let files = vec![
316 DiscoveredFile {
317 id: FileId(0),
318 path: PathBuf::from("/project/entry.ts"),
319 size_bytes: 100,
320 },
321 DiscoveredFile {
322 id: FileId(1),
323 path: PathBuf::from("/project/utils.ts"),
324 size_bytes: 50,
325 },
326 ];
327
328 let entry_points = vec![EntryPoint {
329 path: PathBuf::from("/project/entry.ts"),
330 source: EntryPointSource::PackageJsonMain,
331 }];
332
333 let resolved_modules = vec![
334 ResolvedModule {
335 file_id: FileId(0),
336 path: PathBuf::from("/project/entry.ts"),
337 exports: vec![],
338 re_exports: vec![],
339 resolved_imports: vec![ResolvedImport {
340 info: ImportInfo {
341 source: "./utils".to_string(),
342 imported_name: ImportedName::Namespace,
343 local_name: "utils".to_string(),
344 is_type_only: false,
345 span: oxc_span::Span::new(0, 10),
346 },
347 target: ResolveResult::InternalModule(FileId(1)),
348 }],
349 resolved_dynamic_imports: vec![],
350 resolved_dynamic_patterns: vec![],
351 member_accesses: vec![],
352 whole_object_uses: vec![],
353 has_cjs_exports: false,
354 unused_import_bindings: vec![],
355 },
356 ResolvedModule {
357 file_id: FileId(1),
358 path: PathBuf::from("/project/utils.ts"),
359 exports: vec![fallow_types::extract::ExportInfo {
360 name: ExportName::Named("foo".to_string()),
361 local_name: Some("foo".to_string()),
362 is_type_only: false,
363 span: oxc_span::Span::new(0, 20),
364 members: vec![],
365 }],
366 re_exports: vec![],
367 resolved_imports: vec![],
368 resolved_dynamic_imports: vec![],
369 resolved_dynamic_patterns: vec![],
370 member_accesses: vec![],
371 whole_object_uses: vec![],
372 has_cjs_exports: false,
373 unused_import_bindings: vec![],
374 },
375 ];
376
377 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
378 assert!(
379 graph.has_namespace_import(FileId(1)),
380 "utils should have namespace import"
381 );
382 }
383
384 #[test]
385 fn graph_has_namespace_import_out_of_bounds() {
386 let graph = build_simple_graph();
387 assert!(!graph.has_namespace_import(FileId(999)));
388 }
389
390 #[test]
391 fn graph_unreachable_module() {
392 let files = vec![
394 DiscoveredFile {
395 id: FileId(0),
396 path: PathBuf::from("/project/entry.ts"),
397 size_bytes: 100,
398 },
399 DiscoveredFile {
400 id: FileId(1),
401 path: PathBuf::from("/project/utils.ts"),
402 size_bytes: 50,
403 },
404 DiscoveredFile {
405 id: FileId(2),
406 path: PathBuf::from("/project/orphan.ts"),
407 size_bytes: 30,
408 },
409 ];
410
411 let entry_points = vec![EntryPoint {
412 path: PathBuf::from("/project/entry.ts"),
413 source: EntryPointSource::PackageJsonMain,
414 }];
415
416 let resolved_modules = vec![
417 ResolvedModule {
418 file_id: FileId(0),
419 path: PathBuf::from("/project/entry.ts"),
420 exports: vec![],
421 re_exports: vec![],
422 resolved_imports: vec![ResolvedImport {
423 info: ImportInfo {
424 source: "./utils".to_string(),
425 imported_name: ImportedName::Named("foo".to_string()),
426 local_name: "foo".to_string(),
427 is_type_only: false,
428 span: oxc_span::Span::new(0, 10),
429 },
430 target: ResolveResult::InternalModule(FileId(1)),
431 }],
432 resolved_dynamic_imports: vec![],
433 resolved_dynamic_patterns: vec![],
434 member_accesses: vec![],
435 whole_object_uses: vec![],
436 has_cjs_exports: false,
437 unused_import_bindings: vec![],
438 },
439 ResolvedModule {
440 file_id: FileId(1),
441 path: PathBuf::from("/project/utils.ts"),
442 exports: vec![fallow_types::extract::ExportInfo {
443 name: ExportName::Named("foo".to_string()),
444 local_name: Some("foo".to_string()),
445 is_type_only: false,
446 span: oxc_span::Span::new(0, 20),
447 members: vec![],
448 }],
449 re_exports: vec![],
450 resolved_imports: vec![],
451 resolved_dynamic_imports: vec![],
452 resolved_dynamic_patterns: vec![],
453 member_accesses: vec![],
454 whole_object_uses: vec![],
455 has_cjs_exports: false,
456 unused_import_bindings: vec![],
457 },
458 ResolvedModule {
459 file_id: FileId(2),
460 path: PathBuf::from("/project/orphan.ts"),
461 exports: vec![fallow_types::extract::ExportInfo {
462 name: ExportName::Named("orphan".to_string()),
463 local_name: Some("orphan".to_string()),
464 is_type_only: false,
465 span: oxc_span::Span::new(0, 20),
466 members: vec![],
467 }],
468 re_exports: vec![],
469 resolved_imports: vec![],
470 resolved_dynamic_imports: vec![],
471 resolved_dynamic_patterns: vec![],
472 member_accesses: vec![],
473 whole_object_uses: vec![],
474 has_cjs_exports: false,
475 unused_import_bindings: vec![],
476 },
477 ];
478
479 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
480
481 assert!(graph.modules[0].is_reachable, "entry should be reachable");
482 assert!(graph.modules[1].is_reachable, "utils should be reachable");
483 assert!(
484 !graph.modules[2].is_reachable,
485 "orphan should NOT be reachable"
486 );
487 }
488
489 #[test]
490 fn graph_package_usage_tracked() {
491 let files = vec![DiscoveredFile {
492 id: FileId(0),
493 path: PathBuf::from("/project/entry.ts"),
494 size_bytes: 100,
495 }];
496
497 let entry_points = vec![EntryPoint {
498 path: PathBuf::from("/project/entry.ts"),
499 source: EntryPointSource::PackageJsonMain,
500 }];
501
502 let resolved_modules = vec![ResolvedModule {
503 file_id: FileId(0),
504 path: PathBuf::from("/project/entry.ts"),
505 exports: vec![],
506 re_exports: vec![],
507 resolved_imports: vec![
508 ResolvedImport {
509 info: ImportInfo {
510 source: "react".to_string(),
511 imported_name: ImportedName::Default,
512 local_name: "React".to_string(),
513 is_type_only: false,
514 span: oxc_span::Span::new(0, 10),
515 },
516 target: ResolveResult::NpmPackage("react".to_string()),
517 },
518 ResolvedImport {
519 info: ImportInfo {
520 source: "lodash".to_string(),
521 imported_name: ImportedName::Named("merge".to_string()),
522 local_name: "merge".to_string(),
523 is_type_only: false,
524 span: oxc_span::Span::new(15, 30),
525 },
526 target: ResolveResult::NpmPackage("lodash".to_string()),
527 },
528 ],
529 resolved_dynamic_imports: vec![],
530 resolved_dynamic_patterns: vec![],
531 member_accesses: vec![],
532 whole_object_uses: vec![],
533 has_cjs_exports: false,
534 unused_import_bindings: vec![],
535 }];
536
537 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
538 assert!(graph.package_usage.contains_key("react"));
539 assert!(graph.package_usage.contains_key("lodash"));
540 assert!(!graph.package_usage.contains_key("express"));
541 }
542
543 #[test]
544 fn graph_empty() {
545 let graph = ModuleGraph::build(&[], &[], &[]);
546 assert_eq!(graph.module_count(), 0);
547 assert_eq!(graph.edge_count(), 0);
548 }
549
550 #[test]
551 fn graph_cjs_exports_tracked() {
552 let files = vec![DiscoveredFile {
553 id: FileId(0),
554 path: PathBuf::from("/project/entry.ts"),
555 size_bytes: 100,
556 }];
557
558 let entry_points = vec![EntryPoint {
559 path: PathBuf::from("/project/entry.ts"),
560 source: EntryPointSource::PackageJsonMain,
561 }];
562
563 let resolved_modules = vec![ResolvedModule {
564 file_id: FileId(0),
565 path: PathBuf::from("/project/entry.ts"),
566 exports: vec![],
567 re_exports: vec![],
568 resolved_imports: vec![],
569 resolved_dynamic_imports: vec![],
570 resolved_dynamic_patterns: vec![],
571 member_accesses: vec![],
572 whole_object_uses: vec![],
573 has_cjs_exports: true,
574 unused_import_bindings: vec![],
575 }];
576
577 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
578 assert!(graph.modules[0].has_cjs_exports);
579 }
580}