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