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 expected_unused_reason: Option<String>,
172 pub span: oxc_span::Span,
174 pub references: Vec<SymbolReference>,
176 pub members: Vec<fallow_types::extract::MemberInfo>,
178}
179
180#[derive(Debug, Clone, Copy)]
182pub struct SymbolReference {
183 pub from_file: FileId,
185 pub kind: ReferenceKind,
187 pub import_span: oxc_span::Span,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum ReferenceKind {
195 NamedImport,
197 DefaultImport,
199 NamespaceImport,
201 ReExport,
203 DynamicImport,
205 SideEffectImport,
207}
208
209#[cfg(target_pointer_width = "64")]
210const _: () = assert!(std::mem::size_of::<ExportSymbol>() == 112);
211#[cfg(target_pointer_width = "64")]
212const _: () = assert!(std::mem::size_of::<SymbolReference>() == 16);
213#[cfg(target_pointer_width = "64")]
214const _: () = assert!(std::mem::size_of::<ReExportEdge>() == 64);
215#[cfg(all(target_pointer_width = "64", unix))]
216const _: () = assert!(std::mem::size_of::<ModuleNode>() == 96);
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn reference_kind_equality() {
224 assert_eq!(ReferenceKind::NamedImport, ReferenceKind::NamedImport);
225 assert_ne!(ReferenceKind::NamedImport, ReferenceKind::DefaultImport);
226 }
227
228 #[test]
229 fn reference_kind_all_variants_are_distinct() {
230 let all = [
231 ReferenceKind::NamedImport,
232 ReferenceKind::DefaultImport,
233 ReferenceKind::NamespaceImport,
234 ReferenceKind::ReExport,
235 ReferenceKind::DynamicImport,
236 ReferenceKind::SideEffectImport,
237 ];
238 for (i, a) in all.iter().enumerate() {
239 for (j, b) in all.iter().enumerate() {
240 if i == j {
241 assert_eq!(a, b);
242 } else {
243 assert_ne!(a, b);
244 }
245 }
246 }
247 }
248
249 #[test]
250 fn reference_kind_copy() {
251 let original = ReferenceKind::NamespaceImport;
252 let copied = original;
253 assert_eq!(original, copied);
254 }
255
256 #[test]
257 fn reference_kind_debug_format() {
258 let kind = ReferenceKind::DynamicImport;
259 let debug_str = format!("{kind:?}");
260 assert_eq!(debug_str, "DynamicImport");
261 }
262
263 #[test]
264 fn symbol_reference_construction() {
265 let reference = SymbolReference {
266 from_file: FileId(42),
267 kind: ReferenceKind::NamedImport,
268 import_span: oxc_span::Span::new(10, 30),
269 };
270 assert_eq!(reference.from_file, FileId(42));
271 assert_eq!(reference.kind, ReferenceKind::NamedImport);
272 assert_eq!(reference.import_span.start, 10);
273 assert_eq!(reference.import_span.end, 30);
274 }
275
276 #[test]
277 fn symbol_reference_copy_preserves_all_fields() {
278 let reference = SymbolReference {
279 from_file: FileId(7),
280 kind: ReferenceKind::ReExport,
281 import_span: oxc_span::Span::new(5, 25),
282 };
283 let copied = reference;
284 assert_eq!(copied.from_file, reference.from_file);
285 assert_eq!(copied.kind, reference.kind);
286 assert_eq!(copied.import_span.start, reference.import_span.start);
287 assert_eq!(copied.import_span.end, reference.import_span.end);
288 }
289
290 #[test]
291 fn re_export_edge_construction() {
292 let edge = ReExportEdge {
293 source_file: FileId(3),
294 imported_name: "*".to_string(),
295 exported_name: "*".to_string(),
296 is_type_only: false,
297 span: oxc_span::Span::default(),
298 };
299 assert_eq!(edge.source_file, FileId(3));
300 assert_eq!(edge.imported_name, "*");
301 assert_eq!(edge.exported_name, "*");
302 assert!(!edge.is_type_only);
303 }
304
305 #[test]
306 fn re_export_edge_type_only() {
307 let edge = ReExportEdge {
308 source_file: FileId(1),
309 imported_name: "MyType".to_string(),
310 exported_name: "MyType".to_string(),
311 is_type_only: true,
312 span: oxc_span::Span::default(),
313 };
314 assert!(edge.is_type_only);
315 }
316
317 #[test]
318 fn re_export_edge_renamed() {
319 let edge = ReExportEdge {
320 source_file: FileId(2),
321 imported_name: "internal".to_string(),
322 exported_name: "public".to_string(),
323 is_type_only: false,
324 span: oxc_span::Span::default(),
325 };
326 assert_ne!(edge.imported_name, edge.exported_name);
327 assert_eq!(edge.imported_name, "internal");
328 assert_eq!(edge.exported_name, "public");
329 }
330
331 #[test]
332 fn export_symbol_named() {
333 let sym = ExportSymbol {
334 name: ExportName::Named("myFunction".to_string()),
335 is_type_only: false,
336 is_side_effect_used: false,
337 visibility: VisibilityTag::None,
338 expected_unused_reason: None,
339 span: oxc_span::Span::new(0, 50),
340 references: vec![],
341 members: vec![],
342 };
343 assert!(matches!(sym.name, ExportName::Named(ref n) if n == "myFunction"));
344 assert!(!sym.is_type_only);
345 assert_eq!(sym.visibility, VisibilityTag::None);
346 }
347
348 #[test]
349 fn export_symbol_default() {
350 let sym = ExportSymbol {
351 name: ExportName::Default,
352 is_type_only: false,
353 is_side_effect_used: false,
354 visibility: VisibilityTag::None,
355 expected_unused_reason: None,
356 span: oxc_span::Span::new(0, 20),
357 references: vec![],
358 members: vec![],
359 };
360 assert!(matches!(sym.name, ExportName::Default));
361 }
362
363 #[test]
364 fn export_symbol_public_tag() {
365 let sym = ExportSymbol {
366 name: ExportName::Named("api".to_string()),
367 is_type_only: false,
368 is_side_effect_used: false,
369 visibility: VisibilityTag::Public,
370 expected_unused_reason: None,
371 span: oxc_span::Span::new(0, 10),
372 references: vec![],
373 members: vec![],
374 };
375 assert_eq!(sym.visibility, VisibilityTag::Public);
376 }
377
378 #[test]
379 fn export_symbol_type_only() {
380 let sym = ExportSymbol {
381 name: ExportName::Named("MyInterface".to_string()),
382 is_type_only: true,
383 is_side_effect_used: false,
384 visibility: VisibilityTag::None,
385 expected_unused_reason: None,
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_side_effect_used: false,
399 visibility: VisibilityTag::None,
400 expected_unused_reason: None,
401 span: oxc_span::Span::new(0, 20),
402 references: vec![
403 SymbolReference {
404 from_file: FileId(1),
405 kind: ReferenceKind::NamedImport,
406 import_span: oxc_span::Span::new(0, 10),
407 },
408 SymbolReference {
409 from_file: FileId(2),
410 kind: ReferenceKind::ReExport,
411 import_span: oxc_span::Span::new(5, 15),
412 },
413 ],
414 members: vec![],
415 };
416 assert_eq!(sym.references.len(), 2);
417 assert_eq!(sym.references[0].from_file, FileId(1));
418 assert_eq!(sym.references[1].kind, ReferenceKind::ReExport);
419 }
420
421 #[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_side_effect_used: false,
484 visibility: VisibilityTag::None,
485 expected_unused_reason: None,
486 span: oxc_span::Span::new(0, 20),
487 references: vec![],
488 members: vec![],
489 }],
490 re_exports: vec![ReExportEdge {
491 source_file: FileId(2),
492 imported_name: "*".to_string(),
493 exported_name: "*".to_string(),
494 is_type_only: false,
495 span: oxc_span::Span::default(),
496 }],
497 flags: ModuleNode::flags_from(false, true, false),
498 };
499 assert_eq!(node.exports.len(), 1);
500 assert_eq!(node.re_exports.len(), 1);
501 assert_eq!(node.re_exports[0].source_file, FileId(2));
502 }
503}