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