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 has CJS exports (module.exports / exports.*).
27    pub has_cjs_exports: bool,
28}
29
30/// A re-export edge, tracking which exports are forwarded from which module.
31#[derive(Debug)]
32pub struct ReExportEdge {
33    /// The module being re-exported from.
34    pub source_file: FileId,
35    /// The name imported from the source (or "*" for star re-exports).
36    pub imported_name: String,
37    /// The name exported from this module.
38    pub exported_name: String,
39    /// Whether this is a type-only re-export.
40    pub is_type_only: bool,
41}
42
43/// An export with reference tracking.
44#[derive(Debug)]
45pub struct ExportSymbol {
46    /// The exported name (named or default).
47    pub name: ExportName,
48    /// Whether this is a type-only export.
49    pub is_type_only: bool,
50    /// Whether this export has a `@public` JSDoc/TSDoc tag.
51    /// Exports marked `@public` are never reported as unused.
52    pub is_public: bool,
53    /// Source span of the export declaration.
54    pub span: oxc_span::Span,
55    /// Which files reference this export.
56    pub references: Vec<SymbolReference>,
57    /// Members of this export (enum members, class members).
58    pub members: Vec<fallow_types::extract::MemberInfo>,
59}
60
61/// A reference to an export from another file.
62#[derive(Debug, Clone)]
63pub struct SymbolReference {
64    /// The file that references this export.
65    pub from_file: FileId,
66    /// How the export is referenced.
67    pub kind: ReferenceKind,
68    /// Byte span of the import statement in the referencing file.
69    /// Used by the LSP to locate references for Code Lens navigation.
70    pub import_span: oxc_span::Span,
71}
72
73/// How an export is referenced.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum ReferenceKind {
76    /// A named import (`import { foo }`).
77    NamedImport,
78    /// A default import (`import Foo`).
79    DefaultImport,
80    /// A namespace import (`import * as ns`).
81    NamespaceImport,
82    /// A re-export (`export { foo } from './bar'`).
83    ReExport,
84    /// A dynamic import (`import('./foo')`).
85    DynamicImport,
86    /// A side-effect import (`import './styles'`).
87    SideEffectImport,
88}
89
90// Size assertions for types defined in this module.
91// `ExportSymbol` and `SymbolReference` are stored in Vecs per module node.
92// `ReExportEdge` is stored in a Vec per module for re-export chain resolution.
93#[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// `ModuleNode` is stored in a Vec — one per discovered file.
100// PathBuf has different sizes on Unix vs Windows, so restrict to Unix.
101#[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    // ── ReferenceKind ───────────────────────────────────────────
109
110    #[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    // ── SymbolReference ─────────────────────────────────────────
152
153    #[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        // Verify the clone matches the original
175        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    // ── ReExportEdge ────────────────────────────────────────────
182
183    #[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    // ── ExportSymbol ────────────────────────────────────────────
222
223    #[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    // ── ModuleNode ──────────────────────────────────────────────
304
305    #[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}