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