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 is_public: false,
226 span: oxc_span::Span::new(0, 20),
227 members: vec![],
228 },
229 fallow_types::extract::ExportInfo {
230 name: ExportName::Named("bar".to_string()),
231 local_name: Some("bar".to_string()),
232 is_type_only: false,
233 is_public: false,
234 span: oxc_span::Span::new(25, 45),
235 members: vec![],
236 },
237 ],
238 re_exports: vec![],
239 resolved_imports: vec![],
240 resolved_dynamic_imports: vec![],
241 resolved_dynamic_patterns: vec![],
242 member_accesses: vec![],
243 whole_object_uses: vec![],
244 has_cjs_exports: false,
245 unused_import_bindings: vec![],
246 },
247 ];
248
249 ModuleGraph::build(&resolved_modules, &entry_points, &files)
250 }
251
252 #[test]
253 fn graph_module_count() {
254 let graph = build_simple_graph();
255 assert_eq!(graph.module_count(), 2);
256 }
257
258 #[test]
259 fn graph_edge_count() {
260 let graph = build_simple_graph();
261 assert_eq!(graph.edge_count(), 1);
262 }
263
264 #[test]
265 fn graph_entry_point_is_reachable() {
266 let graph = build_simple_graph();
267 assert!(graph.modules[0].is_entry_point);
268 assert!(graph.modules[0].is_reachable);
269 }
270
271 #[test]
272 fn graph_imported_module_is_reachable() {
273 let graph = build_simple_graph();
274 assert!(!graph.modules[1].is_entry_point);
275 assert!(graph.modules[1].is_reachable);
276 }
277
278 #[test]
279 fn graph_export_has_reference() {
280 let graph = build_simple_graph();
281 let utils = &graph.modules[1];
282 let foo_export = utils
283 .exports
284 .iter()
285 .find(|e| e.name.to_string() == "foo")
286 .unwrap();
287 assert!(
288 !foo_export.references.is_empty(),
289 "foo should have references"
290 );
291 }
292
293 #[test]
294 fn graph_unused_export_no_reference() {
295 let graph = build_simple_graph();
296 let utils = &graph.modules[1];
297 let bar_export = utils
298 .exports
299 .iter()
300 .find(|e| e.name.to_string() == "bar")
301 .unwrap();
302 assert!(
303 bar_export.references.is_empty(),
304 "bar should have no references"
305 );
306 }
307
308 #[test]
309 fn graph_no_namespace_import() {
310 let graph = build_simple_graph();
311 assert!(!graph.has_namespace_import(FileId(0)));
312 assert!(!graph.has_namespace_import(FileId(1)));
313 }
314
315 #[test]
316 fn graph_has_namespace_import() {
317 let files = vec![
318 DiscoveredFile {
319 id: FileId(0),
320 path: PathBuf::from("/project/entry.ts"),
321 size_bytes: 100,
322 },
323 DiscoveredFile {
324 id: FileId(1),
325 path: PathBuf::from("/project/utils.ts"),
326 size_bytes: 50,
327 },
328 ];
329
330 let entry_points = vec![EntryPoint {
331 path: PathBuf::from("/project/entry.ts"),
332 source: EntryPointSource::PackageJsonMain,
333 }];
334
335 let resolved_modules = vec![
336 ResolvedModule {
337 file_id: FileId(0),
338 path: PathBuf::from("/project/entry.ts"),
339 exports: vec![],
340 re_exports: vec![],
341 resolved_imports: vec![ResolvedImport {
342 info: ImportInfo {
343 source: "./utils".to_string(),
344 imported_name: ImportedName::Namespace,
345 local_name: "utils".to_string(),
346 is_type_only: false,
347 span: oxc_span::Span::new(0, 10),
348 },
349 target: ResolveResult::InternalModule(FileId(1)),
350 }],
351 resolved_dynamic_imports: vec![],
352 resolved_dynamic_patterns: vec![],
353 member_accesses: vec![],
354 whole_object_uses: vec![],
355 has_cjs_exports: false,
356 unused_import_bindings: vec![],
357 },
358 ResolvedModule {
359 file_id: FileId(1),
360 path: PathBuf::from("/project/utils.ts"),
361 exports: vec![fallow_types::extract::ExportInfo {
362 name: ExportName::Named("foo".to_string()),
363 local_name: Some("foo".to_string()),
364 is_type_only: false,
365 is_public: false,
366 span: oxc_span::Span::new(0, 20),
367 members: vec![],
368 }],
369 re_exports: vec![],
370 resolved_imports: vec![],
371 resolved_dynamic_imports: vec![],
372 resolved_dynamic_patterns: vec![],
373 member_accesses: vec![],
374 whole_object_uses: vec![],
375 has_cjs_exports: false,
376 unused_import_bindings: vec![],
377 },
378 ];
379
380 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
381 assert!(
382 graph.has_namespace_import(FileId(1)),
383 "utils should have namespace import"
384 );
385 }
386
387 #[test]
388 fn graph_has_namespace_import_out_of_bounds() {
389 let graph = build_simple_graph();
390 assert!(!graph.has_namespace_import(FileId(999)));
391 }
392
393 #[test]
394 fn graph_unreachable_module() {
395 let files = vec![
397 DiscoveredFile {
398 id: FileId(0),
399 path: PathBuf::from("/project/entry.ts"),
400 size_bytes: 100,
401 },
402 DiscoveredFile {
403 id: FileId(1),
404 path: PathBuf::from("/project/utils.ts"),
405 size_bytes: 50,
406 },
407 DiscoveredFile {
408 id: FileId(2),
409 path: PathBuf::from("/project/orphan.ts"),
410 size_bytes: 30,
411 },
412 ];
413
414 let entry_points = vec![EntryPoint {
415 path: PathBuf::from("/project/entry.ts"),
416 source: EntryPointSource::PackageJsonMain,
417 }];
418
419 let resolved_modules = vec![
420 ResolvedModule {
421 file_id: FileId(0),
422 path: PathBuf::from("/project/entry.ts"),
423 exports: vec![],
424 re_exports: vec![],
425 resolved_imports: vec![ResolvedImport {
426 info: ImportInfo {
427 source: "./utils".to_string(),
428 imported_name: ImportedName::Named("foo".to_string()),
429 local_name: "foo".to_string(),
430 is_type_only: false,
431 span: oxc_span::Span::new(0, 10),
432 },
433 target: ResolveResult::InternalModule(FileId(1)),
434 }],
435 resolved_dynamic_imports: vec![],
436 resolved_dynamic_patterns: vec![],
437 member_accesses: vec![],
438 whole_object_uses: vec![],
439 has_cjs_exports: false,
440 unused_import_bindings: vec![],
441 },
442 ResolvedModule {
443 file_id: FileId(1),
444 path: PathBuf::from("/project/utils.ts"),
445 exports: vec![fallow_types::extract::ExportInfo {
446 name: ExportName::Named("foo".to_string()),
447 local_name: Some("foo".to_string()),
448 is_type_only: false,
449 is_public: false,
450 span: oxc_span::Span::new(0, 20),
451 members: vec![],
452 }],
453 re_exports: vec![],
454 resolved_imports: vec![],
455 resolved_dynamic_imports: vec![],
456 resolved_dynamic_patterns: vec![],
457 member_accesses: vec![],
458 whole_object_uses: vec![],
459 has_cjs_exports: false,
460 unused_import_bindings: vec![],
461 },
462 ResolvedModule {
463 file_id: FileId(2),
464 path: PathBuf::from("/project/orphan.ts"),
465 exports: vec![fallow_types::extract::ExportInfo {
466 name: ExportName::Named("orphan".to_string()),
467 local_name: Some("orphan".to_string()),
468 is_type_only: false,
469 is_public: false,
470 span: oxc_span::Span::new(0, 20),
471 members: vec![],
472 }],
473 re_exports: vec![],
474 resolved_imports: vec![],
475 resolved_dynamic_imports: vec![],
476 resolved_dynamic_patterns: vec![],
477 member_accesses: vec![],
478 whole_object_uses: vec![],
479 has_cjs_exports: false,
480 unused_import_bindings: vec![],
481 },
482 ];
483
484 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
485
486 assert!(graph.modules[0].is_reachable, "entry should be reachable");
487 assert!(graph.modules[1].is_reachable, "utils should be reachable");
488 assert!(
489 !graph.modules[2].is_reachable,
490 "orphan should NOT be reachable"
491 );
492 }
493
494 #[test]
495 fn graph_package_usage_tracked() {
496 let files = vec![DiscoveredFile {
497 id: FileId(0),
498 path: PathBuf::from("/project/entry.ts"),
499 size_bytes: 100,
500 }];
501
502 let entry_points = vec![EntryPoint {
503 path: PathBuf::from("/project/entry.ts"),
504 source: EntryPointSource::PackageJsonMain,
505 }];
506
507 let resolved_modules = vec![ResolvedModule {
508 file_id: FileId(0),
509 path: PathBuf::from("/project/entry.ts"),
510 exports: vec![],
511 re_exports: vec![],
512 resolved_imports: vec![
513 ResolvedImport {
514 info: ImportInfo {
515 source: "react".to_string(),
516 imported_name: ImportedName::Default,
517 local_name: "React".to_string(),
518 is_type_only: false,
519 span: oxc_span::Span::new(0, 10),
520 },
521 target: ResolveResult::NpmPackage("react".to_string()),
522 },
523 ResolvedImport {
524 info: ImportInfo {
525 source: "lodash".to_string(),
526 imported_name: ImportedName::Named("merge".to_string()),
527 local_name: "merge".to_string(),
528 is_type_only: false,
529 span: oxc_span::Span::new(15, 30),
530 },
531 target: ResolveResult::NpmPackage("lodash".to_string()),
532 },
533 ],
534 resolved_dynamic_imports: vec![],
535 resolved_dynamic_patterns: vec![],
536 member_accesses: vec![],
537 whole_object_uses: vec![],
538 has_cjs_exports: false,
539 unused_import_bindings: vec![],
540 }];
541
542 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
543 assert!(graph.package_usage.contains_key("react"));
544 assert!(graph.package_usage.contains_key("lodash"));
545 assert!(!graph.package_usage.contains_key("express"));
546 }
547
548 #[test]
549 fn graph_empty() {
550 let graph = ModuleGraph::build(&[], &[], &[]);
551 assert_eq!(graph.module_count(), 0);
552 assert_eq!(graph.edge_count(), 0);
553 }
554
555 #[test]
556 fn graph_cjs_exports_tracked() {
557 let files = vec![DiscoveredFile {
558 id: FileId(0),
559 path: PathBuf::from("/project/entry.ts"),
560 size_bytes: 100,
561 }];
562
563 let entry_points = vec![EntryPoint {
564 path: PathBuf::from("/project/entry.ts"),
565 source: EntryPointSource::PackageJsonMain,
566 }];
567
568 let resolved_modules = vec![ResolvedModule {
569 file_id: FileId(0),
570 path: PathBuf::from("/project/entry.ts"),
571 exports: vec![],
572 re_exports: vec![],
573 resolved_imports: vec![],
574 resolved_dynamic_imports: vec![],
575 resolved_dynamic_patterns: vec![],
576 member_accesses: vec![],
577 whole_object_uses: vec![],
578 has_cjs_exports: true,
579 unused_import_bindings: vec![],
580 }];
581
582 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
583 assert!(graph.modules[0].has_cjs_exports);
584 }
585}