1use std::ops::Range;
4use std::path::PathBuf;
5
6use fallow_types::discover::FileId;
7use fallow_types::extract::ExportName;
8
9#[derive(Debug)]
15pub struct ModuleNode {
16 pub file_id: FileId,
18 pub path: PathBuf,
20 pub edge_range: Range<usize>,
22 pub exports: Vec<ExportSymbol>,
24 pub re_exports: Vec<ReExportEdge>,
26 pub(crate) flags: u8,
28}
29
30const FLAG_ENTRY_POINT: u8 = 1 << 0;
32const FLAG_REACHABLE: u8 = 1 << 1;
33const FLAG_RUNTIME_REACHABLE: u8 = 1 << 2;
34const FLAG_TEST_REACHABLE: u8 = 1 << 3;
35const FLAG_CJS_EXPORTS: u8 = 1 << 4;
36
37impl ModuleNode {
38 #[inline]
40 pub const fn is_entry_point(&self) -> bool {
41 self.flags & FLAG_ENTRY_POINT != 0
42 }
43
44 #[inline]
46 pub const fn is_reachable(&self) -> bool {
47 self.flags & FLAG_REACHABLE != 0
48 }
49
50 #[inline]
52 pub const fn is_runtime_reachable(&self) -> bool {
53 self.flags & FLAG_RUNTIME_REACHABLE != 0
54 }
55
56 #[inline]
58 pub const fn is_test_reachable(&self) -> bool {
59 self.flags & FLAG_TEST_REACHABLE != 0
60 }
61
62 #[inline]
64 pub const fn has_cjs_exports(&self) -> bool {
65 self.flags & FLAG_CJS_EXPORTS != 0
66 }
67
68 #[inline]
70 pub fn set_entry_point(&mut self, v: bool) {
71 if v {
72 self.flags |= FLAG_ENTRY_POINT;
73 } else {
74 self.flags &= !FLAG_ENTRY_POINT;
75 }
76 }
77
78 #[inline]
80 pub fn set_reachable(&mut self, v: bool) {
81 if v {
82 self.flags |= FLAG_REACHABLE;
83 } else {
84 self.flags &= !FLAG_REACHABLE;
85 }
86 }
87
88 #[inline]
90 pub fn set_runtime_reachable(&mut self, v: bool) {
91 if v {
92 self.flags |= FLAG_RUNTIME_REACHABLE;
93 } else {
94 self.flags &= !FLAG_RUNTIME_REACHABLE;
95 }
96 }
97
98 #[inline]
100 pub fn set_test_reachable(&mut self, v: bool) {
101 if v {
102 self.flags |= FLAG_TEST_REACHABLE;
103 } else {
104 self.flags &= !FLAG_TEST_REACHABLE;
105 }
106 }
107
108 #[inline]
110 pub fn set_cjs_exports(&mut self, v: bool) {
111 if v {
112 self.flags |= FLAG_CJS_EXPORTS;
113 } else {
114 self.flags &= !FLAG_CJS_EXPORTS;
115 }
116 }
117
118 #[inline]
120 pub(crate) fn flags_from(
121 is_entry_point: bool,
122 is_runtime_reachable: bool,
123 has_cjs_exports: bool,
124 ) -> u8 {
125 let mut f = 0u8;
126 if is_entry_point {
127 f |= FLAG_ENTRY_POINT;
128 }
129 if is_runtime_reachable {
130 f |= FLAG_RUNTIME_REACHABLE;
131 }
132 if has_cjs_exports {
133 f |= FLAG_CJS_EXPORTS;
134 }
135 f
136 }
137}
138
139#[derive(Debug)]
141pub struct ReExportEdge {
142 pub source_file: FileId,
144 pub imported_name: String,
146 pub exported_name: String,
148 pub is_type_only: bool,
150 pub span: oxc_span::Span,
154}
155
156#[derive(Debug)]
158pub struct ExportSymbol {
159 pub name: ExportName,
161 pub is_type_only: bool,
163 pub is_public: bool,
166 pub span: oxc_span::Span,
168 pub references: Vec<SymbolReference>,
170 pub members: Vec<fallow_types::extract::MemberInfo>,
172}
173
174#[derive(Debug, Clone, Copy)]
176pub struct SymbolReference {
177 pub from_file: FileId,
179 pub kind: ReferenceKind,
181 pub import_span: oxc_span::Span,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum ReferenceKind {
189 NamedImport,
191 DefaultImport,
193 NamespaceImport,
195 ReExport,
197 DynamicImport,
199 SideEffectImport,
201}
202
203#[cfg(target_pointer_width = "64")]
207const _: () = assert!(std::mem::size_of::<ExportSymbol>() == 88);
208#[cfg(target_pointer_width = "64")]
209const _: () = assert!(std::mem::size_of::<SymbolReference>() == 16);
210#[cfg(target_pointer_width = "64")]
211const _: () = assert!(std::mem::size_of::<ReExportEdge>() == 64);
212#[cfg(all(target_pointer_width = "64", unix))]
215const _: () = assert!(std::mem::size_of::<ModuleNode>() == 96);
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
224 fn reference_kind_equality() {
225 assert_eq!(ReferenceKind::NamedImport, ReferenceKind::NamedImport);
226 assert_ne!(ReferenceKind::NamedImport, ReferenceKind::DefaultImport);
227 }
228
229 #[test]
230 fn reference_kind_all_variants_are_distinct() {
231 let all = [
232 ReferenceKind::NamedImport,
233 ReferenceKind::DefaultImport,
234 ReferenceKind::NamespaceImport,
235 ReferenceKind::ReExport,
236 ReferenceKind::DynamicImport,
237 ReferenceKind::SideEffectImport,
238 ];
239 for (i, a) in all.iter().enumerate() {
240 for (j, b) in all.iter().enumerate() {
241 if i == j {
242 assert_eq!(a, b);
243 } else {
244 assert_ne!(a, b);
245 }
246 }
247 }
248 }
249
250 #[test]
251 fn reference_kind_copy() {
252 let original = ReferenceKind::NamespaceImport;
253 let copied = original;
254 assert_eq!(original, copied);
255 }
256
257 #[test]
258 fn reference_kind_debug_format() {
259 let kind = ReferenceKind::DynamicImport;
260 let debug_str = format!("{kind:?}");
261 assert_eq!(debug_str, "DynamicImport");
262 }
263
264 #[test]
267 fn symbol_reference_construction() {
268 let reference = SymbolReference {
269 from_file: FileId(42),
270 kind: ReferenceKind::NamedImport,
271 import_span: oxc_span::Span::new(10, 30),
272 };
273 assert_eq!(reference.from_file, FileId(42));
274 assert_eq!(reference.kind, ReferenceKind::NamedImport);
275 assert_eq!(reference.import_span.start, 10);
276 assert_eq!(reference.import_span.end, 30);
277 }
278
279 #[test]
280 fn symbol_reference_copy_preserves_all_fields() {
281 let reference = SymbolReference {
282 from_file: FileId(7),
283 kind: ReferenceKind::ReExport,
284 import_span: oxc_span::Span::new(5, 25),
285 };
286 let copied = reference;
287 assert_eq!(copied.from_file, reference.from_file);
289 assert_eq!(copied.kind, reference.kind);
290 assert_eq!(copied.import_span.start, reference.import_span.start);
291 assert_eq!(copied.import_span.end, reference.import_span.end);
292 }
293
294 #[test]
297 fn re_export_edge_construction() {
298 let edge = ReExportEdge {
299 source_file: FileId(3),
300 imported_name: "*".to_string(),
301 exported_name: "*".to_string(),
302 is_type_only: false,
303 span: oxc_span::Span::default(),
304 };
305 assert_eq!(edge.source_file, FileId(3));
306 assert_eq!(edge.imported_name, "*");
307 assert_eq!(edge.exported_name, "*");
308 assert!(!edge.is_type_only);
309 }
310
311 #[test]
312 fn re_export_edge_type_only() {
313 let edge = ReExportEdge {
314 source_file: FileId(1),
315 imported_name: "MyType".to_string(),
316 exported_name: "MyType".to_string(),
317 is_type_only: true,
318 span: oxc_span::Span::default(),
319 };
320 assert!(edge.is_type_only);
321 }
322
323 #[test]
324 fn re_export_edge_renamed() {
325 let edge = ReExportEdge {
326 source_file: FileId(2),
327 imported_name: "internal".to_string(),
328 exported_name: "public".to_string(),
329 is_type_only: false,
330 span: oxc_span::Span::default(),
331 };
332 assert_ne!(edge.imported_name, edge.exported_name);
333 assert_eq!(edge.imported_name, "internal");
334 assert_eq!(edge.exported_name, "public");
335 }
336
337 #[test]
340 fn export_symbol_named() {
341 let sym = ExportSymbol {
342 name: ExportName::Named("myFunction".to_string()),
343 is_type_only: false,
344 is_public: false,
345 span: oxc_span::Span::new(0, 50),
346 references: vec![],
347 members: vec![],
348 };
349 assert!(matches!(sym.name, ExportName::Named(ref n) if n == "myFunction"));
350 assert!(!sym.is_type_only);
351 assert!(!sym.is_public);
352 }
353
354 #[test]
355 fn export_symbol_default() {
356 let sym = ExportSymbol {
357 name: ExportName::Default,
358 is_type_only: false,
359 is_public: false,
360 span: oxc_span::Span::new(0, 20),
361 references: vec![],
362 members: vec![],
363 };
364 assert!(matches!(sym.name, ExportName::Default));
365 }
366
367 #[test]
368 fn export_symbol_public_tag() {
369 let sym = ExportSymbol {
370 name: ExportName::Named("api".to_string()),
371 is_type_only: false,
372 is_public: true,
373 span: oxc_span::Span::new(0, 10),
374 references: vec![],
375 members: vec![],
376 };
377 assert!(sym.is_public);
378 }
379
380 #[test]
381 fn export_symbol_type_only() {
382 let sym = ExportSymbol {
383 name: ExportName::Named("MyInterface".to_string()),
384 is_type_only: true,
385 is_public: false,
386 span: oxc_span::Span::new(0, 30),
387 references: vec![],
388 members: vec![],
389 };
390 assert!(sym.is_type_only);
391 }
392
393 #[test]
394 fn export_symbol_with_references() {
395 let sym = ExportSymbol {
396 name: ExportName::Named("helper".to_string()),
397 is_type_only: false,
398 is_public: false,
399 span: oxc_span::Span::new(0, 20),
400 references: vec![
401 SymbolReference {
402 from_file: FileId(1),
403 kind: ReferenceKind::NamedImport,
404 import_span: oxc_span::Span::new(0, 10),
405 },
406 SymbolReference {
407 from_file: FileId(2),
408 kind: ReferenceKind::ReExport,
409 import_span: oxc_span::Span::new(5, 15),
410 },
411 ],
412 members: vec![],
413 };
414 assert_eq!(sym.references.len(), 2);
415 assert_eq!(sym.references[0].from_file, FileId(1));
416 assert_eq!(sym.references[1].kind, ReferenceKind::ReExport);
417 }
418
419 #[test]
422 fn module_node_construction() {
423 let mut node = ModuleNode {
424 file_id: FileId(0),
425 path: PathBuf::from("/project/src/index.ts"),
426 edge_range: 0..5,
427 exports: vec![],
428 re_exports: vec![],
429 flags: ModuleNode::flags_from(true, true, false),
430 };
431 node.set_reachable(true);
432 assert_eq!(node.file_id, FileId(0));
433 assert!(node.is_entry_point());
434 assert!(node.is_reachable());
435 assert!(node.is_runtime_reachable());
436 assert!(!node.is_test_reachable());
437 assert!(!node.has_cjs_exports());
438 assert_eq!(node.edge_range, 0..5);
439 }
440
441 #[test]
442 fn module_node_non_entry_unreachable() {
443 let node = ModuleNode {
444 file_id: FileId(5),
445 path: PathBuf::from("/project/src/orphan.ts"),
446 edge_range: 0..0,
447 exports: vec![],
448 re_exports: vec![],
449 flags: ModuleNode::flags_from(false, false, false),
450 };
451 assert!(!node.is_entry_point());
452 assert!(!node.is_reachable());
453 assert!(!node.is_runtime_reachable());
454 assert!(!node.is_test_reachable());
455 assert!(node.edge_range.is_empty());
456 }
457
458 #[test]
459 fn module_node_cjs_exports() {
460 let mut node = ModuleNode {
461 file_id: FileId(2),
462 path: PathBuf::from("/project/lib/legacy.js"),
463 edge_range: 3..7,
464 exports: vec![],
465 re_exports: vec![],
466 flags: ModuleNode::flags_from(false, true, true),
467 };
468 node.set_reachable(true);
469 assert!(node.has_cjs_exports());
470 assert!(node.is_runtime_reachable());
471 assert_eq!(node.edge_range.len(), 4);
472 }
473
474 #[test]
475 fn module_node_with_exports_and_re_exports() {
476 let node = ModuleNode {
477 file_id: FileId(1),
478 path: PathBuf::from("/project/src/barrel.ts"),
479 edge_range: 0..3,
480 exports: vec![ExportSymbol {
481 name: ExportName::Named("localFn".to_string()),
482 is_type_only: false,
483 is_public: false,
484 span: oxc_span::Span::new(0, 20),
485 references: vec![],
486 members: vec![],
487 }],
488 re_exports: vec![ReExportEdge {
489 source_file: FileId(2),
490 imported_name: "*".to_string(),
491 exported_name: "*".to_string(),
492 is_type_only: false,
493 span: oxc_span::Span::default(),
494 }],
495 flags: ModuleNode::flags_from(false, true, false),
496 };
497 assert_eq!(node.exports.len(), 1);
498 assert_eq!(node.re_exports.len(), 1);
499 assert_eq!(node.re_exports[0].source_file, FileId(2));
500 }
501}