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    /// Source span of the re-export declaration on this module, used for
151    /// line-number reporting. `(0, 0)` for re-exports synthesized inside the
152    /// graph layer (e.g., `export *` chain propagation, namespace narrowing).
153    pub span: oxc_span::Span,
154}
155
156/// An export with reference tracking.
157#[derive(Debug)]
158pub struct ExportSymbol {
159    /// The exported name (named or default).
160    pub name: ExportName,
161    /// Whether this is a type-only export.
162    pub is_type_only: bool,
163    /// Whether this export has a `@public` JSDoc/TSDoc tag.
164    /// Exports marked `@public` are never reported as unused.
165    pub is_public: bool,
166    /// Source span of the export declaration.
167    pub span: oxc_span::Span,
168    /// Which files reference this export.
169    pub references: Vec<SymbolReference>,
170    /// Members of this export (enum members, class members).
171    pub members: Vec<fallow_types::extract::MemberInfo>,
172}
173
174/// A reference to an export from another file.
175#[derive(Debug, Clone, Copy)]
176pub struct SymbolReference {
177    /// The file that references this export.
178    pub from_file: FileId,
179    /// How the export is referenced.
180    pub kind: ReferenceKind,
181    /// Byte span of the import statement in the referencing file.
182    /// Used by the LSP to locate references for Code Lens navigation.
183    pub import_span: oxc_span::Span,
184}
185
186/// How an export is referenced.
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum ReferenceKind {
189    /// A named import (`import { foo }`).
190    NamedImport,
191    /// A default import (`import Foo`).
192    DefaultImport,
193    /// A namespace import (`import * as ns`).
194    NamespaceImport,
195    /// A re-export (`export { foo } from './bar'`).
196    ReExport,
197    /// A dynamic import (`import('./foo')`).
198    DynamicImport,
199    /// A side-effect import (`import './styles'`).
200    SideEffectImport,
201}
202
203// Size assertions for types defined in this module.
204// `ExportSymbol` and `SymbolReference` are stored in Vecs per module node.
205// `ReExportEdge` is stored in a Vec per module for re-export chain resolution.
206#[cfg(target_pointer_width = "64")]
207const _: () = assert!(std::mem::size_of::<ExportSymbol>() == 88);
208#[cfg(target_pointer_width = "64")]
209const _: () = assert!(std::mem::size_of::<SymbolReference>() == 16);
210#[cfg(target_pointer_width = "64")]
211const _: () = assert!(std::mem::size_of::<ReExportEdge>() == 64);
212// `ModuleNode` is stored in a Vec — one per discovered file.
213// PathBuf has different sizes on Unix vs Windows, so restrict to Unix.
214#[cfg(all(target_pointer_width = "64", unix))]
215const _: () = assert!(std::mem::size_of::<ModuleNode>() == 96);
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    // ── ReferenceKind ───────────────────────────────────────────
222
223    #[test]
224    fn reference_kind_equality() {
225        assert_eq!(ReferenceKind::NamedImport, ReferenceKind::NamedImport);
226        assert_ne!(ReferenceKind::NamedImport, ReferenceKind::DefaultImport);
227    }
228
229    #[test]
230    fn reference_kind_all_variants_are_distinct() {
231        let all = [
232            ReferenceKind::NamedImport,
233            ReferenceKind::DefaultImport,
234            ReferenceKind::NamespaceImport,
235            ReferenceKind::ReExport,
236            ReferenceKind::DynamicImport,
237            ReferenceKind::SideEffectImport,
238        ];
239        for (i, a) in all.iter().enumerate() {
240            for (j, b) in all.iter().enumerate() {
241                if i == j {
242                    assert_eq!(a, b);
243                } else {
244                    assert_ne!(a, b);
245                }
246            }
247        }
248    }
249
250    #[test]
251    fn reference_kind_copy() {
252        let original = ReferenceKind::NamespaceImport;
253        let copied = original;
254        assert_eq!(original, copied);
255    }
256
257    #[test]
258    fn reference_kind_debug_format() {
259        let kind = ReferenceKind::DynamicImport;
260        let debug_str = format!("{kind:?}");
261        assert_eq!(debug_str, "DynamicImport");
262    }
263
264    // ── SymbolReference ─────────────────────────────────────────
265
266    #[test]
267    fn symbol_reference_construction() {
268        let reference = SymbolReference {
269            from_file: FileId(42),
270            kind: ReferenceKind::NamedImport,
271            import_span: oxc_span::Span::new(10, 30),
272        };
273        assert_eq!(reference.from_file, FileId(42));
274        assert_eq!(reference.kind, ReferenceKind::NamedImport);
275        assert_eq!(reference.import_span.start, 10);
276        assert_eq!(reference.import_span.end, 30);
277    }
278
279    #[test]
280    fn symbol_reference_copy_preserves_all_fields() {
281        let reference = SymbolReference {
282            from_file: FileId(7),
283            kind: ReferenceKind::ReExport,
284            import_span: oxc_span::Span::new(5, 25),
285        };
286        let copied = reference;
287        // Verify the copy matches the original
288        assert_eq!(copied.from_file, reference.from_file);
289        assert_eq!(copied.kind, reference.kind);
290        assert_eq!(copied.import_span.start, reference.import_span.start);
291        assert_eq!(copied.import_span.end, reference.import_span.end);
292    }
293
294    // ── ReExportEdge ────────────────────────────────────────────
295
296    #[test]
297    fn re_export_edge_construction() {
298        let edge = ReExportEdge {
299            source_file: FileId(3),
300            imported_name: "*".to_string(),
301            exported_name: "*".to_string(),
302            is_type_only: false,
303            span: oxc_span::Span::default(),
304        };
305        assert_eq!(edge.source_file, FileId(3));
306        assert_eq!(edge.imported_name, "*");
307        assert_eq!(edge.exported_name, "*");
308        assert!(!edge.is_type_only);
309    }
310
311    #[test]
312    fn re_export_edge_type_only() {
313        let edge = ReExportEdge {
314            source_file: FileId(1),
315            imported_name: "MyType".to_string(),
316            exported_name: "MyType".to_string(),
317            is_type_only: true,
318            span: oxc_span::Span::default(),
319        };
320        assert!(edge.is_type_only);
321    }
322
323    #[test]
324    fn re_export_edge_renamed() {
325        let edge = ReExportEdge {
326            source_file: FileId(2),
327            imported_name: "internal".to_string(),
328            exported_name: "public".to_string(),
329            is_type_only: false,
330            span: oxc_span::Span::default(),
331        };
332        assert_ne!(edge.imported_name, edge.exported_name);
333        assert_eq!(edge.imported_name, "internal");
334        assert_eq!(edge.exported_name, "public");
335    }
336
337    // ── ExportSymbol ────────────────────────────────────────────
338
339    #[test]
340    fn export_symbol_named() {
341        let sym = ExportSymbol {
342            name: ExportName::Named("myFunction".to_string()),
343            is_type_only: false,
344            is_public: false,
345            span: oxc_span::Span::new(0, 50),
346            references: vec![],
347            members: vec![],
348        };
349        assert!(matches!(sym.name, ExportName::Named(ref n) if n == "myFunction"));
350        assert!(!sym.is_type_only);
351        assert!(!sym.is_public);
352    }
353
354    #[test]
355    fn export_symbol_default() {
356        let sym = ExportSymbol {
357            name: ExportName::Default,
358            is_type_only: false,
359            is_public: false,
360            span: oxc_span::Span::new(0, 20),
361            references: vec![],
362            members: vec![],
363        };
364        assert!(matches!(sym.name, ExportName::Default));
365    }
366
367    #[test]
368    fn export_symbol_public_tag() {
369        let sym = ExportSymbol {
370            name: ExportName::Named("api".to_string()),
371            is_type_only: false,
372            is_public: true,
373            span: oxc_span::Span::new(0, 10),
374            references: vec![],
375            members: vec![],
376        };
377        assert!(sym.is_public);
378    }
379
380    #[test]
381    fn export_symbol_type_only() {
382        let sym = ExportSymbol {
383            name: ExportName::Named("MyInterface".to_string()),
384            is_type_only: true,
385            is_public: false,
386            span: oxc_span::Span::new(0, 30),
387            references: vec![],
388            members: vec![],
389        };
390        assert!(sym.is_type_only);
391    }
392
393    #[test]
394    fn export_symbol_with_references() {
395        let sym = ExportSymbol {
396            name: ExportName::Named("helper".to_string()),
397            is_type_only: false,
398            is_public: false,
399            span: oxc_span::Span::new(0, 20),
400            references: vec![
401                SymbolReference {
402                    from_file: FileId(1),
403                    kind: ReferenceKind::NamedImport,
404                    import_span: oxc_span::Span::new(0, 10),
405                },
406                SymbolReference {
407                    from_file: FileId(2),
408                    kind: ReferenceKind::ReExport,
409                    import_span: oxc_span::Span::new(5, 15),
410                },
411            ],
412            members: vec![],
413        };
414        assert_eq!(sym.references.len(), 2);
415        assert_eq!(sym.references[0].from_file, FileId(1));
416        assert_eq!(sym.references[1].kind, ReferenceKind::ReExport);
417    }
418
419    // ── ModuleNode ──────────────────────────────────────────────
420
421    #[test]
422    fn module_node_construction() {
423        let mut node = ModuleNode {
424            file_id: FileId(0),
425            path: PathBuf::from("/project/src/index.ts"),
426            edge_range: 0..5,
427            exports: vec![],
428            re_exports: vec![],
429            flags: ModuleNode::flags_from(true, true, false),
430        };
431        node.set_reachable(true);
432        assert_eq!(node.file_id, FileId(0));
433        assert!(node.is_entry_point());
434        assert!(node.is_reachable());
435        assert!(node.is_runtime_reachable());
436        assert!(!node.is_test_reachable());
437        assert!(!node.has_cjs_exports());
438        assert_eq!(node.edge_range, 0..5);
439    }
440
441    #[test]
442    fn module_node_non_entry_unreachable() {
443        let node = ModuleNode {
444            file_id: FileId(5),
445            path: PathBuf::from("/project/src/orphan.ts"),
446            edge_range: 0..0,
447            exports: vec![],
448            re_exports: vec![],
449            flags: ModuleNode::flags_from(false, false, false),
450        };
451        assert!(!node.is_entry_point());
452        assert!(!node.is_reachable());
453        assert!(!node.is_runtime_reachable());
454        assert!(!node.is_test_reachable());
455        assert!(node.edge_range.is_empty());
456    }
457
458    #[test]
459    fn module_node_cjs_exports() {
460        let mut node = ModuleNode {
461            file_id: FileId(2),
462            path: PathBuf::from("/project/lib/legacy.js"),
463            edge_range: 3..7,
464            exports: vec![],
465            re_exports: vec![],
466            flags: ModuleNode::flags_from(false, true, true),
467        };
468        node.set_reachable(true);
469        assert!(node.has_cjs_exports());
470        assert!(node.is_runtime_reachable());
471        assert_eq!(node.edge_range.len(), 4);
472    }
473
474    #[test]
475    fn module_node_with_exports_and_re_exports() {
476        let node = ModuleNode {
477            file_id: FileId(1),
478            path: PathBuf::from("/project/src/barrel.ts"),
479            edge_range: 0..3,
480            exports: vec![ExportSymbol {
481                name: ExportName::Named("localFn".to_string()),
482                is_type_only: false,
483                is_public: false,
484                span: oxc_span::Span::new(0, 20),
485                references: vec![],
486                members: vec![],
487            }],
488            re_exports: vec![ReExportEdge {
489                source_file: FileId(2),
490                imported_name: "*".to_string(),
491                exported_name: "*".to_string(),
492                is_type_only: false,
493                span: oxc_span::Span::default(),
494            }],
495            flags: ModuleNode::flags_from(false, true, false),
496        };
497        assert_eq!(node.exports.len(), 1);
498        assert_eq!(node.re_exports.len(), 1);
499        assert_eq!(node.re_exports[0].source_file, FileId(2));
500    }
501}