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///
11/// Boolean flags are packed into a `u8` to keep the struct at 96 bytes
12/// (down from 104 with 5 separate `bool` fields), improving cache line
13/// utilization in hot graph traversal loops.
14#[derive(Debug)]
15pub struct ModuleNode {
16    /// Unique identifier for this module.
17    pub file_id: FileId,
18    /// Absolute path to the module file.
19    pub path: PathBuf,
20    /// Range into the flat `edges` array.
21    pub edge_range: Range<usize>,
22    /// Exports declared by this module.
23    pub exports: Vec<ExportSymbol>,
24    /// Re-exports from this module (export { x } from './y', export * from './z').
25    pub re_exports: Vec<ReExportEdge>,
26    /// Packed boolean flags (entry point, reachability, CJS).
27    pub(crate) flags: u8,
28}
29
30// Bit positions for packed boolean flags in `ModuleNode::flags`.
31const 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    /// Whether this module is an entry point.
39    #[inline]
40    pub const fn is_entry_point(&self) -> bool {
41        self.flags & FLAG_ENTRY_POINT != 0
42    }
43
44    /// Whether this module is reachable from any entry point.
45    #[inline]
46    pub const fn is_reachable(&self) -> bool {
47        self.flags & FLAG_REACHABLE != 0
48    }
49
50    /// Whether this module is reachable from a runtime/application root.
51    #[inline]
52    pub const fn is_runtime_reachable(&self) -> bool {
53        self.flags & FLAG_RUNTIME_REACHABLE != 0
54    }
55
56    /// Whether this module is reachable from a test root.
57    #[inline]
58    pub const fn is_test_reachable(&self) -> bool {
59        self.flags & FLAG_TEST_REACHABLE != 0
60    }
61
62    /// Whether this module has CJS exports (module.exports / exports.*).
63    #[inline]
64    pub const fn has_cjs_exports(&self) -> bool {
65        self.flags & FLAG_CJS_EXPORTS != 0
66    }
67
68    /// Set whether this module is an entry point.
69    #[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    /// Set whether this module is reachable from any entry point.
79    #[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    /// Set whether this module is reachable from a runtime/application root.
89    #[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    /// Set whether this module is reachable from a test root.
99    #[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    /// Set whether this module has CJS exports.
109    #[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    /// Build flags byte from individual booleans (used by graph construction).
119    #[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/// A re-export edge, tracking which exports are forwarded from which module.
140#[derive(Debug)]
141pub struct ReExportEdge {
142    /// The module being re-exported from.
143    pub source_file: FileId,
144    /// The name imported from the source (or "*" for star re-exports).
145    pub imported_name: String,
146    /// The name exported from this module.
147    pub exported_name: String,
148    /// Whether this is a type-only re-export.
149    pub is_type_only: bool,
150}
151
152/// An export with reference tracking.
153#[derive(Debug)]
154pub struct ExportSymbol {
155    /// The exported name (named or default).
156    pub name: ExportName,
157    /// Whether this is a type-only export.
158    pub is_type_only: bool,
159    /// Whether this export has a `@public` JSDoc/TSDoc tag.
160    /// Exports marked `@public` are never reported as unused.
161    pub is_public: bool,
162    /// Source span of the export declaration.
163    pub span: oxc_span::Span,
164    /// Which files reference this export.
165    pub references: Vec<SymbolReference>,
166    /// Members of this export (enum members, class members).
167    pub members: Vec<fallow_types::extract::MemberInfo>,
168}
169
170/// A reference to an export from another file.
171#[derive(Debug, Clone)]
172pub struct SymbolReference {
173    /// The file that references this export.
174    pub from_file: FileId,
175    /// How the export is referenced.
176    pub kind: ReferenceKind,
177    /// Byte span of the import statement in the referencing file.
178    /// Used by the LSP to locate references for Code Lens navigation.
179    pub import_span: oxc_span::Span,
180}
181
182/// How an export is referenced.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub enum ReferenceKind {
185    /// A named import (`import { foo }`).
186    NamedImport,
187    /// A default import (`import Foo`).
188    DefaultImport,
189    /// A namespace import (`import * as ns`).
190    NamespaceImport,
191    /// A re-export (`export { foo } from './bar'`).
192    ReExport,
193    /// A dynamic import (`import('./foo')`).
194    DynamicImport,
195    /// A side-effect import (`import './styles'`).
196    SideEffectImport,
197}
198
199// Size assertions for types defined in this module.
200// `ExportSymbol` and `SymbolReference` are stored in Vecs per module node.
201// `ReExportEdge` is stored in a Vec per module for re-export chain resolution.
202#[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// `ModuleNode` is stored in a Vec — one per discovered file.
209// PathBuf has different sizes on Unix vs Windows, so restrict to Unix.
210#[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    // ── ReferenceKind ───────────────────────────────────────────
218
219    #[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    // ── SymbolReference ─────────────────────────────────────────
261
262    #[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        // Verify the clone matches the original
284        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    // ── ReExportEdge ────────────────────────────────────────────
291
292    #[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    // ── ExportSymbol ────────────────────────────────────────────
331
332    #[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    // ── ModuleNode ──────────────────────────────────────────────
413
414    #[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}