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