1use std::ops::Range;
4use std::path::PathBuf;
5
6use fallow_types::discover::FileId;
7use fallow_types::extract::{ExportName, VisibilityTag};
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_side_effect_used: bool,
168 pub visibility: VisibilityTag,
171 pub span: oxc_span::Span,
173 pub references: Vec<SymbolReference>,
175 pub members: Vec<fallow_types::extract::MemberInfo>,
177}
178
179#[derive(Debug, Clone, Copy)]
181pub struct SymbolReference {
182 pub from_file: FileId,
184 pub kind: ReferenceKind,
186 pub import_span: oxc_span::Span,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum ReferenceKind {
194 NamedImport,
196 DefaultImport,
198 NamespaceImport,
200 ReExport,
202 DynamicImport,
204 SideEffectImport,
206}
207
208#[cfg(target_pointer_width = "64")]
212const _: () = assert!(std::mem::size_of::<ExportSymbol>() == 88);
213#[cfg(target_pointer_width = "64")]
214const _: () = assert!(std::mem::size_of::<SymbolReference>() == 16);
215#[cfg(target_pointer_width = "64")]
216const _: () = assert!(std::mem::size_of::<ReExportEdge>() == 64);
217#[cfg(all(target_pointer_width = "64", unix))]
220const _: () = assert!(std::mem::size_of::<ModuleNode>() == 96);
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
229 fn reference_kind_equality() {
230 assert_eq!(ReferenceKind::NamedImport, ReferenceKind::NamedImport);
231 assert_ne!(ReferenceKind::NamedImport, ReferenceKind::DefaultImport);
232 }
233
234 #[test]
235 fn reference_kind_all_variants_are_distinct() {
236 let all = [
237 ReferenceKind::NamedImport,
238 ReferenceKind::DefaultImport,
239 ReferenceKind::NamespaceImport,
240 ReferenceKind::ReExport,
241 ReferenceKind::DynamicImport,
242 ReferenceKind::SideEffectImport,
243 ];
244 for (i, a) in all.iter().enumerate() {
245 for (j, b) in all.iter().enumerate() {
246 if i == j {
247 assert_eq!(a, b);
248 } else {
249 assert_ne!(a, b);
250 }
251 }
252 }
253 }
254
255 #[test]
256 fn reference_kind_copy() {
257 let original = ReferenceKind::NamespaceImport;
258 let copied = original;
259 assert_eq!(original, copied);
260 }
261
262 #[test]
263 fn reference_kind_debug_format() {
264 let kind = ReferenceKind::DynamicImport;
265 let debug_str = format!("{kind:?}");
266 assert_eq!(debug_str, "DynamicImport");
267 }
268
269 #[test]
272 fn symbol_reference_construction() {
273 let reference = SymbolReference {
274 from_file: FileId(42),
275 kind: ReferenceKind::NamedImport,
276 import_span: oxc_span::Span::new(10, 30),
277 };
278 assert_eq!(reference.from_file, FileId(42));
279 assert_eq!(reference.kind, ReferenceKind::NamedImport);
280 assert_eq!(reference.import_span.start, 10);
281 assert_eq!(reference.import_span.end, 30);
282 }
283
284 #[test]
285 fn symbol_reference_copy_preserves_all_fields() {
286 let reference = SymbolReference {
287 from_file: FileId(7),
288 kind: ReferenceKind::ReExport,
289 import_span: oxc_span::Span::new(5, 25),
290 };
291 let copied = reference;
292 assert_eq!(copied.from_file, reference.from_file);
294 assert_eq!(copied.kind, reference.kind);
295 assert_eq!(copied.import_span.start, reference.import_span.start);
296 assert_eq!(copied.import_span.end, reference.import_span.end);
297 }
298
299 #[test]
302 fn re_export_edge_construction() {
303 let edge = ReExportEdge {
304 source_file: FileId(3),
305 imported_name: "*".to_string(),
306 exported_name: "*".to_string(),
307 is_type_only: false,
308 span: oxc_span::Span::default(),
309 };
310 assert_eq!(edge.source_file, FileId(3));
311 assert_eq!(edge.imported_name, "*");
312 assert_eq!(edge.exported_name, "*");
313 assert!(!edge.is_type_only);
314 }
315
316 #[test]
317 fn re_export_edge_type_only() {
318 let edge = ReExportEdge {
319 source_file: FileId(1),
320 imported_name: "MyType".to_string(),
321 exported_name: "MyType".to_string(),
322 is_type_only: true,
323 span: oxc_span::Span::default(),
324 };
325 assert!(edge.is_type_only);
326 }
327
328 #[test]
329 fn re_export_edge_renamed() {
330 let edge = ReExportEdge {
331 source_file: FileId(2),
332 imported_name: "internal".to_string(),
333 exported_name: "public".to_string(),
334 is_type_only: false,
335 span: oxc_span::Span::default(),
336 };
337 assert_ne!(edge.imported_name, edge.exported_name);
338 assert_eq!(edge.imported_name, "internal");
339 assert_eq!(edge.exported_name, "public");
340 }
341
342 #[test]
345 fn export_symbol_named() {
346 let sym = ExportSymbol {
347 name: ExportName::Named("myFunction".to_string()),
348 is_type_only: false,
349 is_side_effect_used: false,
350 visibility: VisibilityTag::None,
351 span: oxc_span::Span::new(0, 50),
352 references: vec![],
353 members: vec![],
354 };
355 assert!(matches!(sym.name, ExportName::Named(ref n) if n == "myFunction"));
356 assert!(!sym.is_type_only);
357 assert_eq!(sym.visibility, VisibilityTag::None);
358 }
359
360 #[test]
361 fn export_symbol_default() {
362 let sym = ExportSymbol {
363 name: ExportName::Default,
364 is_type_only: false,
365 is_side_effect_used: false,
366 visibility: VisibilityTag::None,
367 span: oxc_span::Span::new(0, 20),
368 references: vec![],
369 members: vec![],
370 };
371 assert!(matches!(sym.name, ExportName::Default));
372 }
373
374 #[test]
375 fn export_symbol_public_tag() {
376 let sym = ExportSymbol {
377 name: ExportName::Named("api".to_string()),
378 is_type_only: false,
379 is_side_effect_used: false,
380 visibility: VisibilityTag::Public,
381 span: oxc_span::Span::new(0, 10),
382 references: vec![],
383 members: vec![],
384 };
385 assert_eq!(sym.visibility, VisibilityTag::Public);
386 }
387
388 #[test]
389 fn export_symbol_type_only() {
390 let sym = ExportSymbol {
391 name: ExportName::Named("MyInterface".to_string()),
392 is_type_only: true,
393 is_side_effect_used: false,
394 visibility: VisibilityTag::None,
395 span: oxc_span::Span::new(0, 30),
396 references: vec![],
397 members: vec![],
398 };
399 assert!(sym.is_type_only);
400 }
401
402 #[test]
403 fn export_symbol_with_references() {
404 let sym = ExportSymbol {
405 name: ExportName::Named("helper".to_string()),
406 is_type_only: false,
407 is_side_effect_used: false,
408 visibility: VisibilityTag::None,
409 span: oxc_span::Span::new(0, 20),
410 references: vec![
411 SymbolReference {
412 from_file: FileId(1),
413 kind: ReferenceKind::NamedImport,
414 import_span: oxc_span::Span::new(0, 10),
415 },
416 SymbolReference {
417 from_file: FileId(2),
418 kind: ReferenceKind::ReExport,
419 import_span: oxc_span::Span::new(5, 15),
420 },
421 ],
422 members: vec![],
423 };
424 assert_eq!(sym.references.len(), 2);
425 assert_eq!(sym.references[0].from_file, FileId(1));
426 assert_eq!(sym.references[1].kind, ReferenceKind::ReExport);
427 }
428
429 #[test]
432 fn module_node_construction() {
433 let mut node = ModuleNode {
434 file_id: FileId(0),
435 path: PathBuf::from("/project/src/index.ts"),
436 edge_range: 0..5,
437 exports: vec![],
438 re_exports: vec![],
439 flags: ModuleNode::flags_from(true, true, false),
440 };
441 node.set_reachable(true);
442 assert_eq!(node.file_id, FileId(0));
443 assert!(node.is_entry_point());
444 assert!(node.is_reachable());
445 assert!(node.is_runtime_reachable());
446 assert!(!node.is_test_reachable());
447 assert!(!node.has_cjs_exports());
448 assert_eq!(node.edge_range, 0..5);
449 }
450
451 #[test]
452 fn module_node_non_entry_unreachable() {
453 let node = ModuleNode {
454 file_id: FileId(5),
455 path: PathBuf::from("/project/src/orphan.ts"),
456 edge_range: 0..0,
457 exports: vec![],
458 re_exports: vec![],
459 flags: ModuleNode::flags_from(false, false, false),
460 };
461 assert!(!node.is_entry_point());
462 assert!(!node.is_reachable());
463 assert!(!node.is_runtime_reachable());
464 assert!(!node.is_test_reachable());
465 assert!(node.edge_range.is_empty());
466 }
467
468 #[test]
469 fn module_node_cjs_exports() {
470 let mut node = ModuleNode {
471 file_id: FileId(2),
472 path: PathBuf::from("/project/lib/legacy.js"),
473 edge_range: 3..7,
474 exports: vec![],
475 re_exports: vec![],
476 flags: ModuleNode::flags_from(false, true, true),
477 };
478 node.set_reachable(true);
479 assert!(node.has_cjs_exports());
480 assert!(node.is_runtime_reachable());
481 assert_eq!(node.edge_range.len(), 4);
482 }
483
484 #[test]
485 fn module_node_with_exports_and_re_exports() {
486 let node = ModuleNode {
487 file_id: FileId(1),
488 path: PathBuf::from("/project/src/barrel.ts"),
489 edge_range: 0..3,
490 exports: vec![ExportSymbol {
491 name: ExportName::Named("localFn".to_string()),
492 is_type_only: false,
493 is_side_effect_used: false,
494 visibility: VisibilityTag::None,
495 span: oxc_span::Span::new(0, 20),
496 references: vec![],
497 members: vec![],
498 }],
499 re_exports: vec![ReExportEdge {
500 source_file: FileId(2),
501 imported_name: "*".to_string(),
502 exported_name: "*".to_string(),
503 is_type_only: false,
504 span: oxc_span::Span::default(),
505 }],
506 flags: ModuleNode::flags_from(false, true, false),
507 };
508 assert_eq!(node.exports.len(), 1);
509 assert_eq!(node.re_exports.len(), 1);
510 assert_eq!(node.re_exports[0].source_file, FileId(2));
511 }
512}