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