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                    is_self_returning: m.is_self_returning,
81                })
82                .collect(),
83            super_class: e.super_class.clone(),
84        })
85        .collect();
86
87    let imports = cached
88        .imports
89        .iter()
90        .map(|i| ImportInfo {
91            source: i.source.clone(),
92            imported_name: match i.kind {
93                IMPORT_KIND_DEFAULT => ImportedName::Default,
94                IMPORT_KIND_NAMESPACE => ImportedName::Namespace,
95                IMPORT_KIND_SIDE_EFFECT => ImportedName::SideEffect,
96                // IMPORT_KIND_NAMED (0) and any unknown value default to Named
97                _ => ImportedName::Named(i.imported_name.clone()),
98            },
99            local_name: i.local_name.clone(),
100            is_type_only: i.is_type_only,
101            from_style: i.from_style,
102            span: Span::new(i.span_start, i.span_end),
103            source_span: Span::new(i.source_span_start, i.source_span_end),
104        })
105        .collect();
106
107    let re_exports = cached
108        .re_exports
109        .iter()
110        .map(|r| ReExportInfo {
111            source: r.source.clone(),
112            imported_name: r.imported_name.clone(),
113            exported_name: r.exported_name.clone(),
114            is_type_only: r.is_type_only,
115            span: Span::new(r.span_start, r.span_end),
116        })
117        .collect();
118
119    let dynamic_imports = cached
120        .dynamic_imports
121        .iter()
122        .map(|d| DynamicImportInfo {
123            source: d.source.clone(),
124            span: Span::new(d.span_start, d.span_end),
125            destructured_names: d.destructured_names.clone(),
126            local_name: d.local_name.clone(),
127            is_speculative: d.is_speculative,
128        })
129        .collect();
130
131    let require_calls = cached
132        .require_calls
133        .iter()
134        .map(|r| RequireCallInfo {
135            source: r.source.clone(),
136            span: Span::new(r.span_start, r.span_end),
137            destructured_names: r.destructured_names.clone(),
138            local_name: r.local_name.clone(),
139        })
140        .collect();
141
142    let dynamic_import_patterns = cached
143        .dynamic_import_patterns
144        .iter()
145        .map(|p| crate::DynamicImportPattern {
146            prefix: p.prefix.clone(),
147            suffix: p.suffix.clone(),
148            span: Span::new(p.span_start, p.span_end),
149        })
150        .collect();
151
152    let suppressions = cached
153        .suppressions
154        .iter()
155        .map(|s| crate::suppress::Suppression {
156            line: s.line,
157            comment_line: s.comment_line,
158            kind: if s.kind == 0 {
159                None
160            } else {
161                crate::suppress::IssueKind::from_discriminant(s.kind)
162            },
163        })
164        .collect();
165
166    ModuleInfo {
167        file_id,
168        exports,
169        imports,
170        re_exports,
171        dynamic_imports,
172        dynamic_import_patterns,
173        require_calls,
174        member_accesses: cached.member_accesses.clone(),
175        whole_object_uses: cached.whole_object_uses.clone(),
176        has_cjs_exports: cached.has_cjs_exports,
177        has_angular_component_template_url: cached.has_angular_component_template_url,
178        content_hash: cached.content_hash,
179        suppressions,
180        unused_import_bindings: cached.unused_import_bindings.clone(),
181        type_referenced_import_bindings: cached.type_referenced_import_bindings.clone(),
182        value_referenced_import_bindings: cached.value_referenced_import_bindings.clone(),
183        line_offsets: cached.line_offsets.clone(),
184        complexity: if need_complexity {
185            cached.complexity.clone()
186        } else {
187            Vec::new()
188        },
189        flag_uses: cached.flag_uses.clone(),
190        class_heritage: cached.class_heritage.clone(),
191        local_type_declarations: cached
192            .local_type_declarations
193            .iter()
194            .map(|decl| LocalTypeDeclaration {
195                name: decl.name.clone(),
196                span: Span::new(decl.span_start, decl.span_end),
197            })
198            .collect(),
199        public_signature_type_references: cached
200            .public_signature_type_references
201            .iter()
202            .map(|reference| PublicSignatureTypeReference {
203                export_name: reference.export_name.clone(),
204                type_name: reference.type_name.clone(),
205                span: Span::new(reference.span_start, reference.span_end),
206            })
207            .collect(),
208        namespace_object_aliases: cached
209            .namespace_object_aliases
210            .iter()
211            .map(|alias| NamespaceObjectAlias {
212                via_export_name: alias.via_export_name.clone(),
213                suffix: alias.suffix.clone(),
214                namespace_local: alias.namespace_local.clone(),
215            })
216            .collect(),
217    }
218}
219
220/// Convert a [`ModuleInfo`](crate::ModuleInfo) to a [`CachedModule`] for storage.
221///
222/// `mtime_secs` and `file_size` come from `std::fs::metadata()` at parse time
223/// and enable fast cache validation on subsequent runs (skip file read when
224/// mtime+size match).
225#[must_use]
226#[expect(
227    clippy::too_many_lines,
228    reason = "single flat field-by-field serialization; splitting it harms readability"
229)]
230pub fn module_to_cached(
231    module: &crate::ModuleInfo,
232    mtime_secs: u64,
233    file_size: u64,
234) -> CachedModule {
235    CachedModule {
236        content_hash: module.content_hash,
237        mtime_secs,
238        file_size,
239        exports: module
240            .exports
241            .iter()
242            .map(|e| CachedExport {
243                name: match &e.name {
244                    ExportName::Named(n) => n.clone(),
245                    ExportName::Default => String::new(),
246                },
247                is_default: matches!(e.name, ExportName::Default),
248                is_type_only: e.is_type_only,
249                is_side_effect_used: e.is_side_effect_used,
250                visibility: e.visibility as u8,
251                local_name: e.local_name.clone(),
252                span_start: e.span.start,
253                span_end: e.span.end,
254                members: e
255                    .members
256                    .iter()
257                    .map(|m| CachedMember {
258                        name: m.name.clone(),
259                        kind: m.kind,
260                        span_start: m.span.start,
261                        span_end: m.span.end,
262                        has_decorator: m.has_decorator,
263                        is_instance_returning_static: m.is_instance_returning_static,
264                        is_self_returning: m.is_self_returning,
265                    })
266                    .collect(),
267                super_class: e.super_class.clone(),
268            })
269            .collect(),
270        imports: module
271            .imports
272            .iter()
273            .map(|i| {
274                let (kind, imported_name) = match &i.imported_name {
275                    crate::ImportedName::Named(n) => (IMPORT_KIND_NAMED, n.clone()),
276                    crate::ImportedName::Default => (IMPORT_KIND_DEFAULT, String::new()),
277                    crate::ImportedName::Namespace => (IMPORT_KIND_NAMESPACE, String::new()),
278                    crate::ImportedName::SideEffect => (IMPORT_KIND_SIDE_EFFECT, String::new()),
279                };
280                CachedImport {
281                    source: i.source.clone(),
282                    imported_name,
283                    local_name: i.local_name.clone(),
284                    is_type_only: i.is_type_only,
285                    from_style: i.from_style,
286                    kind,
287                    span_start: i.span.start,
288                    span_end: i.span.end,
289                    source_span_start: i.source_span.start,
290                    source_span_end: i.source_span.end,
291                }
292            })
293            .collect(),
294        re_exports: module
295            .re_exports
296            .iter()
297            .map(|r| CachedReExport {
298                source: r.source.clone(),
299                imported_name: r.imported_name.clone(),
300                exported_name: r.exported_name.clone(),
301                is_type_only: r.is_type_only,
302                span_start: r.span.start,
303                span_end: r.span.end,
304            })
305            .collect(),
306        dynamic_imports: module
307            .dynamic_imports
308            .iter()
309            .map(|d| CachedDynamicImport {
310                source: d.source.clone(),
311                span_start: d.span.start,
312                span_end: d.span.end,
313                destructured_names: d.destructured_names.clone(),
314                local_name: d.local_name.clone(),
315                is_speculative: d.is_speculative,
316            })
317            .collect(),
318        require_calls: module
319            .require_calls
320            .iter()
321            .map(|r| CachedRequireCall {
322                source: r.source.clone(),
323                span_start: r.span.start,
324                span_end: r.span.end,
325                destructured_names: r.destructured_names.clone(),
326                local_name: r.local_name.clone(),
327            })
328            .collect(),
329        member_accesses: module.member_accesses.clone(),
330        whole_object_uses: module.whole_object_uses.clone(),
331        dynamic_import_patterns: module
332            .dynamic_import_patterns
333            .iter()
334            .map(|p| CachedDynamicImportPattern {
335                prefix: p.prefix.clone(),
336                suffix: p.suffix.clone(),
337                span_start: p.span.start,
338                span_end: p.span.end,
339            })
340            .collect(),
341        has_cjs_exports: module.has_cjs_exports,
342        has_angular_component_template_url: module.has_angular_component_template_url,
343        unused_import_bindings: module.unused_import_bindings.clone(),
344        type_referenced_import_bindings: module.type_referenced_import_bindings.clone(),
345        value_referenced_import_bindings: module.value_referenced_import_bindings.clone(),
346        suppressions: module
347            .suppressions
348            .iter()
349            .map(|s| CachedSuppression {
350                line: s.line,
351                comment_line: s.comment_line,
352                kind: s
353                    .kind
354                    .map_or(0, crate::suppress::IssueKind::to_discriminant),
355            })
356            .collect(),
357        line_offsets: module.line_offsets.clone(),
358        complexity: module.complexity.clone(),
359        flag_uses: module.flag_uses.clone(),
360        class_heritage: module.class_heritage.clone(),
361        local_type_declarations: module
362            .local_type_declarations
363            .iter()
364            .map(|decl| CachedLocalTypeDeclaration {
365                name: decl.name.clone(),
366                span_start: decl.span.start,
367                span_end: decl.span.end,
368            })
369            .collect(),
370        public_signature_type_references: module
371            .public_signature_type_references
372            .iter()
373            .map(|reference| CachedPublicSignatureTypeReference {
374                export_name: reference.export_name.clone(),
375                type_name: reference.type_name.clone(),
376                span_start: reference.span.start,
377                span_end: reference.span.end,
378            })
379            .collect(),
380        namespace_object_aliases: module
381            .namespace_object_aliases
382            .iter()
383            .map(|alias| CachedNamespaceObjectAlias {
384                via_export_name: alias.via_export_name.clone(),
385                suffix: alias.suffix.clone(),
386                namespace_local: alias.namespace_local.clone(),
387            })
388            .collect(),
389    }
390}