1use std::ops::Range;
4use std::path::PathBuf;
5
6use fallow_types::discover::FileId;
7use fallow_types::extract::ExportName;
8
9#[derive(Debug)]
11pub struct ModuleNode {
12 pub file_id: FileId,
14 pub path: PathBuf,
16 pub edge_range: Range<usize>,
18 pub exports: Vec<ExportSymbol>,
20 pub re_exports: Vec<ReExportEdge>,
22 pub is_entry_point: bool,
24 pub is_reachable: bool,
26 pub is_runtime_reachable: bool,
28 pub is_test_reachable: bool,
30 pub has_cjs_exports: bool,
32}
33
34#[derive(Debug)]
36pub struct ReExportEdge {
37 pub source_file: FileId,
39 pub imported_name: String,
41 pub exported_name: String,
43 pub is_type_only: bool,
45}
46
47#[derive(Debug)]
49pub struct ExportSymbol {
50 pub name: ExportName,
52 pub is_type_only: bool,
54 pub is_public: bool,
57 pub span: oxc_span::Span,
59 pub references: Vec<SymbolReference>,
61 pub members: Vec<fallow_types::extract::MemberInfo>,
63}
64
65#[derive(Debug, Clone)]
67pub struct SymbolReference {
68 pub from_file: FileId,
70 pub kind: ReferenceKind,
72 pub import_span: oxc_span::Span,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum ReferenceKind {
80 NamedImport,
82 DefaultImport,
84 NamespaceImport,
86 ReExport,
88 DynamicImport,
90 SideEffectImport,
92}
93
94#[cfg(target_pointer_width = "64")]
98const _: () = assert!(std::mem::size_of::<ExportSymbol>() == 88);
99#[cfg(target_pointer_width = "64")]
100const _: () = assert!(std::mem::size_of::<SymbolReference>() == 16);
101#[cfg(target_pointer_width = "64")]
102const _: () = assert!(std::mem::size_of::<ReExportEdge>() == 56);
103#[cfg(all(target_pointer_width = "64", unix))]
106const _: () = assert!(std::mem::size_of::<ModuleNode>() == 104);
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
115 fn reference_kind_equality() {
116 assert_eq!(ReferenceKind::NamedImport, ReferenceKind::NamedImport);
117 assert_ne!(ReferenceKind::NamedImport, ReferenceKind::DefaultImport);
118 }
119
120 #[test]
121 fn reference_kind_all_variants_are_distinct() {
122 let all = [
123 ReferenceKind::NamedImport,
124 ReferenceKind::DefaultImport,
125 ReferenceKind::NamespaceImport,
126 ReferenceKind::ReExport,
127 ReferenceKind::DynamicImport,
128 ReferenceKind::SideEffectImport,
129 ];
130 for (i, a) in all.iter().enumerate() {
131 for (j, b) in all.iter().enumerate() {
132 if i == j {
133 assert_eq!(a, b);
134 } else {
135 assert_ne!(a, b);
136 }
137 }
138 }
139 }
140
141 #[test]
142 fn reference_kind_clone() {
143 let original = ReferenceKind::NamespaceImport;
144 let cloned = original.clone();
145 assert_eq!(original, cloned);
146 }
147
148 #[test]
149 fn reference_kind_debug_format() {
150 let kind = ReferenceKind::DynamicImport;
151 let debug_str = format!("{kind:?}");
152 assert_eq!(debug_str, "DynamicImport");
153 }
154
155 #[test]
158 fn symbol_reference_construction() {
159 let reference = SymbolReference {
160 from_file: FileId(42),
161 kind: ReferenceKind::NamedImport,
162 import_span: oxc_span::Span::new(10, 30),
163 };
164 assert_eq!(reference.from_file, FileId(42));
165 assert_eq!(reference.kind, ReferenceKind::NamedImport);
166 assert_eq!(reference.import_span.start, 10);
167 assert_eq!(reference.import_span.end, 30);
168 }
169
170 #[test]
171 fn symbol_reference_clone_preserves_all_fields() {
172 let reference = SymbolReference {
173 from_file: FileId(7),
174 kind: ReferenceKind::ReExport,
175 import_span: oxc_span::Span::new(5, 25),
176 };
177 let cloned = reference.clone();
178 assert_eq!(cloned.from_file, reference.from_file);
180 assert_eq!(cloned.kind, reference.kind);
181 assert_eq!(cloned.import_span.start, reference.import_span.start);
182 assert_eq!(cloned.import_span.end, reference.import_span.end);
183 }
184
185 #[test]
188 fn re_export_edge_construction() {
189 let edge = ReExportEdge {
190 source_file: FileId(3),
191 imported_name: "*".to_string(),
192 exported_name: "*".to_string(),
193 is_type_only: false,
194 };
195 assert_eq!(edge.source_file, FileId(3));
196 assert_eq!(edge.imported_name, "*");
197 assert_eq!(edge.exported_name, "*");
198 assert!(!edge.is_type_only);
199 }
200
201 #[test]
202 fn re_export_edge_type_only() {
203 let edge = ReExportEdge {
204 source_file: FileId(1),
205 imported_name: "MyType".to_string(),
206 exported_name: "MyType".to_string(),
207 is_type_only: true,
208 };
209 assert!(edge.is_type_only);
210 }
211
212 #[test]
213 fn re_export_edge_renamed() {
214 let edge = ReExportEdge {
215 source_file: FileId(2),
216 imported_name: "internal".to_string(),
217 exported_name: "public".to_string(),
218 is_type_only: false,
219 };
220 assert_ne!(edge.imported_name, edge.exported_name);
221 assert_eq!(edge.imported_name, "internal");
222 assert_eq!(edge.exported_name, "public");
223 }
224
225 #[test]
228 fn export_symbol_named() {
229 let sym = ExportSymbol {
230 name: ExportName::Named("myFunction".to_string()),
231 is_type_only: false,
232 is_public: false,
233 span: oxc_span::Span::new(0, 50),
234 references: vec![],
235 members: vec![],
236 };
237 assert!(matches!(sym.name, ExportName::Named(ref n) if n == "myFunction"));
238 assert!(!sym.is_type_only);
239 assert!(!sym.is_public);
240 }
241
242 #[test]
243 fn export_symbol_default() {
244 let sym = ExportSymbol {
245 name: ExportName::Default,
246 is_type_only: false,
247 is_public: false,
248 span: oxc_span::Span::new(0, 20),
249 references: vec![],
250 members: vec![],
251 };
252 assert!(matches!(sym.name, ExportName::Default));
253 }
254
255 #[test]
256 fn export_symbol_public_tag() {
257 let sym = ExportSymbol {
258 name: ExportName::Named("api".to_string()),
259 is_type_only: false,
260 is_public: true,
261 span: oxc_span::Span::new(0, 10),
262 references: vec![],
263 members: vec![],
264 };
265 assert!(sym.is_public);
266 }
267
268 #[test]
269 fn export_symbol_type_only() {
270 let sym = ExportSymbol {
271 name: ExportName::Named("MyInterface".to_string()),
272 is_type_only: true,
273 is_public: false,
274 span: oxc_span::Span::new(0, 30),
275 references: vec![],
276 members: vec![],
277 };
278 assert!(sym.is_type_only);
279 }
280
281 #[test]
282 fn export_symbol_with_references() {
283 let sym = ExportSymbol {
284 name: ExportName::Named("helper".to_string()),
285 is_type_only: false,
286 is_public: false,
287 span: oxc_span::Span::new(0, 20),
288 references: vec![
289 SymbolReference {
290 from_file: FileId(1),
291 kind: ReferenceKind::NamedImport,
292 import_span: oxc_span::Span::new(0, 10),
293 },
294 SymbolReference {
295 from_file: FileId(2),
296 kind: ReferenceKind::ReExport,
297 import_span: oxc_span::Span::new(5, 15),
298 },
299 ],
300 members: vec![],
301 };
302 assert_eq!(sym.references.len(), 2);
303 assert_eq!(sym.references[0].from_file, FileId(1));
304 assert_eq!(sym.references[1].kind, ReferenceKind::ReExport);
305 }
306
307 #[test]
310 fn module_node_construction() {
311 let node = ModuleNode {
312 file_id: FileId(0),
313 path: PathBuf::from("/project/src/index.ts"),
314 edge_range: 0..5,
315 exports: vec![],
316 re_exports: vec![],
317 is_entry_point: true,
318 is_reachable: true,
319 is_runtime_reachable: true,
320 is_test_reachable: false,
321 has_cjs_exports: false,
322 };
323 assert_eq!(node.file_id, FileId(0));
324 assert!(node.is_entry_point);
325 assert!(node.is_reachable);
326 assert!(node.is_runtime_reachable);
327 assert!(!node.is_test_reachable);
328 assert!(!node.has_cjs_exports);
329 assert_eq!(node.edge_range, 0..5);
330 }
331
332 #[test]
333 fn module_node_non_entry_unreachable() {
334 let node = ModuleNode {
335 file_id: FileId(5),
336 path: PathBuf::from("/project/src/orphan.ts"),
337 edge_range: 0..0,
338 exports: vec![],
339 re_exports: vec![],
340 is_entry_point: false,
341 is_reachable: false,
342 is_runtime_reachable: false,
343 is_test_reachable: false,
344 has_cjs_exports: false,
345 };
346 assert!(!node.is_entry_point);
347 assert!(!node.is_reachable);
348 assert!(!node.is_runtime_reachable);
349 assert!(!node.is_test_reachable);
350 assert!(node.edge_range.is_empty());
351 }
352
353 #[test]
354 fn module_node_cjs_exports() {
355 let node = ModuleNode {
356 file_id: FileId(2),
357 path: PathBuf::from("/project/lib/legacy.js"),
358 edge_range: 3..7,
359 exports: vec![],
360 re_exports: vec![],
361 is_entry_point: false,
362 is_reachable: true,
363 is_runtime_reachable: true,
364 is_test_reachable: false,
365 has_cjs_exports: true,
366 };
367 assert!(node.has_cjs_exports);
368 assert!(node.is_runtime_reachable);
369 assert_eq!(node.edge_range.len(), 4);
370 }
371
372 #[test]
373 fn module_node_with_exports_and_re_exports() {
374 let node = ModuleNode {
375 file_id: FileId(1),
376 path: PathBuf::from("/project/src/barrel.ts"),
377 edge_range: 0..3,
378 exports: vec![ExportSymbol {
379 name: ExportName::Named("localFn".to_string()),
380 is_type_only: false,
381 is_public: false,
382 span: oxc_span::Span::new(0, 20),
383 references: vec![],
384 members: vec![],
385 }],
386 re_exports: vec![ReExportEdge {
387 source_file: FileId(2),
388 imported_name: "*".to_string(),
389 exported_name: "*".to_string(),
390 is_type_only: false,
391 }],
392 is_entry_point: false,
393 is_reachable: true,
394 is_runtime_reachable: true,
395 is_test_reachable: false,
396 has_cjs_exports: false,
397 };
398 assert_eq!(node.exports.len(), 1);
399 assert_eq!(node.re_exports.len(), 1);
400 assert_eq!(node.re_exports[0].source_file, FileId(2));
401 }
402}