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