Skip to main content

fallow_graph/graph/
types.rs

1//! Shared graph types: module nodes, re-export edges, export symbols, and references.
2
3use std::ops::Range;
4use std::path::PathBuf;
5
6use fallow_types::discover::FileId;
7use fallow_types::extract::ExportName;
8
9/// A single module in the graph.
10#[derive(Debug)]
11pub struct ModuleNode {
12    /// Unique identifier for this module.
13    pub file_id: FileId,
14    /// Absolute path to the module file.
15    pub path: PathBuf,
16    /// Range into the flat `edges` array.
17    pub edge_range: Range<usize>,
18    /// Exports declared by this module.
19    pub exports: Vec<ExportSymbol>,
20    /// Re-exports from this module (export { x } from './y', export * from './z').
21    pub re_exports: Vec<ReExportEdge>,
22    /// Whether this module is an entry point.
23    pub is_entry_point: bool,
24    /// Whether this module is reachable from any entry point.
25    pub is_reachable: bool,
26    /// Whether this module is reachable from a runtime/application root.
27    pub is_runtime_reachable: bool,
28    /// Whether this module is reachable from a test root.
29    pub is_test_reachable: bool,
30    /// Whether this module has CJS exports (module.exports / exports.*).
31    pub has_cjs_exports: bool,
32}
33
34/// A re-export edge, tracking which exports are forwarded from which module.
35#[derive(Debug)]
36pub struct ReExportEdge {
37    /// The module being re-exported from.
38    pub source_file: FileId,
39    /// The name imported from the source (or "*" for star re-exports).
40    pub imported_name: String,
41    /// The name exported from this module.
42    pub exported_name: String,
43    /// Whether this is a type-only re-export.
44    pub is_type_only: bool,
45}
46
47/// An export with reference tracking.
48#[derive(Debug)]
49pub struct ExportSymbol {
50    /// The exported name (named or default).
51    pub name: ExportName,
52    /// Whether this is a type-only export.
53    pub is_type_only: bool,
54    /// Whether this export has a `@public` JSDoc/TSDoc tag.
55    /// Exports marked `@public` are never reported as unused.
56    pub is_public: bool,
57    /// Source span of the export declaration.
58    pub span: oxc_span::Span,
59    /// Which files reference this export.
60    pub references: Vec<SymbolReference>,
61    /// Members of this export (enum members, class members).
62    pub members: Vec<fallow_types::extract::MemberInfo>,
63}
64
65/// A reference to an export from another file.
66#[derive(Debug, Clone)]
67pub struct SymbolReference {
68    /// The file that references this export.
69    pub from_file: FileId,
70    /// How the export is referenced.
71    pub kind: ReferenceKind,
72    /// Byte span of the import statement in the referencing file.
73    /// Used by the LSP to locate references for Code Lens navigation.
74    pub import_span: oxc_span::Span,
75}
76
77/// How an export is referenced.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum ReferenceKind {
80    /// A named import (`import { foo }`).
81    NamedImport,
82    /// A default import (`import Foo`).
83    DefaultImport,
84    /// A namespace import (`import * as ns`).
85    NamespaceImport,
86    /// A re-export (`export { foo } from './bar'`).
87    ReExport,
88    /// A dynamic import (`import('./foo')`).
89    DynamicImport,
90    /// A side-effect import (`import './styles'`).
91    SideEffectImport,
92}
93
94// Size assertions for types defined in this module.
95// `ExportSymbol` and `SymbolReference` are stored in Vecs per module node.
96// `ReExportEdge` is stored in a Vec per module for re-export chain resolution.
97#[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// `ModuleNode` is stored in a Vec — one per discovered file.
104// PathBuf has different sizes on Unix vs Windows, so restrict to Unix.
105#[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    // ── ReferenceKind ───────────────────────────────────────────
113
114    #[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    // ── SymbolReference ─────────────────────────────────────────
156
157    #[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        // Verify the clone matches the original
179        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    // ── ReExportEdge ────────────────────────────────────────────
186
187    #[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    // ── ExportSymbol ────────────────────────────────────────────
226
227    #[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    // ── ModuleNode ──────────────────────────────────────────────
308
309    #[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}