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