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 oxc_span::Span;
11
12use crate::ExportName;
13use fallow_types::extract::VisibilityTag;
14
15use super::types::{
16    CachedDynamicImport, CachedDynamicImportPattern, CachedExport, CachedImport, CachedMember,
17    CachedModule, CachedReExport, CachedRequireCall, CachedSuppression, IMPORT_KIND_DEFAULT,
18    IMPORT_KIND_NAMED, IMPORT_KIND_NAMESPACE, IMPORT_KIND_SIDE_EFFECT,
19};
20
21/// Reconstruct a [`ModuleInfo`](crate::ModuleInfo) from a [`CachedModule`].
22#[must_use]
23pub fn cached_to_module(
24    cached: &CachedModule,
25    file_id: fallow_types::discover::FileId,
26) -> crate::ModuleInfo {
27    use crate::{
28        DynamicImportInfo, ExportInfo, ImportInfo, ImportedName, MemberInfo, ModuleInfo,
29        ReExportInfo, RequireCallInfo,
30    };
31
32    let exports = cached
33        .exports
34        .iter()
35        .map(|e| ExportInfo {
36            name: if e.is_default {
37                ExportName::Default
38            } else {
39                ExportName::Named(e.name.clone())
40            },
41            local_name: e.local_name.clone(),
42            is_type_only: e.is_type_only,
43            visibility: match e.visibility {
44                1 => VisibilityTag::Public,
45                2 => VisibilityTag::Internal,
46                3 => VisibilityTag::Beta,
47                4 => VisibilityTag::Alpha,
48                5 => VisibilityTag::ExpectedUnused,
49                _ => VisibilityTag::None,
50            },
51            span: Span::new(e.span_start, e.span_end),
52            members: e
53                .members
54                .iter()
55                .map(|m| MemberInfo {
56                    name: m.name.clone(),
57                    kind: m.kind,
58                    span: Span::new(m.span_start, m.span_end),
59                    has_decorator: m.has_decorator,
60                })
61                .collect(),
62            super_class: e.super_class.clone(),
63        })
64        .collect();
65
66    let imports = cached
67        .imports
68        .iter()
69        .map(|i| ImportInfo {
70            source: i.source.clone(),
71            imported_name: match i.kind {
72                IMPORT_KIND_DEFAULT => ImportedName::Default,
73                IMPORT_KIND_NAMESPACE => ImportedName::Namespace,
74                IMPORT_KIND_SIDE_EFFECT => ImportedName::SideEffect,
75                // IMPORT_KIND_NAMED (0) and any unknown value default to Named
76                _ => ImportedName::Named(i.imported_name.clone()),
77            },
78            local_name: i.local_name.clone(),
79            is_type_only: i.is_type_only,
80            span: Span::new(i.span_start, i.span_end),
81            source_span: Span::new(i.source_span_start, i.source_span_end),
82        })
83        .collect();
84
85    let re_exports = cached
86        .re_exports
87        .iter()
88        .map(|r| ReExportInfo {
89            source: r.source.clone(),
90            imported_name: r.imported_name.clone(),
91            exported_name: r.exported_name.clone(),
92            is_type_only: r.is_type_only,
93            span: Span::new(r.span_start, r.span_end),
94        })
95        .collect();
96
97    let dynamic_imports = cached
98        .dynamic_imports
99        .iter()
100        .map(|d| DynamicImportInfo {
101            source: d.source.clone(),
102            span: Span::new(d.span_start, d.span_end),
103            destructured_names: d.destructured_names.clone(),
104            local_name: d.local_name.clone(),
105        })
106        .collect();
107
108    let require_calls = cached
109        .require_calls
110        .iter()
111        .map(|r| RequireCallInfo {
112            source: r.source.clone(),
113            span: Span::new(r.span_start, r.span_end),
114            destructured_names: r.destructured_names.clone(),
115            local_name: r.local_name.clone(),
116        })
117        .collect();
118
119    let dynamic_import_patterns = cached
120        .dynamic_import_patterns
121        .iter()
122        .map(|p| crate::DynamicImportPattern {
123            prefix: p.prefix.clone(),
124            suffix: p.suffix.clone(),
125            span: Span::new(p.span_start, p.span_end),
126        })
127        .collect();
128
129    let suppressions = cached
130        .suppressions
131        .iter()
132        .map(|s| crate::suppress::Suppression {
133            line: s.line,
134            comment_line: s.comment_line,
135            kind: if s.kind == 0 {
136                None
137            } else {
138                crate::suppress::IssueKind::from_discriminant(s.kind)
139            },
140        })
141        .collect();
142
143    ModuleInfo {
144        file_id,
145        exports,
146        imports,
147        re_exports,
148        dynamic_imports,
149        dynamic_import_patterns,
150        require_calls,
151        member_accesses: cached.member_accesses.clone(),
152        whole_object_uses: cached.whole_object_uses.clone(),
153        has_cjs_exports: cached.has_cjs_exports,
154        content_hash: cached.content_hash,
155        suppressions,
156        unused_import_bindings: cached.unused_import_bindings.clone(),
157        line_offsets: cached.line_offsets.clone(),
158        complexity: cached.complexity.clone(),
159        flag_uses: cached.flag_uses.clone(),
160        class_heritage: cached.class_heritage.clone(),
161    }
162}
163
164/// Convert a [`ModuleInfo`](crate::ModuleInfo) to a [`CachedModule`] for storage.
165///
166/// `mtime_secs` and `file_size` come from `std::fs::metadata()` at parse time
167/// and enable fast cache validation on subsequent runs (skip file read when
168/// mtime+size match).
169#[must_use]
170pub fn module_to_cached(
171    module: &crate::ModuleInfo,
172    mtime_secs: u64,
173    file_size: u64,
174) -> CachedModule {
175    CachedModule {
176        content_hash: module.content_hash,
177        mtime_secs,
178        file_size,
179        exports: module
180            .exports
181            .iter()
182            .map(|e| CachedExport {
183                name: match &e.name {
184                    ExportName::Named(n) => n.clone(),
185                    ExportName::Default => String::new(),
186                },
187                is_default: matches!(e.name, ExportName::Default),
188                is_type_only: e.is_type_only,
189                visibility: e.visibility as u8,
190                local_name: e.local_name.clone(),
191                span_start: e.span.start,
192                span_end: e.span.end,
193                members: e
194                    .members
195                    .iter()
196                    .map(|m| CachedMember {
197                        name: m.name.clone(),
198                        kind: m.kind,
199                        span_start: m.span.start,
200                        span_end: m.span.end,
201                        has_decorator: m.has_decorator,
202                    })
203                    .collect(),
204                super_class: e.super_class.clone(),
205            })
206            .collect(),
207        imports: module
208            .imports
209            .iter()
210            .map(|i| {
211                let (kind, imported_name) = match &i.imported_name {
212                    crate::ImportedName::Named(n) => (IMPORT_KIND_NAMED, n.clone()),
213                    crate::ImportedName::Default => (IMPORT_KIND_DEFAULT, String::new()),
214                    crate::ImportedName::Namespace => (IMPORT_KIND_NAMESPACE, String::new()),
215                    crate::ImportedName::SideEffect => (IMPORT_KIND_SIDE_EFFECT, String::new()),
216                };
217                CachedImport {
218                    source: i.source.clone(),
219                    imported_name,
220                    local_name: i.local_name.clone(),
221                    is_type_only: i.is_type_only,
222                    kind,
223                    span_start: i.span.start,
224                    span_end: i.span.end,
225                    source_span_start: i.source_span.start,
226                    source_span_end: i.source_span.end,
227                }
228            })
229            .collect(),
230        re_exports: module
231            .re_exports
232            .iter()
233            .map(|r| CachedReExport {
234                source: r.source.clone(),
235                imported_name: r.imported_name.clone(),
236                exported_name: r.exported_name.clone(),
237                is_type_only: r.is_type_only,
238                span_start: r.span.start,
239                span_end: r.span.end,
240            })
241            .collect(),
242        dynamic_imports: module
243            .dynamic_imports
244            .iter()
245            .map(|d| CachedDynamicImport {
246                source: d.source.clone(),
247                span_start: d.span.start,
248                span_end: d.span.end,
249                destructured_names: d.destructured_names.clone(),
250                local_name: d.local_name.clone(),
251            })
252            .collect(),
253        require_calls: module
254            .require_calls
255            .iter()
256            .map(|r| CachedRequireCall {
257                source: r.source.clone(),
258                span_start: r.span.start,
259                span_end: r.span.end,
260                destructured_names: r.destructured_names.clone(),
261                local_name: r.local_name.clone(),
262            })
263            .collect(),
264        member_accesses: module.member_accesses.clone(),
265        whole_object_uses: module.whole_object_uses.clone(),
266        dynamic_import_patterns: module
267            .dynamic_import_patterns
268            .iter()
269            .map(|p| CachedDynamicImportPattern {
270                prefix: p.prefix.clone(),
271                suffix: p.suffix.clone(),
272                span_start: p.span.start,
273                span_end: p.span.end,
274            })
275            .collect(),
276        has_cjs_exports: module.has_cjs_exports,
277        unused_import_bindings: module.unused_import_bindings.clone(),
278        suppressions: module
279            .suppressions
280            .iter()
281            .map(|s| CachedSuppression {
282                line: s.line,
283                comment_line: s.comment_line,
284                kind: s
285                    .kind
286                    .map_or(0, crate::suppress::IssueKind::to_discriminant),
287            })
288            .collect(),
289        line_offsets: module.line_offsets.clone(),
290        complexity: module.complexity.clone(),
291        flag_uses: module.flag_uses.clone(),
292        class_heritage: module.class_heritage.clone(),
293    }
294}