Skip to main content

fallow_extract/cache/
conversion.rs

1//! Conversion between [`ModuleInfo`](crate::ModuleInfo) and [`CachedModule`].
2//!
3//! Both functions convert between borrowed source structs and owned target structs
4//! (`&CachedModule -> ModuleInfo`, `&ModuleInfo -> CachedModule`). All `String` clones
5//! are structurally necessary: the cache store retains ownership of `CachedModule`
6//! entries (for persistence), and `ModuleInfo` must outlive the cache for the
7//! analysis pipeline. Eliminating these clones would require shared ownership
8//! (`Arc<str>`) across the entire extraction + analysis pipeline.
9
10use oxc_span::Span;
11
12use crate::ExportName;
13use fallow_types::extract::{NamespaceObjectAlias, VisibilityTag};
14
15use super::types::{
16    CachedDynamicImport, CachedDynamicImportPattern, CachedExport, CachedImport,
17    CachedLocalTypeDeclaration, CachedMember, CachedModule, CachedNamespaceObjectAlias,
18    CachedPublicSignatureTypeReference, CachedReExport, CachedRequireCall, CachedSuppression,
19    IMPORT_KIND_DEFAULT, IMPORT_KIND_NAMED, IMPORT_KIND_NAMESPACE, IMPORT_KIND_SIDE_EFFECT,
20};
21
22/// Reconstruct a [`ModuleInfo`](crate::ModuleInfo) from a [`CachedModule`].
23#[must_use]
24pub fn cached_to_module(
25    cached: &CachedModule,
26    file_id: fallow_types::discover::FileId,
27) -> crate::ModuleInfo {
28    cached_to_module_opts(cached, file_id, true)
29}
30
31/// Reconstruct a [`ModuleInfo`](crate::ModuleInfo) from a [`CachedModule`], skipping
32/// the per-function complexity vec when `need_complexity` is `false`. Avoids the
33/// `Vec<FunctionComplexity>` clone on warm runs of commands (e.g. `fallow check`)
34/// that don't consume complexity, which adds up across tens of thousands of files.
35#[must_use]
36#[expect(
37    clippy::too_many_lines,
38    reason = "single flat field-by-field deserialization; splitting it harms readability"
39)]
40pub fn cached_to_module_opts(
41    cached: &CachedModule,
42    file_id: fallow_types::discover::FileId,
43    need_complexity: bool,
44) -> crate::ModuleInfo {
45    use crate::{
46        DynamicImportInfo, ExportInfo, ImportInfo, ImportedName, LocalTypeDeclaration, MemberInfo,
47        ModuleInfo, PublicSignatureTypeReference, ReExportInfo, RequireCallInfo,
48    };
49
50    let exports = cached
51        .exports
52        .iter()
53        .map(|e| ExportInfo {
54            name: if e.is_default {
55                ExportName::Default
56            } else {
57                ExportName::Named(e.name.clone())
58            },
59            local_name: e.local_name.clone(),
60            is_type_only: e.is_type_only,
61            is_side_effect_used: e.is_side_effect_used,
62            visibility: match e.visibility {
63                1 => VisibilityTag::Public,
64                2 => VisibilityTag::Internal,
65                3 => VisibilityTag::Beta,
66                4 => VisibilityTag::Alpha,
67                5 => VisibilityTag::ExpectedUnused,
68                _ => VisibilityTag::None,
69            },
70            span: Span::new(e.span_start, e.span_end),
71            members: e
72                .members
73                .iter()
74                .map(|m| MemberInfo {
75                    name: m.name.clone(),
76                    kind: m.kind,
77                    span: Span::new(m.span_start, m.span_end),
78                    has_decorator: m.has_decorator,
79                    is_instance_returning_static: m.is_instance_returning_static,
80                })
81                .collect(),
82            super_class: e.super_class.clone(),
83        })
84        .collect();
85
86    let imports = cached
87        .imports
88        .iter()
89        .map(|i| ImportInfo {
90            source: i.source.clone(),
91            imported_name: match i.kind {
92                IMPORT_KIND_DEFAULT => ImportedName::Default,
93                IMPORT_KIND_NAMESPACE => ImportedName::Namespace,
94                IMPORT_KIND_SIDE_EFFECT => ImportedName::SideEffect,
95                // IMPORT_KIND_NAMED (0) and any unknown value default to Named
96                _ => ImportedName::Named(i.imported_name.clone()),
97            },
98            local_name: i.local_name.clone(),
99            is_type_only: i.is_type_only,
100            from_style: i.from_style,
101            span: Span::new(i.span_start, i.span_end),
102            source_span: Span::new(i.source_span_start, i.source_span_end),
103        })
104        .collect();
105
106    let re_exports = cached
107        .re_exports
108        .iter()
109        .map(|r| ReExportInfo {
110            source: r.source.clone(),
111            imported_name: r.imported_name.clone(),
112            exported_name: r.exported_name.clone(),
113            is_type_only: r.is_type_only,
114            span: Span::new(r.span_start, r.span_end),
115        })
116        .collect();
117
118    let dynamic_imports = cached
119        .dynamic_imports
120        .iter()
121        .map(|d| DynamicImportInfo {
122            source: d.source.clone(),
123            span: Span::new(d.span_start, d.span_end),
124            destructured_names: d.destructured_names.clone(),
125            local_name: d.local_name.clone(),
126            is_speculative: d.is_speculative,
127        })
128        .collect();
129
130    let require_calls = cached
131        .require_calls
132        .iter()
133        .map(|r| RequireCallInfo {
134            source: r.source.clone(),
135            span: Span::new(r.span_start, r.span_end),
136            destructured_names: r.destructured_names.clone(),
137            local_name: r.local_name.clone(),
138        })
139        .collect();
140
141    let dynamic_import_patterns = cached
142        .dynamic_import_patterns
143        .iter()
144        .map(|p| crate::DynamicImportPattern {
145            prefix: p.prefix.clone(),
146            suffix: p.suffix.clone(),
147            span: Span::new(p.span_start, p.span_end),
148        })
149        .collect();
150
151    let suppressions = cached
152        .suppressions
153        .iter()
154        .map(|s| crate::suppress::Suppression {
155            line: s.line,
156            comment_line: s.comment_line,
157            kind: if s.kind == 0 {
158                None
159            } else {
160                crate::suppress::IssueKind::from_discriminant(s.kind)
161            },
162        })
163        .collect();
164
165    ModuleInfo {
166        file_id,
167        exports,
168        imports,
169        re_exports,
170        dynamic_imports,
171        dynamic_import_patterns,
172        require_calls,
173        member_accesses: cached.member_accesses.clone(),
174        whole_object_uses: cached.whole_object_uses.clone(),
175        has_cjs_exports: cached.has_cjs_exports,
176        content_hash: cached.content_hash,
177        suppressions,
178        unused_import_bindings: cached.unused_import_bindings.clone(),
179        type_referenced_import_bindings: cached.type_referenced_import_bindings.clone(),
180        value_referenced_import_bindings: cached.value_referenced_import_bindings.clone(),
181        line_offsets: cached.line_offsets.clone(),
182        complexity: if need_complexity {
183            cached.complexity.clone()
184        } else {
185            Vec::new()
186        },
187        flag_uses: cached.flag_uses.clone(),
188        class_heritage: cached.class_heritage.clone(),
189        local_type_declarations: cached
190            .local_type_declarations
191            .iter()
192            .map(|decl| LocalTypeDeclaration {
193                name: decl.name.clone(),
194                span: Span::new(decl.span_start, decl.span_end),
195            })
196            .collect(),
197        public_signature_type_references: cached
198            .public_signature_type_references
199            .iter()
200            .map(|reference| PublicSignatureTypeReference {
201                export_name: reference.export_name.clone(),
202                type_name: reference.type_name.clone(),
203                span: Span::new(reference.span_start, reference.span_end),
204            })
205            .collect(),
206        namespace_object_aliases: cached
207            .namespace_object_aliases
208            .iter()
209            .map(|alias| NamespaceObjectAlias {
210                via_export_name: alias.via_export_name.clone(),
211                suffix: alias.suffix.clone(),
212                namespace_local: alias.namespace_local.clone(),
213            })
214            .collect(),
215    }
216}
217
218/// Convert a [`ModuleInfo`](crate::ModuleInfo) to a [`CachedModule`] for storage.
219///
220/// `mtime_secs` and `file_size` come from `std::fs::metadata()` at parse time
221/// and enable fast cache validation on subsequent runs (skip file read when
222/// mtime+size match).
223#[must_use]
224#[expect(
225    clippy::too_many_lines,
226    reason = "single flat field-by-field serialization; splitting it harms readability"
227)]
228pub fn module_to_cached(
229    module: &crate::ModuleInfo,
230    mtime_secs: u64,
231    file_size: u64,
232) -> CachedModule {
233    CachedModule {
234        content_hash: module.content_hash,
235        mtime_secs,
236        file_size,
237        exports: module
238            .exports
239            .iter()
240            .map(|e| CachedExport {
241                name: match &e.name {
242                    ExportName::Named(n) => n.clone(),
243                    ExportName::Default => String::new(),
244                },
245                is_default: matches!(e.name, ExportName::Default),
246                is_type_only: e.is_type_only,
247                is_side_effect_used: e.is_side_effect_used,
248                visibility: e.visibility as u8,
249                local_name: e.local_name.clone(),
250                span_start: e.span.start,
251                span_end: e.span.end,
252                members: e
253                    .members
254                    .iter()
255                    .map(|m| CachedMember {
256                        name: m.name.clone(),
257                        kind: m.kind,
258                        span_start: m.span.start,
259                        span_end: m.span.end,
260                        has_decorator: m.has_decorator,
261                        is_instance_returning_static: m.is_instance_returning_static,
262                    })
263                    .collect(),
264                super_class: e.super_class.clone(),
265            })
266            .collect(),
267        imports: module
268            .imports
269            .iter()
270            .map(|i| {
271                let (kind, imported_name) = match &i.imported_name {
272                    crate::ImportedName::Named(n) => (IMPORT_KIND_NAMED, n.clone()),
273                    crate::ImportedName::Default => (IMPORT_KIND_DEFAULT, String::new()),
274                    crate::ImportedName::Namespace => (IMPORT_KIND_NAMESPACE, String::new()),
275                    crate::ImportedName::SideEffect => (IMPORT_KIND_SIDE_EFFECT, String::new()),
276                };
277                CachedImport {
278                    source: i.source.clone(),
279                    imported_name,
280                    local_name: i.local_name.clone(),
281                    is_type_only: i.is_type_only,
282                    from_style: i.from_style,
283                    kind,
284                    span_start: i.span.start,
285                    span_end: i.span.end,
286                    source_span_start: i.source_span.start,
287                    source_span_end: i.source_span.end,
288                }
289            })
290            .collect(),
291        re_exports: module
292            .re_exports
293            .iter()
294            .map(|r| CachedReExport {
295                source: r.source.clone(),
296                imported_name: r.imported_name.clone(),
297                exported_name: r.exported_name.clone(),
298                is_type_only: r.is_type_only,
299                span_start: r.span.start,
300                span_end: r.span.end,
301            })
302            .collect(),
303        dynamic_imports: module
304            .dynamic_imports
305            .iter()
306            .map(|d| CachedDynamicImport {
307                source: d.source.clone(),
308                span_start: d.span.start,
309                span_end: d.span.end,
310                destructured_names: d.destructured_names.clone(),
311                local_name: d.local_name.clone(),
312                is_speculative: d.is_speculative,
313            })
314            .collect(),
315        require_calls: module
316            .require_calls
317            .iter()
318            .map(|r| CachedRequireCall {
319                source: r.source.clone(),
320                span_start: r.span.start,
321                span_end: r.span.end,
322                destructured_names: r.destructured_names.clone(),
323                local_name: r.local_name.clone(),
324            })
325            .collect(),
326        member_accesses: module.member_accesses.clone(),
327        whole_object_uses: module.whole_object_uses.clone(),
328        dynamic_import_patterns: module
329            .dynamic_import_patterns
330            .iter()
331            .map(|p| CachedDynamicImportPattern {
332                prefix: p.prefix.clone(),
333                suffix: p.suffix.clone(),
334                span_start: p.span.start,
335                span_end: p.span.end,
336            })
337            .collect(),
338        has_cjs_exports: module.has_cjs_exports,
339        unused_import_bindings: module.unused_import_bindings.clone(),
340        type_referenced_import_bindings: module.type_referenced_import_bindings.clone(),
341        value_referenced_import_bindings: module.value_referenced_import_bindings.clone(),
342        suppressions: module
343            .suppressions
344            .iter()
345            .map(|s| CachedSuppression {
346                line: s.line,
347                comment_line: s.comment_line,
348                kind: s
349                    .kind
350                    .map_or(0, crate::suppress::IssueKind::to_discriminant),
351            })
352            .collect(),
353        line_offsets: module.line_offsets.clone(),
354        complexity: module.complexity.clone(),
355        flag_uses: module.flag_uses.clone(),
356        class_heritage: module.class_heritage.clone(),
357        local_type_declarations: module
358            .local_type_declarations
359            .iter()
360            .map(|decl| CachedLocalTypeDeclaration {
361                name: decl.name.clone(),
362                span_start: decl.span.start,
363                span_end: decl.span.end,
364            })
365            .collect(),
366        public_signature_type_references: module
367            .public_signature_type_references
368            .iter()
369            .map(|reference| CachedPublicSignatureTypeReference {
370                export_name: reference.export_name.clone(),
371                type_name: reference.type_name.clone(),
372                span_start: reference.span.start,
373                span_end: reference.span.end,
374            })
375            .collect(),
376        namespace_object_aliases: module
377            .namespace_object_aliases
378            .iter()
379            .map(|alias| CachedNamespaceObjectAlias {
380                via_export_name: alias.via_export_name.clone(),
381                suffix: alias.suffix.clone(),
382                namespace_local: alias.namespace_local.clone(),
383            })
384            .collect(),
385    }
386}