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