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}
151
152#[derive(Debug)]
154pub struct ExportSymbol {
155 pub name: ExportName,
157 pub is_type_only: bool,
159 pub is_public: bool,
162 pub span: oxc_span::Span,
164 pub references: Vec<SymbolReference>,
166 pub members: Vec<fallow_types::extract::MemberInfo>,
168}
169
170#[derive(Debug, Clone)]
172pub struct SymbolReference {
173 pub from_file: FileId,
175 pub kind: ReferenceKind,
177 pub import_span: oxc_span::Span,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
184pub enum ReferenceKind {
185 NamedImport,
187 DefaultImport,
189 NamespaceImport,
191 ReExport,
193 DynamicImport,
195 SideEffectImport,
197}
198
199#[cfg(target_pointer_width = "64")]
203const _: () = assert!(std::mem::size_of::<ExportSymbol>() == 88);
204#[cfg(target_pointer_width = "64")]
205const _: () = assert!(std::mem::size_of::<SymbolReference>() == 16);
206#[cfg(target_pointer_width = "64")]
207const _: () = assert!(std::mem::size_of::<ReExportEdge>() == 56);
208#[cfg(all(target_pointer_width = "64", unix))]
211const _: () = assert!(std::mem::size_of::<ModuleNode>() == 96);
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
220 fn reference_kind_equality() {
221 assert_eq!(ReferenceKind::NamedImport, ReferenceKind::NamedImport);
222 assert_ne!(ReferenceKind::NamedImport, ReferenceKind::DefaultImport);
223 }
224
225 #[test]
226 fn reference_kind_all_variants_are_distinct() {
227 let all = [
228 ReferenceKind::NamedImport,
229 ReferenceKind::DefaultImport,
230 ReferenceKind::NamespaceImport,
231 ReferenceKind::ReExport,
232 ReferenceKind::DynamicImport,
233 ReferenceKind::SideEffectImport,
234 ];
235 for (i, a) in all.iter().enumerate() {
236 for (j, b) in all.iter().enumerate() {
237 if i == j {
238 assert_eq!(a, b);
239 } else {
240 assert_ne!(a, b);
241 }
242 }
243 }
244 }
245
246 #[test]
247 fn reference_kind_clone() {
248 let original = ReferenceKind::NamespaceImport;
249 let cloned = original.clone();
250 assert_eq!(original, cloned);
251 }
252
253 #[test]
254 fn reference_kind_debug_format() {
255 let kind = ReferenceKind::DynamicImport;
256 let debug_str = format!("{kind:?}");
257 assert_eq!(debug_str, "DynamicImport");
258 }
259
260 #[test]
263 fn symbol_reference_construction() {
264 let reference = SymbolReference {
265 from_file: FileId(42),
266 kind: ReferenceKind::NamedImport,
267 import_span: oxc_span::Span::new(10, 30),
268 };
269 assert_eq!(reference.from_file, FileId(42));
270 assert_eq!(reference.kind, ReferenceKind::NamedImport);
271 assert_eq!(reference.import_span.start, 10);
272 assert_eq!(reference.import_span.end, 30);
273 }
274
275 #[test]
276 fn symbol_reference_clone_preserves_all_fields() {
277 let reference = SymbolReference {
278 from_file: FileId(7),
279 kind: ReferenceKind::ReExport,
280 import_span: oxc_span::Span::new(5, 25),
281 };
282 let cloned = reference.clone();
283 assert_eq!(cloned.from_file, reference.from_file);
285 assert_eq!(cloned.kind, reference.kind);
286 assert_eq!(cloned.import_span.start, reference.import_span.start);
287 assert_eq!(cloned.import_span.end, reference.import_span.end);
288 }
289
290 #[test]
293 fn re_export_edge_construction() {
294 let edge = ReExportEdge {
295 source_file: FileId(3),
296 imported_name: "*".to_string(),
297 exported_name: "*".to_string(),
298 is_type_only: false,
299 };
300 assert_eq!(edge.source_file, FileId(3));
301 assert_eq!(edge.imported_name, "*");
302 assert_eq!(edge.exported_name, "*");
303 assert!(!edge.is_type_only);
304 }
305
306 #[test]
307 fn re_export_edge_type_only() {
308 let edge = ReExportEdge {
309 source_file: FileId(1),
310 imported_name: "MyType".to_string(),
311 exported_name: "MyType".to_string(),
312 is_type_only: true,
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 };
325 assert_ne!(edge.imported_name, edge.exported_name);
326 assert_eq!(edge.imported_name, "internal");
327 assert_eq!(edge.exported_name, "public");
328 }
329
330 #[test]
333 fn export_symbol_named() {
334 let sym = ExportSymbol {
335 name: ExportName::Named("myFunction".to_string()),
336 is_type_only: false,
337 is_public: false,
338 span: oxc_span::Span::new(0, 50),
339 references: vec![],
340 members: vec![],
341 };
342 assert!(matches!(sym.name, ExportName::Named(ref n) if n == "myFunction"));
343 assert!(!sym.is_type_only);
344 assert!(!sym.is_public);
345 }
346
347 #[test]
348 fn export_symbol_default() {
349 let sym = ExportSymbol {
350 name: ExportName::Default,
351 is_type_only: false,
352 is_public: false,
353 span: oxc_span::Span::new(0, 20),
354 references: vec![],
355 members: vec![],
356 };
357 assert!(matches!(sym.name, ExportName::Default));
358 }
359
360 #[test]
361 fn export_symbol_public_tag() {
362 let sym = ExportSymbol {
363 name: ExportName::Named("api".to_string()),
364 is_type_only: false,
365 is_public: true,
366 span: oxc_span::Span::new(0, 10),
367 references: vec![],
368 members: vec![],
369 };
370 assert!(sym.is_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_public: false,
379 span: oxc_span::Span::new(0, 30),
380 references: vec![],
381 members: vec![],
382 };
383 assert!(sym.is_type_only);
384 }
385
386 #[test]
387 fn export_symbol_with_references() {
388 let sym = ExportSymbol {
389 name: ExportName::Named("helper".to_string()),
390 is_type_only: false,
391 is_public: false,
392 span: oxc_span::Span::new(0, 20),
393 references: vec![
394 SymbolReference {
395 from_file: FileId(1),
396 kind: ReferenceKind::NamedImport,
397 import_span: oxc_span::Span::new(0, 10),
398 },
399 SymbolReference {
400 from_file: FileId(2),
401 kind: ReferenceKind::ReExport,
402 import_span: oxc_span::Span::new(5, 15),
403 },
404 ],
405 members: vec![],
406 };
407 assert_eq!(sym.references.len(), 2);
408 assert_eq!(sym.references[0].from_file, FileId(1));
409 assert_eq!(sym.references[1].kind, ReferenceKind::ReExport);
410 }
411
412 #[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_public: false,
477 span: oxc_span::Span::new(0, 20),
478 references: vec![],
479 members: vec![],
480 }],
481 re_exports: vec![ReExportEdge {
482 source_file: FileId(2),
483 imported_name: "*".to_string(),
484 exported_name: "*".to_string(),
485 is_type_only: false,
486 }],
487 flags: ModuleNode::flags_from(false, true, false),
488 };
489 assert_eq!(node.exports.len(), 1);
490 assert_eq!(node.re_exports.len(), 1);
491 assert_eq!(node.re_exports[0].source_file, FileId(2));
492 }
493}