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