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    }
161}
162
163/// Convert a [`ModuleInfo`](crate::ModuleInfo) to a [`CachedModule`] for storage.
164///
165/// `mtime_secs` and `file_size` come from `std::fs::metadata()` at parse time
166/// and enable fast cache validation on subsequent runs (skip file read when
167/// mtime+size match).
168#[must_use]
169pub fn module_to_cached(
170    module: &crate::ModuleInfo,
171    mtime_secs: u64,
172    file_size: u64,
173) -> CachedModule {
174    CachedModule {
175        content_hash: module.content_hash,
176        mtime_secs,
177        file_size,
178        exports: module
179            .exports
180            .iter()
181            .map(|e| CachedExport {
182                name: match &e.name {
183                    ExportName::Named(n) => n.clone(),
184                    ExportName::Default => String::new(),
185                },
186                is_default: matches!(e.name, ExportName::Default),
187                is_type_only: e.is_type_only,
188                visibility: e.visibility as u8,
189                local_name: e.local_name.clone(),
190                span_start: e.span.start,
191                span_end: e.span.end,
192                members: e
193                    .members
194                    .iter()
195                    .map(|m| CachedMember {
196                        name: m.name.clone(),
197                        kind: m.kind,
198                        span_start: m.span.start,
199                        span_end: m.span.end,
200                        has_decorator: m.has_decorator,
201                    })
202                    .collect(),
203                super_class: e.super_class.clone(),
204            })
205            .collect(),
206        imports: module
207            .imports
208            .iter()
209            .map(|i| {
210                let (kind, imported_name) = match &i.imported_name {
211                    crate::ImportedName::Named(n) => (IMPORT_KIND_NAMED, n.clone()),
212                    crate::ImportedName::Default => (IMPORT_KIND_DEFAULT, String::new()),
213                    crate::ImportedName::Namespace => (IMPORT_KIND_NAMESPACE, String::new()),
214                    crate::ImportedName::SideEffect => (IMPORT_KIND_SIDE_EFFECT, String::new()),
215                };
216                CachedImport {
217                    source: i.source.clone(),
218                    imported_name,
219                    local_name: i.local_name.clone(),
220                    is_type_only: i.is_type_only,
221                    kind,
222                    span_start: i.span.start,
223                    span_end: i.span.end,
224                    source_span_start: i.source_span.start,
225                    source_span_end: i.source_span.end,
226                }
227            })
228            .collect(),
229        re_exports: module
230            .re_exports
231            .iter()
232            .map(|r| CachedReExport {
233                source: r.source.clone(),
234                imported_name: r.imported_name.clone(),
235                exported_name: r.exported_name.clone(),
236                is_type_only: r.is_type_only,
237                span_start: r.span.start,
238                span_end: r.span.end,
239            })
240            .collect(),
241        dynamic_imports: module
242            .dynamic_imports
243            .iter()
244            .map(|d| CachedDynamicImport {
245                source: d.source.clone(),
246                span_start: d.span.start,
247                span_end: d.span.end,
248                destructured_names: d.destructured_names.clone(),
249                local_name: d.local_name.clone(),
250            })
251            .collect(),
252        require_calls: module
253            .require_calls
254            .iter()
255            .map(|r| CachedRequireCall {
256                source: r.source.clone(),
257                span_start: r.span.start,
258                span_end: r.span.end,
259                destructured_names: r.destructured_names.clone(),
260                local_name: r.local_name.clone(),
261            })
262            .collect(),
263        member_accesses: module.member_accesses.clone(),
264        whole_object_uses: module.whole_object_uses.clone(),
265        dynamic_import_patterns: module
266            .dynamic_import_patterns
267            .iter()
268            .map(|p| CachedDynamicImportPattern {
269                prefix: p.prefix.clone(),
270                suffix: p.suffix.clone(),
271                span_start: p.span.start,
272                span_end: p.span.end,
273            })
274            .collect(),
275        has_cjs_exports: module.has_cjs_exports,
276        unused_import_bindings: module.unused_import_bindings.clone(),
277        suppressions: module
278            .suppressions
279            .iter()
280            .map(|s| CachedSuppression {
281                line: s.line,
282                comment_line: s.comment_line,
283                kind: s
284                    .kind
285                    .map_or(0, crate::suppress::IssueKind::to_discriminant),
286            })
287            .collect(),
288        line_offsets: module.line_offsets.clone(),
289        complexity: module.complexity.clone(),
290        flag_uses: module.flag_uses.clone(),
291    }
292}