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