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
46fn cached_exports_to_module(exports: &[CachedExport]) -> Vec<crate::ExportInfo> {
47    exports
48        .iter()
49        .map(|export| crate::ExportInfo {
50            name: if export.is_default {
51                ExportName::Default
52            } else {
53                ExportName::Named(export.name.clone())
54            },
55            local_name: export.local_name.clone(),
56            is_type_only: export.is_type_only,
57            is_side_effect_used: export.is_side_effect_used,
58            visibility: match export.visibility {
59                1 => VisibilityTag::Public,
60                2 => VisibilityTag::Internal,
61                3 => VisibilityTag::Beta,
62                4 => VisibilityTag::Alpha,
63                5 => VisibilityTag::ExpectedUnused,
64                _ => VisibilityTag::None,
65            },
66            span: Span::new(export.span_start, export.span_end),
67            members: export
68                .members
69                .iter()
70                .map(|member| crate::MemberInfo {
71                    name: member.name.clone(),
72                    kind: member.kind,
73                    span: Span::new(member.span_start, member.span_end),
74                    has_decorator: member.has_decorator,
75                    decorator_names: member.decorator_names.clone(),
76                    is_instance_returning_static: member.is_instance_returning_static,
77                    is_self_returning: member.is_self_returning,
78                })
79                .collect(),
80            super_class: export.super_class.clone(),
81        })
82        .collect()
83}
84
85fn cached_imports_to_module(imports: &[CachedImport]) -> Vec<crate::ImportInfo> {
86    imports
87        .iter()
88        .map(|import| crate::ImportInfo {
89            source: import.source.clone(),
90            imported_name: match import.kind {
91                IMPORT_KIND_DEFAULT => crate::ImportedName::Default,
92                IMPORT_KIND_NAMESPACE => crate::ImportedName::Namespace,
93                IMPORT_KIND_SIDE_EFFECT => crate::ImportedName::SideEffect,
94                _ => crate::ImportedName::Named(import.imported_name.clone()),
95            },
96            local_name: import.local_name.clone(),
97            is_type_only: import.is_type_only,
98            from_style: import.from_style,
99            span: Span::new(import.span_start, import.span_end),
100            source_span: Span::new(import.source_span_start, import.source_span_end),
101        })
102        .collect()
103}
104
105fn cached_re_exports_to_module(re_exports: &[CachedReExport]) -> Vec<crate::ReExportInfo> {
106    re_exports
107        .iter()
108        .map(|re_export| crate::ReExportInfo {
109            source: re_export.source.clone(),
110            imported_name: re_export.imported_name.clone(),
111            exported_name: re_export.exported_name.clone(),
112            is_type_only: re_export.is_type_only,
113            span: Span::new(re_export.span_start, re_export.span_end),
114        })
115        .collect()
116}
117
118fn cached_dynamic_imports_to_module(
119    dynamic_imports: &[CachedDynamicImport],
120) -> Vec<crate::DynamicImportInfo> {
121    dynamic_imports
122        .iter()
123        .map(|dynamic_import| crate::DynamicImportInfo {
124            source: dynamic_import.source.clone(),
125            span: Span::new(dynamic_import.span_start, dynamic_import.span_end),
126            destructured_names: dynamic_import.destructured_names.clone(),
127            local_name: dynamic_import.local_name.clone(),
128            is_speculative: dynamic_import.is_speculative,
129        })
130        .collect()
131}
132
133fn cached_require_calls_to_module(
134    require_calls: &[CachedRequireCall],
135) -> Vec<crate::RequireCallInfo> {
136    require_calls
137        .iter()
138        .map(|require_call| crate::RequireCallInfo {
139            source: require_call.source.clone(),
140            span: Span::new(require_call.span_start, require_call.span_end),
141            source_span: Span::new(require_call.source_span_start, require_call.source_span_end),
142            destructured_names: require_call.destructured_names.clone(),
143            local_name: require_call.local_name.clone(),
144        })
145        .collect()
146}
147
148fn cached_dynamic_patterns_to_module(
149    dynamic_import_patterns: &[CachedDynamicImportPattern],
150) -> Vec<crate::DynamicImportPattern> {
151    dynamic_import_patterns
152        .iter()
153        .map(|pattern| crate::DynamicImportPattern {
154            prefix: pattern.prefix.clone(),
155            suffix: pattern.suffix.clone(),
156            span: Span::new(pattern.span_start, pattern.span_end),
157        })
158        .collect()
159}
160
161fn cached_suppressions_to_module(
162    suppressions: &[CachedSuppression],
163) -> Vec<crate::suppress::Suppression> {
164    suppressions
165        .iter()
166        .map(|suppression| crate::suppress::Suppression {
167            line: suppression.line,
168            comment_line: suppression.comment_line,
169            kind: if suppression.kind == 0 {
170                None
171            } else {
172                crate::suppress::IssueKind::from_discriminant(suppression.kind)
173            },
174        })
175        .collect()
176}
177
178fn cached_unknown_suppressions_to_module(
179    unknown_suppression_kinds: &[CachedUnknownSuppressionKind],
180) -> Vec<fallow_types::suppress::UnknownSuppressionKind> {
181    unknown_suppression_kinds
182        .iter()
183        .map(|unknown| fallow_types::suppress::UnknownSuppressionKind {
184            comment_line: unknown.comment_line,
185            is_file_level: unknown.is_file_level,
186            token: unknown.token.clone(),
187        })
188        .collect()
189}
190
191fn cached_local_types_to_module(
192    local_type_declarations: &[CachedLocalTypeDeclaration],
193) -> Vec<crate::LocalTypeDeclaration> {
194    local_type_declarations
195        .iter()
196        .map(|declaration| crate::LocalTypeDeclaration {
197            name: declaration.name.clone(),
198            span: Span::new(declaration.span_start, declaration.span_end),
199        })
200        .collect()
201}
202
203fn cached_signature_refs_to_module(
204    public_signature_type_references: &[CachedPublicSignatureTypeReference],
205) -> Vec<crate::PublicSignatureTypeReference> {
206    public_signature_type_references
207        .iter()
208        .map(|reference| crate::PublicSignatureTypeReference {
209            export_name: reference.export_name.clone(),
210            type_name: reference.type_name.clone(),
211            span: Span::new(reference.span_start, reference.span_end),
212        })
213        .collect()
214}
215
216fn cached_namespace_aliases_to_module(
217    namespace_object_aliases: &[CachedNamespaceObjectAlias],
218) -> Vec<NamespaceObjectAlias> {
219    namespace_object_aliases
220        .iter()
221        .map(|alias| NamespaceObjectAlias {
222            via_export_name: alias.via_export_name.clone(),
223            suffix: alias.suffix.clone(),
224            namespace_local: alias.namespace_local.clone(),
225        })
226        .collect()
227}
228
229fn module_exports_to_cached(exports: &[crate::ExportInfo]) -> Vec<CachedExport> {
230    exports
231        .iter()
232        .map(|export| CachedExport {
233            name: match &export.name {
234                ExportName::Named(name) => name.clone(),
235                ExportName::Default => String::new(),
236            },
237            is_default: matches!(export.name, ExportName::Default),
238            is_type_only: export.is_type_only,
239            is_side_effect_used: export.is_side_effect_used,
240            visibility: export.visibility as u8,
241            local_name: export.local_name.clone(),
242            span_start: export.span.start,
243            span_end: export.span.end,
244            members: export
245                .members
246                .iter()
247                .map(|member| CachedMember {
248                    name: member.name.clone(),
249                    kind: member.kind,
250                    span_start: member.span.start,
251                    span_end: member.span.end,
252                    has_decorator: member.has_decorator,
253                    decorator_names: member.decorator_names.clone(),
254                    is_instance_returning_static: member.is_instance_returning_static,
255                    is_self_returning: member.is_self_returning,
256                })
257                .collect(),
258            super_class: export.super_class.clone(),
259        })
260        .collect()
261}
262
263fn module_imports_to_cached(imports: &[crate::ImportInfo]) -> Vec<CachedImport> {
264    imports
265        .iter()
266        .map(|import| {
267            let (kind, imported_name) = match &import.imported_name {
268                crate::ImportedName::Named(name) => (IMPORT_KIND_NAMED, name.clone()),
269                crate::ImportedName::Default => (IMPORT_KIND_DEFAULT, String::new()),
270                crate::ImportedName::Namespace => (IMPORT_KIND_NAMESPACE, String::new()),
271                crate::ImportedName::SideEffect => (IMPORT_KIND_SIDE_EFFECT, String::new()),
272            };
273            CachedImport {
274                source: import.source.clone(),
275                imported_name,
276                local_name: import.local_name.clone(),
277                is_type_only: import.is_type_only,
278                from_style: import.from_style,
279                kind,
280                span_start: import.span.start,
281                span_end: import.span.end,
282                source_span_start: import.source_span.start,
283                source_span_end: import.source_span.end,
284            }
285        })
286        .collect()
287}
288
289fn module_re_exports_to_cached(re_exports: &[crate::ReExportInfo]) -> Vec<CachedReExport> {
290    re_exports
291        .iter()
292        .map(|re_export| CachedReExport {
293            source: re_export.source.clone(),
294            imported_name: re_export.imported_name.clone(),
295            exported_name: re_export.exported_name.clone(),
296            is_type_only: re_export.is_type_only,
297            span_start: re_export.span.start,
298            span_end: re_export.span.end,
299        })
300        .collect()
301}
302
303fn module_dynamic_imports_to_cached(
304    dynamic_imports: &[crate::DynamicImportInfo],
305) -> Vec<CachedDynamicImport> {
306    dynamic_imports
307        .iter()
308        .map(|dynamic_import| CachedDynamicImport {
309            source: dynamic_import.source.clone(),
310            span_start: dynamic_import.span.start,
311            span_end: dynamic_import.span.end,
312            destructured_names: dynamic_import.destructured_names.clone(),
313            local_name: dynamic_import.local_name.clone(),
314            is_speculative: dynamic_import.is_speculative,
315        })
316        .collect()
317}
318
319fn module_require_calls_to_cached(
320    require_calls: &[crate::RequireCallInfo],
321) -> Vec<CachedRequireCall> {
322    require_calls
323        .iter()
324        .map(|require_call| CachedRequireCall {
325            source: require_call.source.clone(),
326            span_start: require_call.span.start,
327            span_end: require_call.span.end,
328            source_span_start: require_call.source_span.start,
329            source_span_end: require_call.source_span.end,
330            destructured_names: require_call.destructured_names.clone(),
331            local_name: require_call.local_name.clone(),
332        })
333        .collect()
334}
335
336fn module_dynamic_patterns_to_cached(
337    dynamic_import_patterns: &[crate::DynamicImportPattern],
338) -> Vec<CachedDynamicImportPattern> {
339    dynamic_import_patterns
340        .iter()
341        .map(|pattern| CachedDynamicImportPattern {
342            prefix: pattern.prefix.clone(),
343            suffix: pattern.suffix.clone(),
344            span_start: pattern.span.start,
345            span_end: pattern.span.end,
346        })
347        .collect()
348}
349
350fn module_suppressions_to_cached(
351    suppressions: &[crate::suppress::Suppression],
352) -> Vec<CachedSuppression> {
353    suppressions
354        .iter()
355        .map(|suppression| CachedSuppression {
356            line: suppression.line,
357            comment_line: suppression.comment_line,
358            kind: suppression
359                .kind
360                .map_or(0, crate::suppress::IssueKind::to_discriminant),
361        })
362        .collect()
363}
364
365fn module_unknown_suppressions_to_cached(
366    unknown_suppression_kinds: &[fallow_types::suppress::UnknownSuppressionKind],
367) -> Vec<CachedUnknownSuppressionKind> {
368    unknown_suppression_kinds
369        .iter()
370        .map(|unknown| CachedUnknownSuppressionKind {
371            comment_line: unknown.comment_line,
372            is_file_level: unknown.is_file_level,
373            token: unknown.token.clone(),
374        })
375        .collect()
376}
377
378fn module_local_types_to_cached(
379    local_type_declarations: &[crate::LocalTypeDeclaration],
380) -> Vec<CachedLocalTypeDeclaration> {
381    local_type_declarations
382        .iter()
383        .map(|declaration| CachedLocalTypeDeclaration {
384            name: declaration.name.clone(),
385            span_start: declaration.span.start,
386            span_end: declaration.span.end,
387        })
388        .collect()
389}
390
391fn module_signature_refs_to_cached(
392    public_signature_type_references: &[crate::PublicSignatureTypeReference],
393) -> Vec<CachedPublicSignatureTypeReference> {
394    public_signature_type_references
395        .iter()
396        .map(|reference| CachedPublicSignatureTypeReference {
397            export_name: reference.export_name.clone(),
398            type_name: reference.type_name.clone(),
399            span_start: reference.span.start,
400            span_end: reference.span.end,
401        })
402        .collect()
403}
404
405fn module_namespace_aliases_to_cached(
406    namespace_object_aliases: &[NamespaceObjectAlias],
407) -> Vec<CachedNamespaceObjectAlias> {
408    namespace_object_aliases
409        .iter()
410        .map(|alias| CachedNamespaceObjectAlias {
411            via_export_name: alias.via_export_name.clone(),
412            suffix: alias.suffix.clone(),
413            namespace_local: alias.namespace_local.clone(),
414        })
415        .collect()
416}
417
418/// Reconstruct a [`ModuleInfo`](crate::ModuleInfo) from a [`CachedModule`], skipping
419/// the per-function complexity vec when `need_complexity` is `false`. Avoids the
420/// `Vec<FunctionComplexity>` clone on warm runs of commands (e.g. `fallow dead-code`)
421/// that don't consume complexity, which adds up across tens of thousands of files.
422#[must_use]
423pub fn cached_to_module_opts(
424    cached: &CachedModule,
425    file_id: fallow_types::discover::FileId,
426    need_complexity: bool,
427) -> crate::ModuleInfo {
428    crate::ModuleInfo {
429        file_id,
430        exports: cached_exports_to_module(&cached.exports),
431        imports: cached_imports_to_module(&cached.imports),
432        re_exports: cached_re_exports_to_module(&cached.re_exports),
433        dynamic_imports: cached_dynamic_imports_to_module(&cached.dynamic_imports),
434        dynamic_import_patterns: cached_dynamic_patterns_to_module(&cached.dynamic_import_patterns),
435        require_calls: cached_require_calls_to_module(&cached.require_calls),
436        package_path_references: cached.package_path_references.clone(),
437        member_accesses: cached.member_accesses.clone(),
438        whole_object_uses: cached.whole_object_uses.clone(),
439        has_cjs_exports: cached.has_cjs_exports,
440        has_angular_component_template_url: cached.has_angular_component_template_url,
441        content_hash: cached.content_hash,
442        suppressions: cached_suppressions_to_module(&cached.suppressions),
443        unknown_suppression_kinds: cached_unknown_suppressions_to_module(
444            &cached.unknown_suppression_kinds,
445        ),
446        unused_import_bindings: cached.unused_import_bindings.clone(),
447        type_referenced_import_bindings: cached.type_referenced_import_bindings.clone(),
448        value_referenced_import_bindings: cached.value_referenced_import_bindings.clone(),
449        line_offsets: cached.line_offsets.clone(),
450        complexity: if need_complexity {
451            cached.complexity.clone()
452        } else {
453            Vec::new()
454        },
455        flag_uses: cached.flag_uses.clone(),
456        class_heritage: cached.class_heritage.clone(),
457        injection_tokens: cached.injection_tokens.clone(),
458        local_type_declarations: cached_local_types_to_module(&cached.local_type_declarations),
459        public_signature_type_references: cached_signature_refs_to_module(
460            &cached.public_signature_type_references,
461        ),
462        namespace_object_aliases: cached_namespace_aliases_to_module(
463            &cached.namespace_object_aliases,
464        ),
465        iconify_prefixes: cached.iconify_prefixes.clone(),
466        iconify_icon_names: cached.iconify_icon_names.clone(),
467        auto_import_candidates: cached.auto_import_candidates.clone(),
468        directives: cached.directives.clone(),
469        security_sinks: cached.security_sinks.clone(),
470        security_sinks_skipped: cached.security_sinks_skipped,
471        security_unresolved_callee_sites: cached.security_unresolved_callee_sites.clone(),
472        tainted_bindings: cached.tainted_bindings.clone(),
473        sanitized_sink_args: cached.sanitized_sink_args.clone(),
474        security_control_sites: cached.security_control_sites.clone(),
475        callee_uses: cached.callee_uses.clone(),
476    }
477}
478
479/// Convert a [`ModuleInfo`](crate::ModuleInfo) to a [`CachedModule`] for storage.
480///
481/// `mtime_secs` and `file_size` come from `std::fs::metadata()` at parse time
482/// and enable fast cache validation on subsequent runs (skip file read when
483/// mtime+size match).
484#[must_use]
485pub fn module_to_cached(
486    module: &crate::ModuleInfo,
487    mtime_secs: u64,
488    file_size: u64,
489) -> CachedModule {
490    CachedModule {
491        content_hash: module.content_hash,
492        mtime_secs,
493        file_size,
494        last_access_secs: current_unix_seconds(),
495        exports: module_exports_to_cached(&module.exports),
496        imports: module_imports_to_cached(&module.imports),
497        re_exports: module_re_exports_to_cached(&module.re_exports),
498        dynamic_imports: module_dynamic_imports_to_cached(&module.dynamic_imports),
499        require_calls: module_require_calls_to_cached(&module.require_calls),
500        package_path_references: module.package_path_references.clone(),
501        member_accesses: module.member_accesses.clone(),
502        whole_object_uses: module.whole_object_uses.clone(),
503        dynamic_import_patterns: module_dynamic_patterns_to_cached(&module.dynamic_import_patterns),
504        has_cjs_exports: module.has_cjs_exports,
505        has_angular_component_template_url: module.has_angular_component_template_url,
506        unused_import_bindings: module.unused_import_bindings.clone(),
507        type_referenced_import_bindings: module.type_referenced_import_bindings.clone(),
508        value_referenced_import_bindings: module.value_referenced_import_bindings.clone(),
509        suppressions: module_suppressions_to_cached(&module.suppressions),
510        unknown_suppression_kinds: module_unknown_suppressions_to_cached(
511            &module.unknown_suppression_kinds,
512        ),
513        line_offsets: module.line_offsets.clone(),
514        complexity: module.complexity.clone(),
515        flag_uses: module.flag_uses.clone(),
516        class_heritage: module.class_heritage.clone(),
517        injection_tokens: module.injection_tokens.clone(),
518        local_type_declarations: module_local_types_to_cached(&module.local_type_declarations),
519        public_signature_type_references: module_signature_refs_to_cached(
520            &module.public_signature_type_references,
521        ),
522        namespace_object_aliases: module_namespace_aliases_to_cached(
523            &module.namespace_object_aliases,
524        ),
525        iconify_prefixes: module.iconify_prefixes.clone(),
526        iconify_icon_names: module.iconify_icon_names.clone(),
527        auto_import_candidates: module.auto_import_candidates.clone(),
528        directives: module.directives.clone(),
529        security_sinks: module.security_sinks.clone(),
530        security_sinks_skipped: module.security_sinks_skipped,
531        security_unresolved_callee_sites: module.security_unresolved_callee_sites.clone(),
532        tainted_bindings: module.tainted_bindings.clone(),
533        sanitized_sink_args: module.sanitized_sink_args.clone(),
534        security_control_sites: module.security_control_sites.clone(),
535        callee_uses: module.callee_uses.clone(),
536    }
537}