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 std::time::{SystemTime, UNIX_EPOCH};
11
12use oxc_span::Span;
13
14use crate::ExportName;
15use fallow_types::extract::{NamespaceObjectAlias, VisibilityTag};
16
17/// Seconds-since-Unix-epoch from the wall clock, saturating to 0 if the
18/// system clock is set before the epoch. Used as the LRU bookkeeping
19/// timestamp on `CachedModule.last_access_secs`. Wall-clock (not monotonic)
20/// is the right source here because the value persists across process
21/// invocations.
22#[must_use]
23pub fn current_unix_seconds() -> u64 {
24    SystemTime::now()
25        .duration_since(UNIX_EPOCH)
26        .map_or(0, |d| d.as_secs())
27}
28
29use super::types::{
30    CachedDynamicImport, CachedDynamicImportPattern, CachedExport, CachedImport,
31    CachedLocalTypeDeclaration, CachedMember, CachedModule, CachedNamespaceObjectAlias,
32    CachedPublicSignatureTypeReference, CachedReExport, CachedRequireCall, CachedSuppression,
33    CachedUnknownSuppressionKind, IMPORT_KIND_DEFAULT, IMPORT_KIND_NAMED, IMPORT_KIND_NAMESPACE,
34    IMPORT_KIND_SIDE_EFFECT,
35};
36
37/// Reconstruct a [`ModuleInfo`](crate::ModuleInfo) from a [`CachedModule`].
38#[must_use]
39pub fn cached_to_module(
40    cached: &CachedModule,
41    file_id: fallow_types::discover::FileId,
42) -> crate::ModuleInfo {
43    cached_to_module_opts(cached, file_id, true)
44}
45
46/// Reconstruct a [`ModuleInfo`](crate::ModuleInfo) from a [`CachedModule`], skipping
47/// the per-function complexity vec when `need_complexity` is `false`. Avoids the
48/// `Vec<FunctionComplexity>` clone on warm runs of commands (e.g. `fallow dead-code`)
49/// that don't consume complexity, which adds up across tens of thousands of files.
50#[must_use]
51#[expect(
52    clippy::too_many_lines,
53    reason = "single flat field-by-field deserialization; splitting it harms readability"
54)]
55pub fn cached_to_module_opts(
56    cached: &CachedModule,
57    file_id: fallow_types::discover::FileId,
58    need_complexity: bool,
59) -> crate::ModuleInfo {
60    use crate::{
61        DynamicImportInfo, ExportInfo, ImportInfo, ImportedName, LocalTypeDeclaration, MemberInfo,
62        ModuleInfo, PublicSignatureTypeReference, ReExportInfo, RequireCallInfo,
63    };
64
65    let exports = cached
66        .exports
67        .iter()
68        .map(|e| ExportInfo {
69            name: if e.is_default {
70                ExportName::Default
71            } else {
72                ExportName::Named(e.name.clone())
73            },
74            local_name: e.local_name.clone(),
75            is_type_only: e.is_type_only,
76            is_side_effect_used: e.is_side_effect_used,
77            visibility: match e.visibility {
78                1 => VisibilityTag::Public,
79                2 => VisibilityTag::Internal,
80                3 => VisibilityTag::Beta,
81                4 => VisibilityTag::Alpha,
82                5 => VisibilityTag::ExpectedUnused,
83                _ => VisibilityTag::None,
84            },
85            span: Span::new(e.span_start, e.span_end),
86            members: e
87                .members
88                .iter()
89                .map(|m| MemberInfo {
90                    name: m.name.clone(),
91                    kind: m.kind,
92                    span: Span::new(m.span_start, m.span_end),
93                    has_decorator: m.has_decorator,
94                    decorator_names: m.decorator_names.clone(),
95                    is_instance_returning_static: m.is_instance_returning_static,
96                    is_self_returning: m.is_self_returning,
97                })
98                .collect(),
99            super_class: e.super_class.clone(),
100        })
101        .collect();
102
103    let imports = cached
104        .imports
105        .iter()
106        .map(|i| ImportInfo {
107            source: i.source.clone(),
108            imported_name: match i.kind {
109                IMPORT_KIND_DEFAULT => ImportedName::Default,
110                IMPORT_KIND_NAMESPACE => ImportedName::Namespace,
111                IMPORT_KIND_SIDE_EFFECT => ImportedName::SideEffect,
112                // IMPORT_KIND_NAMED (0) and any unknown value default to Named
113                _ => ImportedName::Named(i.imported_name.clone()),
114            },
115            local_name: i.local_name.clone(),
116            is_type_only: i.is_type_only,
117            from_style: i.from_style,
118            span: Span::new(i.span_start, i.span_end),
119            source_span: Span::new(i.source_span_start, i.source_span_end),
120        })
121        .collect();
122
123    let re_exports = cached
124        .re_exports
125        .iter()
126        .map(|r| ReExportInfo {
127            source: r.source.clone(),
128            imported_name: r.imported_name.clone(),
129            exported_name: r.exported_name.clone(),
130            is_type_only: r.is_type_only,
131            span: Span::new(r.span_start, r.span_end),
132        })
133        .collect();
134
135    let dynamic_imports = cached
136        .dynamic_imports
137        .iter()
138        .map(|d| DynamicImportInfo {
139            source: d.source.clone(),
140            span: Span::new(d.span_start, d.span_end),
141            destructured_names: d.destructured_names.clone(),
142            local_name: d.local_name.clone(),
143            is_speculative: d.is_speculative,
144        })
145        .collect();
146
147    let require_calls = cached
148        .require_calls
149        .iter()
150        .map(|r| RequireCallInfo {
151            source: r.source.clone(),
152            span: Span::new(r.span_start, r.span_end),
153            source_span: Span::new(r.source_span_start, r.source_span_end),
154            destructured_names: r.destructured_names.clone(),
155            local_name: r.local_name.clone(),
156        })
157        .collect();
158
159    let dynamic_import_patterns = cached
160        .dynamic_import_patterns
161        .iter()
162        .map(|p| crate::DynamicImportPattern {
163            prefix: p.prefix.clone(),
164            suffix: p.suffix.clone(),
165            span: Span::new(p.span_start, p.span_end),
166        })
167        .collect();
168
169    let suppressions = cached
170        .suppressions
171        .iter()
172        .map(|s| crate::suppress::Suppression {
173            line: s.line,
174            comment_line: s.comment_line,
175            kind: if s.kind == 0 {
176                None
177            } else {
178                crate::suppress::IssueKind::from_discriminant(s.kind)
179            },
180        })
181        .collect();
182
183    let unknown_suppression_kinds = cached
184        .unknown_suppression_kinds
185        .iter()
186        .map(|u| fallow_types::suppress::UnknownSuppressionKind {
187            comment_line: u.comment_line,
188            is_file_level: u.is_file_level,
189            token: u.token.clone(),
190        })
191        .collect();
192
193    ModuleInfo {
194        file_id,
195        exports,
196        imports,
197        re_exports,
198        dynamic_imports,
199        dynamic_import_patterns,
200        require_calls,
201        package_path_references: cached.package_path_references.clone(),
202        member_accesses: cached.member_accesses.clone(),
203        whole_object_uses: cached.whole_object_uses.clone(),
204        has_cjs_exports: cached.has_cjs_exports,
205        has_angular_component_template_url: cached.has_angular_component_template_url,
206        content_hash: cached.content_hash,
207        suppressions,
208        unknown_suppression_kinds,
209        unused_import_bindings: cached.unused_import_bindings.clone(),
210        type_referenced_import_bindings: cached.type_referenced_import_bindings.clone(),
211        value_referenced_import_bindings: cached.value_referenced_import_bindings.clone(),
212        line_offsets: cached.line_offsets.clone(),
213        complexity: if need_complexity {
214            cached.complexity.clone()
215        } else {
216            Vec::new()
217        },
218        flag_uses: cached.flag_uses.clone(),
219        class_heritage: cached.class_heritage.clone(),
220        injection_tokens: cached.injection_tokens.clone(),
221        local_type_declarations: cached
222            .local_type_declarations
223            .iter()
224            .map(|decl| LocalTypeDeclaration {
225                name: decl.name.clone(),
226                span: Span::new(decl.span_start, decl.span_end),
227            })
228            .collect(),
229        public_signature_type_references: cached
230            .public_signature_type_references
231            .iter()
232            .map(|reference| PublicSignatureTypeReference {
233                export_name: reference.export_name.clone(),
234                type_name: reference.type_name.clone(),
235                span: Span::new(reference.span_start, reference.span_end),
236            })
237            .collect(),
238        namespace_object_aliases: cached
239            .namespace_object_aliases
240            .iter()
241            .map(|alias| NamespaceObjectAlias {
242                via_export_name: alias.via_export_name.clone(),
243                suffix: alias.suffix.clone(),
244                namespace_local: alias.namespace_local.clone(),
245            })
246            .collect(),
247        iconify_prefixes: cached.iconify_prefixes.clone(),
248        iconify_icon_names: cached.iconify_icon_names.clone(),
249        auto_import_candidates: cached.auto_import_candidates.clone(),
250        directives: cached.directives.clone(),
251        security_sinks: cached.security_sinks.clone(),
252        security_sinks_skipped: cached.security_sinks_skipped,
253        security_unresolved_callee_sites: cached.security_unresolved_callee_sites.clone(),
254        tainted_bindings: cached.tainted_bindings.clone(),
255        sanitized_sink_args: cached.sanitized_sink_args.clone(),
256        security_control_sites: cached.security_control_sites.clone(),
257        callee_uses: cached.callee_uses.clone(),
258    }
259}
260
261/// Convert a [`ModuleInfo`](crate::ModuleInfo) to a [`CachedModule`] for storage.
262///
263/// `mtime_secs` and `file_size` come from `std::fs::metadata()` at parse time
264/// and enable fast cache validation on subsequent runs (skip file read when
265/// mtime+size match).
266#[must_use]
267#[expect(
268    clippy::too_many_lines,
269    reason = "single flat field-by-field serialization; splitting it harms readability"
270)]
271pub fn module_to_cached(
272    module: &crate::ModuleInfo,
273    mtime_secs: u64,
274    file_size: u64,
275) -> CachedModule {
276    CachedModule {
277        content_hash: module.content_hash,
278        mtime_secs,
279        file_size,
280        last_access_secs: current_unix_seconds(),
281        exports: module
282            .exports
283            .iter()
284            .map(|e| CachedExport {
285                name: match &e.name {
286                    ExportName::Named(n) => n.clone(),
287                    ExportName::Default => String::new(),
288                },
289                is_default: matches!(e.name, ExportName::Default),
290                is_type_only: e.is_type_only,
291                is_side_effect_used: e.is_side_effect_used,
292                visibility: e.visibility as u8,
293                local_name: e.local_name.clone(),
294                span_start: e.span.start,
295                span_end: e.span.end,
296                members: e
297                    .members
298                    .iter()
299                    .map(|m| CachedMember {
300                        name: m.name.clone(),
301                        kind: m.kind,
302                        span_start: m.span.start,
303                        span_end: m.span.end,
304                        has_decorator: m.has_decorator,
305                        decorator_names: m.decorator_names.clone(),
306                        is_instance_returning_static: m.is_instance_returning_static,
307                        is_self_returning: m.is_self_returning,
308                    })
309                    .collect(),
310                super_class: e.super_class.clone(),
311            })
312            .collect(),
313        imports: module
314            .imports
315            .iter()
316            .map(|i| {
317                let (kind, imported_name) = match &i.imported_name {
318                    crate::ImportedName::Named(n) => (IMPORT_KIND_NAMED, n.clone()),
319                    crate::ImportedName::Default => (IMPORT_KIND_DEFAULT, String::new()),
320                    crate::ImportedName::Namespace => (IMPORT_KIND_NAMESPACE, String::new()),
321                    crate::ImportedName::SideEffect => (IMPORT_KIND_SIDE_EFFECT, String::new()),
322                };
323                CachedImport {
324                    source: i.source.clone(),
325                    imported_name,
326                    local_name: i.local_name.clone(),
327                    is_type_only: i.is_type_only,
328                    from_style: i.from_style,
329                    kind,
330                    span_start: i.span.start,
331                    span_end: i.span.end,
332                    source_span_start: i.source_span.start,
333                    source_span_end: i.source_span.end,
334                }
335            })
336            .collect(),
337        re_exports: module
338            .re_exports
339            .iter()
340            .map(|r| CachedReExport {
341                source: r.source.clone(),
342                imported_name: r.imported_name.clone(),
343                exported_name: r.exported_name.clone(),
344                is_type_only: r.is_type_only,
345                span_start: r.span.start,
346                span_end: r.span.end,
347            })
348            .collect(),
349        dynamic_imports: module
350            .dynamic_imports
351            .iter()
352            .map(|d| CachedDynamicImport {
353                source: d.source.clone(),
354                span_start: d.span.start,
355                span_end: d.span.end,
356                destructured_names: d.destructured_names.clone(),
357                local_name: d.local_name.clone(),
358                is_speculative: d.is_speculative,
359            })
360            .collect(),
361        require_calls: module
362            .require_calls
363            .iter()
364            .map(|r| CachedRequireCall {
365                source: r.source.clone(),
366                span_start: r.span.start,
367                span_end: r.span.end,
368                source_span_start: r.source_span.start,
369                source_span_end: r.source_span.end,
370                destructured_names: r.destructured_names.clone(),
371                local_name: r.local_name.clone(),
372            })
373            .collect(),
374        package_path_references: module.package_path_references.clone(),
375        member_accesses: module.member_accesses.clone(),
376        whole_object_uses: module.whole_object_uses.clone(),
377        dynamic_import_patterns: module
378            .dynamic_import_patterns
379            .iter()
380            .map(|p| CachedDynamicImportPattern {
381                prefix: p.prefix.clone(),
382                suffix: p.suffix.clone(),
383                span_start: p.span.start,
384                span_end: p.span.end,
385            })
386            .collect(),
387        has_cjs_exports: module.has_cjs_exports,
388        has_angular_component_template_url: module.has_angular_component_template_url,
389        unused_import_bindings: module.unused_import_bindings.clone(),
390        type_referenced_import_bindings: module.type_referenced_import_bindings.clone(),
391        value_referenced_import_bindings: module.value_referenced_import_bindings.clone(),
392        suppressions: module
393            .suppressions
394            .iter()
395            .map(|s| CachedSuppression {
396                line: s.line,
397                comment_line: s.comment_line,
398                kind: s
399                    .kind
400                    .map_or(0, crate::suppress::IssueKind::to_discriminant),
401            })
402            .collect(),
403        unknown_suppression_kinds: module
404            .unknown_suppression_kinds
405            .iter()
406            .map(|u| CachedUnknownSuppressionKind {
407                comment_line: u.comment_line,
408                is_file_level: u.is_file_level,
409                token: u.token.clone(),
410            })
411            .collect(),
412        line_offsets: module.line_offsets.clone(),
413        complexity: module.complexity.clone(),
414        flag_uses: module.flag_uses.clone(),
415        class_heritage: module.class_heritage.clone(),
416        injection_tokens: module.injection_tokens.clone(),
417        local_type_declarations: module
418            .local_type_declarations
419            .iter()
420            .map(|decl| CachedLocalTypeDeclaration {
421                name: decl.name.clone(),
422                span_start: decl.span.start,
423                span_end: decl.span.end,
424            })
425            .collect(),
426        public_signature_type_references: module
427            .public_signature_type_references
428            .iter()
429            .map(|reference| CachedPublicSignatureTypeReference {
430                export_name: reference.export_name.clone(),
431                type_name: reference.type_name.clone(),
432                span_start: reference.span.start,
433                span_end: reference.span.end,
434            })
435            .collect(),
436        namespace_object_aliases: module
437            .namespace_object_aliases
438            .iter()
439            .map(|alias| CachedNamespaceObjectAlias {
440                via_export_name: alias.via_export_name.clone(),
441                suffix: alias.suffix.clone(),
442                namespace_local: alias.namespace_local.clone(),
443            })
444            .collect(),
445        iconify_prefixes: module.iconify_prefixes.clone(),
446        iconify_icon_names: module.iconify_icon_names.clone(),
447        auto_import_candidates: module.auto_import_candidates.clone(),
448        directives: module.directives.clone(),
449        security_sinks: module.security_sinks.clone(),
450        security_sinks_skipped: module.security_sinks_skipped,
451        security_unresolved_callee_sites: module.security_unresolved_callee_sites.clone(),
452        tainted_bindings: module.tainted_bindings.clone(),
453        sanitized_sink_args: module.sanitized_sink_args.clone(),
454        security_control_sites: module.security_control_sites.clone(),
455        callee_uses: module.callee_uses.clone(),
456    }
457}