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::sync::Arc;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use oxc_span::Span;
14
15use crate::ExportName;
16use fallow_types::extract::{NamespaceObjectAlias, VisibilityTag};
17use fallow_types::suppress::{PolicyRuleSuppression, SuppressionTarget};
18
19/// Seconds-since-Unix-epoch from the wall clock, saturating to 0 if the
20/// system clock is set before the epoch. Used as the LRU bookkeeping
21/// timestamp on `CachedModule.last_seen_secs`. Wall-clock (not monotonic)
22/// is the right source here because the value persists across process
23/// invocations.
24#[must_use]
25pub fn current_unix_seconds() -> u64 {
26    SystemTime::now()
27        .duration_since(UNIX_EPOCH)
28        .map_or(0, |d| d.as_secs())
29}
30
31use super::types::{
32    CachedDynamicImport, CachedDynamicImportPattern, CachedExport, CachedImport,
33    CachedLocalTypeDeclaration, CachedMember, CachedModule, CachedNamespaceObjectAlias,
34    CachedPublicSignatureTypeReference, CachedReExport, CachedRequireCall, CachedSuppression,
35    CachedUnknownSuppressionKind, IMPORT_KIND_DEFAULT, IMPORT_KIND_NAMED, IMPORT_KIND_NAMESPACE,
36    IMPORT_KIND_SIDE_EFFECT,
37};
38
39/// Reconstruct a [`ModuleInfo`](crate::ModuleInfo) from a [`CachedModule`].
40#[must_use]
41pub fn cached_to_module(
42    cached: &CachedModule,
43    file_id: fallow_types::discover::FileId,
44) -> crate::ModuleInfo {
45    cached_to_module_opts(cached, file_id, true)
46}
47
48fn cached_exports_to_module(exports: &[CachedExport]) -> Vec<crate::ExportInfo> {
49    exports
50        .iter()
51        .map(|export| crate::ExportInfo {
52            name: if export.is_default {
53                ExportName::Default
54            } else {
55                ExportName::Named(export.name.clone())
56            },
57            local_name: export.local_name.clone(),
58            is_type_only: export.is_type_only,
59            is_side_effect_used: export.is_side_effect_used,
60            visibility: match export.visibility {
61                1 => VisibilityTag::Public,
62                2 => VisibilityTag::Internal,
63                3 => VisibilityTag::Beta,
64                4 => VisibilityTag::Alpha,
65                5 => VisibilityTag::ExpectedUnused,
66                _ => VisibilityTag::None,
67            },
68            expected_unused_reason: export.expected_unused_reason.clone(),
69            deprecated: export.deprecated,
70            deprecated_reason: export.deprecated_reason.clone(),
71            span: Span::new(export.span_start, export.span_end),
72            members: export
73                .members
74                .iter()
75                .map(|member| crate::MemberInfo {
76                    name: member.name.clone(),
77                    kind: member.kind,
78                    span: Span::new(member.span_start, member.span_end),
79                    has_decorator: member.has_decorator,
80                    decorator_names: member.decorator_names.clone(),
81                    is_instance_returning_static: member.is_instance_returning_static,
82                    is_self_returning: member.is_self_returning,
83                    override_base: member.override_base.clone(),
84                })
85                .collect(),
86            super_class: export.super_class.clone(),
87        })
88        .collect()
89}
90
91fn cached_imports_to_module(imports: &[CachedImport]) -> Vec<crate::ImportInfo> {
92    imports
93        .iter()
94        .map(|import| crate::ImportInfo {
95            source: import.source.clone(),
96            imported_name: match import.kind {
97                IMPORT_KIND_DEFAULT => crate::ImportedName::Default,
98                IMPORT_KIND_NAMESPACE => crate::ImportedName::Namespace,
99                IMPORT_KIND_SIDE_EFFECT => crate::ImportedName::SideEffect,
100                _ => crate::ImportedName::Named(import.imported_name.clone()),
101            },
102            local_name: import.local_name.clone(),
103            is_type_only: import.is_type_only,
104            is_type_only_star: import.is_type_only_star,
105            from_style: import.from_style,
106            span: Span::new(import.span_start, import.span_end),
107            source_span: Span::new(import.source_span_start, import.source_span_end),
108        })
109        .collect()
110}
111
112fn cached_re_exports_to_module(re_exports: &[CachedReExport]) -> Vec<crate::ReExportInfo> {
113    re_exports
114        .iter()
115        .map(|re_export| crate::ReExportInfo {
116            source: re_export.source.clone(),
117            imported_name: re_export.imported_name.clone(),
118            exported_name: re_export.exported_name.clone(),
119            is_type_only: re_export.is_type_only,
120            span: Span::new(re_export.span_start, re_export.span_end),
121            statement_span: Span::new(re_export.statement_span_start, re_export.statement_span_end),
122            source_span: Span::new(re_export.source_span_start, re_export.source_span_end),
123        })
124        .collect()
125}
126
127fn cached_dynamic_imports_to_module(
128    dynamic_imports: &[CachedDynamicImport],
129) -> Vec<crate::DynamicImportInfo> {
130    dynamic_imports
131        .iter()
132        .map(|dynamic_import| crate::DynamicImportInfo {
133            source: dynamic_import.source.clone(),
134            span: Span::new(dynamic_import.span_start, dynamic_import.span_end),
135            destructured_names: dynamic_import.destructured_names.clone(),
136            local_name: dynamic_import.local_name.clone(),
137            is_speculative: dynamic_import.is_speculative,
138            value_escapes: dynamic_import.value_escapes,
139        })
140        .collect()
141}
142
143fn cached_require_calls_to_module(
144    require_calls: &[CachedRequireCall],
145) -> Vec<crate::RequireCallInfo> {
146    require_calls
147        .iter()
148        .map(|require_call| crate::RequireCallInfo {
149            source: require_call.source.clone(),
150            span: Span::new(require_call.span_start, require_call.span_end),
151            source_span: Span::new(require_call.source_span_start, require_call.source_span_end),
152            destructured_names: require_call.destructured_names.clone(),
153            local_name: require_call.local_name.clone(),
154            is_type_only: require_call.is_type_only,
155        })
156        .collect()
157}
158
159fn cached_dynamic_patterns_to_module(
160    dynamic_import_patterns: &[CachedDynamicImportPattern],
161) -> Vec<crate::DynamicImportPattern> {
162    dynamic_import_patterns
163        .iter()
164        .map(|pattern| crate::DynamicImportPattern {
165            prefix: pattern.prefix.clone(),
166            suffix: pattern.suffix.clone(),
167            excludes: pattern.excludes.clone(),
168            span: Span::new(pattern.span_start, pattern.span_end),
169            mechanism: pattern.mechanism,
170        })
171        .collect()
172}
173
174fn cached_suppressions_to_module(
175    suppressions: &[CachedSuppression],
176) -> Vec<crate::suppress::Suppression> {
177    suppressions
178        .iter()
179        .map(|suppression| {
180            let target = if suppression.kind == 0 {
181                None
182            } else if suppression.kind
183                == crate::suppress::IssueKind::PolicyViolation.to_discriminant()
184                && !suppression.policy_pack.is_empty()
185                && !suppression.policy_rule_id.is_empty()
186            {
187                Some(SuppressionTarget::PolicyRule(PolicyRuleSuppression::new(
188                    suppression.policy_pack.clone(),
189                    suppression.policy_rule_id.clone(),
190                )))
191            } else {
192                crate::suppress::IssueKind::from_discriminant(suppression.kind)
193                    .map(SuppressionTarget::Issue)
194            };
195            crate::suppress::Suppression {
196                line: suppression.line,
197                comment_line: suppression.comment_line,
198                target,
199                reason: suppression.reason.clone(),
200            }
201        })
202        .collect()
203}
204
205fn cached_unknown_suppressions_to_module(
206    unknown_suppression_kinds: &[CachedUnknownSuppressionKind],
207) -> Vec<fallow_types::suppress::UnknownSuppressionKind> {
208    unknown_suppression_kinds
209        .iter()
210        .map(|unknown| fallow_types::suppress::UnknownSuppressionKind {
211            comment_line: unknown.comment_line,
212            is_file_level: unknown.is_file_level,
213            token: unknown.token.clone(),
214            reason: unknown.reason.clone(),
215        })
216        .collect()
217}
218
219fn cached_local_types_to_module(
220    local_type_declarations: &[CachedLocalTypeDeclaration],
221) -> Vec<crate::LocalTypeDeclaration> {
222    local_type_declarations
223        .iter()
224        .map(|declaration| crate::LocalTypeDeclaration {
225            name: declaration.name.clone(),
226            span: Span::new(declaration.span_start, declaration.span_end),
227        })
228        .collect()
229}
230
231fn cached_signature_refs_to_module(
232    public_signature_type_references: &[CachedPublicSignatureTypeReference],
233) -> Vec<crate::PublicSignatureTypeReference> {
234    public_signature_type_references
235        .iter()
236        .map(|reference| crate::PublicSignatureTypeReference {
237            export_name: reference.export_name.clone(),
238            type_name: reference.type_name.clone(),
239            span: Span::new(reference.span_start, reference.span_end),
240            from_satisfies: reference.from_satisfies,
241        })
242        .collect()
243}
244
245fn cached_namespace_aliases_to_module(
246    namespace_object_aliases: &[CachedNamespaceObjectAlias],
247) -> Vec<NamespaceObjectAlias> {
248    namespace_object_aliases
249        .iter()
250        .map(|alias| NamespaceObjectAlias {
251            via_export_name: alias.via_export_name.clone(),
252            suffix: alias.suffix.clone(),
253            namespace_local: alias.namespace_local.clone(),
254        })
255        .collect()
256}
257
258fn module_exports_to_cached(exports: &[crate::ExportInfo]) -> Vec<CachedExport> {
259    exports
260        .iter()
261        .map(|export| CachedExport {
262            name: match &export.name {
263                ExportName::Named(name) => name.clone(),
264                ExportName::Default => String::new(),
265            },
266            is_default: matches!(export.name, ExportName::Default),
267            is_type_only: export.is_type_only,
268            is_side_effect_used: export.is_side_effect_used,
269            visibility: export.visibility as u8,
270            expected_unused_reason: export.expected_unused_reason.clone(),
271            deprecated: export.deprecated,
272            deprecated_reason: export.deprecated_reason.clone(),
273            local_name: export.local_name.clone(),
274            span_start: export.span.start,
275            span_end: export.span.end,
276            members: export
277                .members
278                .iter()
279                .map(|member| CachedMember {
280                    name: member.name.clone(),
281                    kind: member.kind,
282                    span_start: member.span.start,
283                    span_end: member.span.end,
284                    has_decorator: member.has_decorator,
285                    decorator_names: member.decorator_names.clone(),
286                    is_instance_returning_static: member.is_instance_returning_static,
287                    is_self_returning: member.is_self_returning,
288                    override_base: member.override_base.clone(),
289                })
290                .collect(),
291            super_class: export.super_class.clone(),
292        })
293        .collect()
294}
295
296fn module_imports_to_cached(imports: &[crate::ImportInfo]) -> Vec<CachedImport> {
297    imports
298        .iter()
299        .map(|import| {
300            let (kind, imported_name) = match &import.imported_name {
301                crate::ImportedName::Named(name) => (IMPORT_KIND_NAMED, name.clone()),
302                crate::ImportedName::Default => (IMPORT_KIND_DEFAULT, String::new()),
303                crate::ImportedName::Namespace => (IMPORT_KIND_NAMESPACE, String::new()),
304                crate::ImportedName::SideEffect => (IMPORT_KIND_SIDE_EFFECT, String::new()),
305            };
306            CachedImport {
307                source: import.source.clone(),
308                imported_name,
309                local_name: import.local_name.clone(),
310                is_type_only: import.is_type_only,
311                is_type_only_star: import.is_type_only_star,
312                from_style: import.from_style,
313                kind,
314                span_start: import.span.start,
315                span_end: import.span.end,
316                source_span_start: import.source_span.start,
317                source_span_end: import.source_span.end,
318            }
319        })
320        .collect()
321}
322
323fn module_re_exports_to_cached(re_exports: &[crate::ReExportInfo]) -> Vec<CachedReExport> {
324    re_exports
325        .iter()
326        .map(|re_export| CachedReExport {
327            source: re_export.source.clone(),
328            imported_name: re_export.imported_name.clone(),
329            exported_name: re_export.exported_name.clone(),
330            is_type_only: re_export.is_type_only,
331            span_start: re_export.span.start,
332            span_end: re_export.span.end,
333            statement_span_start: re_export.statement_span.start,
334            statement_span_end: re_export.statement_span.end,
335            source_span_start: re_export.source_span.start,
336            source_span_end: re_export.source_span.end,
337        })
338        .collect()
339}
340
341fn module_dynamic_imports_to_cached(
342    dynamic_imports: &[crate::DynamicImportInfo],
343) -> Vec<CachedDynamicImport> {
344    dynamic_imports
345        .iter()
346        .map(|dynamic_import| CachedDynamicImport {
347            source: dynamic_import.source.clone(),
348            span_start: dynamic_import.span.start,
349            span_end: dynamic_import.span.end,
350            destructured_names: dynamic_import.destructured_names.clone(),
351            local_name: dynamic_import.local_name.clone(),
352            is_speculative: dynamic_import.is_speculative,
353            value_escapes: dynamic_import.value_escapes,
354        })
355        .collect()
356}
357
358fn module_require_calls_to_cached(
359    require_calls: &[crate::RequireCallInfo],
360) -> Vec<CachedRequireCall> {
361    require_calls
362        .iter()
363        .map(|require_call| CachedRequireCall {
364            source: require_call.source.clone(),
365            span_start: require_call.span.start,
366            span_end: require_call.span.end,
367            source_span_start: require_call.source_span.start,
368            source_span_end: require_call.source_span.end,
369            destructured_names: require_call.destructured_names.clone(),
370            local_name: require_call.local_name.clone(),
371            is_type_only: require_call.is_type_only,
372        })
373        .collect()
374}
375
376fn module_dynamic_patterns_to_cached(
377    dynamic_import_patterns: &[crate::DynamicImportPattern],
378) -> Vec<CachedDynamicImportPattern> {
379    dynamic_import_patterns
380        .iter()
381        .map(|pattern| CachedDynamicImportPattern {
382            prefix: pattern.prefix.clone(),
383            suffix: pattern.suffix.clone(),
384            excludes: pattern.excludes.clone(),
385            span_start: pattern.span.start,
386            span_end: pattern.span.end,
387            mechanism: pattern.mechanism,
388        })
389        .collect()
390}
391
392fn module_suppressions_to_cached(
393    suppressions: &[crate::suppress::Suppression],
394) -> Vec<CachedSuppression> {
395    suppressions
396        .iter()
397        .map(|suppression| {
398            let (kind, policy_pack, policy_rule_id) = match &suppression.target {
399                None => (0, String::new(), String::new()),
400                Some(SuppressionTarget::Issue(kind)) => {
401                    (kind.to_discriminant(), String::new(), String::new())
402                }
403                Some(SuppressionTarget::PolicyRule(target)) => (
404                    crate::suppress::IssueKind::PolicyViolation.to_discriminant(),
405                    target.pack.clone(),
406                    target.rule_id.clone(),
407                ),
408            };
409            CachedSuppression {
410                line: suppression.line,
411                comment_line: suppression.comment_line,
412                kind,
413                policy_pack,
414                policy_rule_id,
415                reason: suppression.reason.clone(),
416            }
417        })
418        .collect()
419}
420
421fn module_unknown_suppressions_to_cached(
422    unknown_suppression_kinds: &[fallow_types::suppress::UnknownSuppressionKind],
423) -> Vec<CachedUnknownSuppressionKind> {
424    unknown_suppression_kinds
425        .iter()
426        .map(|unknown| CachedUnknownSuppressionKind {
427            comment_line: unknown.comment_line,
428            is_file_level: unknown.is_file_level,
429            token: unknown.token.clone(),
430            reason: unknown.reason.clone(),
431        })
432        .collect()
433}
434
435fn module_local_types_to_cached(
436    local_type_declarations: &[crate::LocalTypeDeclaration],
437) -> Vec<CachedLocalTypeDeclaration> {
438    local_type_declarations
439        .iter()
440        .map(|declaration| CachedLocalTypeDeclaration {
441            name: declaration.name.clone(),
442            span_start: declaration.span.start,
443            span_end: declaration.span.end,
444        })
445        .collect()
446}
447
448fn module_signature_refs_to_cached(
449    public_signature_type_references: &[crate::PublicSignatureTypeReference],
450) -> Vec<CachedPublicSignatureTypeReference> {
451    public_signature_type_references
452        .iter()
453        .map(|reference| CachedPublicSignatureTypeReference {
454            export_name: reference.export_name.clone(),
455            type_name: reference.type_name.clone(),
456            span_start: reference.span.start,
457            span_end: reference.span.end,
458            from_satisfies: reference.from_satisfies,
459        })
460        .collect()
461}
462
463fn module_namespace_aliases_to_cached(
464    namespace_object_aliases: &[NamespaceObjectAlias],
465) -> Vec<CachedNamespaceObjectAlias> {
466    namespace_object_aliases
467        .iter()
468        .map(|alias| CachedNamespaceObjectAlias {
469            via_export_name: alias.via_export_name.clone(),
470            suffix: alias.suffix.clone(),
471            namespace_local: alias.namespace_local.clone(),
472        })
473        .collect()
474}
475
476/// Rehydrate an optional cached slice into the shared `Arc<[T]>` module form.
477fn cached_opt_to_arc<T: Clone>(cached: Option<&[T]>) -> Arc<[T]> {
478    cached.map(Arc::from).unwrap_or_default()
479}
480
481/// Reconstruct a [`ModuleInfo`](crate::ModuleInfo) from a [`CachedModule`], skipping
482/// the per-function complexity vec when `need_complexity` is `false`. Avoids the
483/// `Vec<FunctionComplexity>` clone on warm runs of commands (e.g. `fallow dead-code`)
484/// that don't consume complexity, which adds up across tens of thousands of files.
485#[must_use]
486#[expect(
487    clippy::too_many_lines,
488    reason = "flat field projection preserves the complete extraction cache contract"
489)]
490pub fn cached_to_module_opts(
491    cached: &CachedModule,
492    file_id: fallow_types::discover::FileId,
493    need_complexity: bool,
494) -> crate::ModuleInfo {
495    crate::ModuleInfo {
496        file_id,
497        exports: cached_exports_to_module(&cached.exports).into(),
498        imports: cached_imports_to_module(&cached.imports),
499        re_exports: cached_re_exports_to_module(&cached.re_exports),
500        dynamic_imports: cached_dynamic_imports_to_module(&cached.dynamic_imports),
501        dynamic_import_patterns: cached_dynamic_patterns_to_module(&cached.dynamic_import_patterns),
502        require_calls: cached_require_calls_to_module(&cached.require_calls),
503        package_path_references: cached.package_path_references.clone(),
504        type_package_references: cached.type_package_references.clone(),
505        bin_path_references: cached.bin_path_references.clone(),
506        package_resolve_sites: cached.package_resolve_sites.clone(),
507        member_accesses: cached.member_accesses.clone().into(),
508        semantic_facts: cached_opt_to_arc(cached.semantic_facts.as_deref()),
509        whole_object_uses: Arc::from(&*cached.whole_object_uses),
510        has_cjs_exports: cached.has_cjs_exports,
511        has_angular_component_template_url: cached.has_angular_component_template_url,
512        content_hash: cached.content_hash,
513        parse_error_count: cached.parse_error_count,
514        parse_panicked: cached.parse_panicked,
515        parse_nesting_too_deep: cached.parse_nesting_too_deep,
516        suppressions: cached_suppressions_to_module(&cached.suppressions),
517        unknown_suppression_kinds: cached_unknown_suppressions_to_module(
518            &cached.unknown_suppression_kinds,
519        ),
520        unused_import_bindings: cached.unused_import_bindings.clone(),
521        type_referenced_import_bindings: cached.type_referenced_import_bindings.clone(),
522        value_referenced_import_bindings: cached.value_referenced_import_bindings.clone(),
523        line_offsets: cached.line_offsets.clone(),
524        complexity: if need_complexity {
525            cached.complexity.clone()
526        } else {
527            Vec::new()
528        },
529        flag_uses: cached.flag_uses.clone(),
530        flag_registry_facts: cached.flag_registry_facts.clone(),
531        class_heritage: cached.class_heritage.clone(),
532        exported_factory_returns: cached_opt_to_arc(cached.exported_factory_returns.as_deref()),
533        exported_factory_return_object_shapes: cached_opt_to_arc(
534            cached.exported_factory_return_object_shapes.as_deref(),
535        ),
536        type_member_types: cached_opt_to_arc(cached.type_member_types.as_deref()),
537        injection_tokens: cached.injection_tokens.clone(),
538        local_type_declarations: cached_local_types_to_module(&cached.local_type_declarations),
539        public_signature_type_references: cached_signature_refs_to_module(
540            &cached.public_signature_type_references,
541        ),
542        namespace_object_aliases: cached_namespace_aliases_to_module(
543            &cached.namespace_object_aliases,
544        ),
545        iconify_prefixes: cached.iconify_prefixes.clone(),
546        iconify_icon_names: cached.iconify_icon_names.clone(),
547        auto_import_candidates: cached.auto_import_candidates.clone(),
548        directives: cached.directives.clone(),
549        client_only_dynamic_import_spans: cached.client_only_dynamic_import_spans.clone(),
550        security_sinks: cached.security_sinks.clone(),
551        security_sinks_skipped: cached.security_sinks_skipped,
552        security_unresolved_callee_sites: cached.security_unresolved_callee_sites.clone(),
553        tainted_bindings: cached.tainted_bindings.clone(),
554        sanitized_sink_args: cached.sanitized_sink_args.clone(),
555        security_control_sites: cached.security_control_sites.clone(),
556        callee_uses: cached.callee_uses.clone(),
557        imported_call_sites: cached.imported_call_sites.clone().into(),
558        import_binding_references: cached.import_binding_references.clone().into(),
559        misplaced_directives: cached.misplaced_directives.clone(),
560        inline_server_action_exports: cached.inline_server_action_exports.clone(),
561        di_key_sites: cached.di_key_sites.clone(),
562        has_dynamic_provide: cached.has_dynamic_provide,
563        is_server_action_module: cached.is_server_action_module,
564        has_global_declarations: cached.has_global_declarations,
565        triple_slash_reference_paths: cached.triple_slash_reference_paths.clone(),
566        jsx_runtime_from_config: cached.jsx_runtime_from_config,
567        // Derived in `release_resolution_payload` from `imports` + `unused_import_bindings`
568        // (both cached); never persisted, so the cache-load path leaves it empty.
569        referenced_import_bindings: Vec::new(),
570        component_props: cached.component_props.clone(),
571        component_contracts: cached.component_contracts.clone(),
572        effect_facts: cached.effect_facts.clone(),
573        has_props_attrs_fallthrough: cached.has_props_attrs_fallthrough,
574        has_define_expose: cached.has_define_expose,
575        has_define_model: cached.has_define_model,
576        has_unharvestable_props: cached.has_unharvestable_props,
577        component_emits: cached.component_emits.clone(),
578        angular_inputs: cached.angular_inputs.clone(),
579        angular_outputs: cached.angular_outputs.clone(),
580        angular_component_selectors: cached.angular_component_selectors.clone(),
581        registered_custom_elements: cached.registered_custom_elements.clone(),
582        used_custom_element_tags: cached.used_custom_element_tags.clone(),
583        angular_used_selectors: cached.angular_used_selectors.clone(),
584        angular_entry_component_refs: cached.angular_entry_component_refs.clone(),
585        has_dynamic_component_render: cached.has_dynamic_component_render,
586        has_unharvestable_emits: cached.has_unharvestable_emits,
587        has_dynamic_emit: cached.has_dynamic_emit,
588        has_emit_whole_object_use: cached.has_emit_whole_object_use,
589        load_return_keys: cached.load_return_keys.clone(),
590        has_unharvestable_load: cached.has_unharvestable_load,
591        has_load_data_whole_use: cached.has_load_data_whole_use,
592        // Derived in `release_resolution_payload` from `whole_object_uses`.
593        has_page_data_store_whole_use: false,
594        // Derived in `release_resolution_payload` from `whole_object_uses`.
595        has_route_loader_data_whole_use: false,
596        component_functions: cached.component_functions.clone(),
597        react_props: cached.react_props.clone(),
598        hook_uses: cached.hook_uses.clone(),
599        render_edges: cached.render_edges.clone(),
600        svelte_dispatched_events: cached.svelte_dispatched_events.clone(),
601        svelte_listened_events: cached.svelte_listened_events.clone(),
602        has_dynamic_dispatch: cached.has_dynamic_dispatch,
603    }
604}
605
606/// Convert a [`ModuleInfo`](crate::ModuleInfo) to a [`CachedModule`] for storage.
607///
608/// The [`SourceFingerprint`](fallow_types::source_fingerprint::SourceFingerprint)
609/// comes from `std::fs::metadata()` at parse time and enables fast cache
610/// validation on subsequent runs.
611#[must_use]
612pub fn module_to_cached(
613    module: &crate::ModuleInfo,
614    fingerprint: fallow_types::source_fingerprint::SourceFingerprint,
615    complexity_extracted: bool,
616) -> CachedModule {
617    let mut cached = module_to_cached_payload(module);
618    cached.mtime_ns = fingerprint.mtime_ns;
619    cached.ctime_ns = fingerprint.ctime_ns;
620    cached.file_size = fingerprint.file_size;
621    cached.complexity_extracted = complexity_extracted;
622    cached
623}
624
625#[expect(
626    clippy::too_many_lines,
627    reason = "flat field projection preserves the complete extraction cache contract"
628)]
629fn module_to_cached_payload(module: &crate::ModuleInfo) -> CachedModule {
630    CachedModule {
631        content_hash: module.content_hash,
632        mtime_ns: 0,
633        ctime_ns: 0,
634        file_size: 0,
635        last_seen_secs: current_unix_seconds(),
636        exports: module_exports_to_cached(&module.exports),
637        imports: module_imports_to_cached(&module.imports),
638        re_exports: module_re_exports_to_cached(&module.re_exports),
639        dynamic_imports: module_dynamic_imports_to_cached(&module.dynamic_imports),
640        require_calls: module_require_calls_to_cached(&module.require_calls),
641        package_path_references: module.package_path_references.clone(),
642        type_package_references: module.type_package_references.clone(),
643        bin_path_references: module.bin_path_references.clone(),
644        package_resolve_sites: module.package_resolve_sites.clone(),
645        member_accesses: module.member_accesses.to_vec(),
646        semantic_facts: cached_semantic_facts(&module.semantic_facts),
647        whole_object_uses: Box::from(&*module.whole_object_uses),
648        dynamic_import_patterns: module_dynamic_patterns_to_cached(&module.dynamic_import_patterns),
649        parse_error_count: module.parse_error_count,
650        parse_panicked: module.parse_panicked,
651        parse_nesting_too_deep: module.parse_nesting_too_deep,
652        has_cjs_exports: module.has_cjs_exports,
653        has_angular_component_template_url: module.has_angular_component_template_url,
654        unused_import_bindings: module.unused_import_bindings.clone(),
655        type_referenced_import_bindings: module.type_referenced_import_bindings.clone(),
656        value_referenced_import_bindings: module.value_referenced_import_bindings.clone(),
657        suppressions: module_suppressions_to_cached(&module.suppressions),
658        unknown_suppression_kinds: module_unknown_suppressions_to_cached(
659            &module.unknown_suppression_kinds,
660        ),
661        line_offsets: module.line_offsets.clone(),
662        complexity: module.complexity.clone(),
663        complexity_extracted: false,
664        flag_uses: module.flag_uses.clone(),
665        flag_registry_facts: module.flag_registry_facts.clone(),
666        class_heritage: module.class_heritage.clone(),
667        exported_factory_returns: (!module.exported_factory_returns.is_empty())
668            .then(|| Box::from(&*module.exported_factory_returns)),
669        exported_factory_return_object_shapes: (!module
670            .exported_factory_return_object_shapes
671            .is_empty())
672        .then(|| Box::from(&*module.exported_factory_return_object_shapes)),
673        type_member_types: (!module.type_member_types.is_empty())
674            .then(|| Box::from(&*module.type_member_types)),
675        injection_tokens: module.injection_tokens.clone(),
676        local_type_declarations: module_local_types_to_cached(&module.local_type_declarations),
677        public_signature_type_references: module_signature_refs_to_cached(
678            &module.public_signature_type_references,
679        ),
680        namespace_object_aliases: module_namespace_aliases_to_cached(
681            &module.namespace_object_aliases,
682        ),
683        iconify_prefixes: module.iconify_prefixes.clone(),
684        iconify_icon_names: module.iconify_icon_names.clone(),
685        auto_import_candidates: module.auto_import_candidates.clone(),
686        directives: module.directives.clone(),
687        client_only_dynamic_import_spans: module.client_only_dynamic_import_spans.clone(),
688        security_sinks: module.security_sinks.clone(),
689        security_sinks_skipped: module.security_sinks_skipped,
690        security_unresolved_callee_sites: module.security_unresolved_callee_sites.clone(),
691        tainted_bindings: module.tainted_bindings.clone(),
692        sanitized_sink_args: module.sanitized_sink_args.clone(),
693        security_control_sites: module.security_control_sites.clone(),
694        callee_uses: module.callee_uses.clone(),
695        imported_call_sites: module.imported_call_sites.to_vec(),
696        import_binding_references: module.import_binding_references.to_vec(),
697        misplaced_directives: module.misplaced_directives.clone(),
698        inline_server_action_exports: module.inline_server_action_exports.clone(),
699        di_key_sites: module.di_key_sites.clone(),
700        has_dynamic_provide: module.has_dynamic_provide,
701        is_server_action_module: module.is_server_action_module,
702        has_global_declarations: module.has_global_declarations,
703        triple_slash_reference_paths: module.triple_slash_reference_paths.clone(),
704        jsx_runtime_from_config: module.jsx_runtime_from_config,
705        component_props: module.component_props.clone(),
706        component_contracts: module.component_contracts.clone(),
707        effect_facts: module.effect_facts.clone(),
708        has_props_attrs_fallthrough: module.has_props_attrs_fallthrough,
709        has_define_expose: module.has_define_expose,
710        has_define_model: module.has_define_model,
711        has_unharvestable_props: module.has_unharvestable_props,
712        component_emits: module.component_emits.clone(),
713        angular_inputs: module.angular_inputs.clone(),
714        angular_outputs: module.angular_outputs.clone(),
715        angular_component_selectors: module.angular_component_selectors.clone(),
716        registered_custom_elements: module.registered_custom_elements.clone(),
717        used_custom_element_tags: module.used_custom_element_tags.clone(),
718        angular_used_selectors: module.angular_used_selectors.clone(),
719        angular_entry_component_refs: module.angular_entry_component_refs.clone(),
720        has_dynamic_component_render: module.has_dynamic_component_render,
721        has_unharvestable_emits: module.has_unharvestable_emits,
722        has_dynamic_emit: module.has_dynamic_emit,
723        has_emit_whole_object_use: module.has_emit_whole_object_use,
724        load_return_keys: module.load_return_keys.clone(),
725        has_unharvestable_load: module.has_unharvestable_load,
726        has_load_data_whole_use: module.has_load_data_whole_use,
727        component_functions: module.component_functions.clone(),
728        react_props: module.react_props.clone(),
729        hook_uses: module.hook_uses.clone(),
730        render_edges: module.render_edges.clone(),
731        svelte_dispatched_events: module.svelte_dispatched_events.clone(),
732        svelte_listened_events: module.svelte_listened_events.clone(),
733        has_dynamic_dispatch: module.has_dynamic_dispatch,
734    }
735}
736
737/// Convert a module to a cache entry from explicit source-fingerprint parts.
738///
739/// Kept for tests that need literal invalidation inputs without filesystem
740/// metadata.
741#[cfg(test)]
742#[must_use]
743pub fn module_to_cached_from_parts(
744    module: &crate::ModuleInfo,
745    mtime_ns: u64,
746    file_size: u64,
747) -> CachedModule {
748    module_to_cached(
749        module,
750        fallow_types::source_fingerprint::SourceFingerprint::new(mtime_ns, file_size),
751        !module.complexity.is_empty(),
752    )
753}
754
755fn cached_semantic_facts(
756    facts: &[fallow_types::extract::SemanticFact],
757) -> Option<Box<[fallow_types::extract::SemanticFact]>> {
758    (!facts.is_empty()).then(|| Box::from(facts))
759}