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