Skip to main content

oxc_parser/observation/
default_export.rs

1//! Borrow the default-export entry already retained by the original parser.
2
3use oxc_span::Span;
4use oxc_syntax::module_record::{ExportEntry, ExportExportName};
5
6use super::AdmittedProgram;
7
8/// The sole direct value-default export in the original parser's ModuleRecord.
9///
10/// These ranges borrow the actual parser entry and keyword, not caller-provided
11/// spans or a second AST/source walk. This does not certify an empty object,
12/// strict-module semantics, whole File eligibility or native product admission.
13/// Named-as-default, type-only, indirect, star and multiple entries do not mint
14/// this bounded receipt. Other statements and imports remain original syntax.
15///
16/// ```compile_fail
17/// use oxc_parser::OriginalDefaultExport;
18/// fn forge() { let _ = OriginalDefaultExport { keyword: true }; }
19/// ```
20/// ```compile_fail
21/// use oxc_parser::OriginalDefaultExport;
22/// fn copy(receipt: OriginalDefaultExport<'_, '_>) { let _ = receipt.clone(); }
23/// ```
24/// The actual parser observation stays borrowed:
25/// ```compile_fail
26/// use oxc_parser::ProgramObservation;
27/// fn discard(observation: ProgramObservation<'_>) {
28///     let receipt = observation.admitted().unwrap().sole_default_export().unwrap();
29///     drop(observation);
30///     let _ = receipt.statement_span();
31/// }
32/// ```
33pub struct OriginalDefaultExport<'p, 'a> {
34    program: AdmittedProgram<'p, 'a>,
35    entry: &'p ExportEntry<'a>,
36    keyword: &'p Span,
37}
38
39impl<'p, 'a> AdmittedProgram<'p, 'a> {
40    /// Project one existing original local Default entry in constant time.
41    /// Syntax admission remains separate from the entry's semantic consumers.
42    #[must_use]
43    pub fn sole_default_export(&self) -> Option<OriginalDefaultExport<'p, 'a>> {
44        let records = &self.observation.parsed.module_record;
45        if !records.indirect_export_entries.is_empty() || !records.star_export_entries.is_empty() {
46            return None;
47        }
48        let [entry] = records.local_export_entries.as_slice() else {
49            return None;
50        };
51        let ExportExportName::Default(keyword) = &entry.export_name else {
52            return None;
53        };
54        if entry.is_type || entry.module_request.is_some() {
55            return None;
56        }
57        Some(OriginalDefaultExport {
58            program: AdmittedProgram { observation: self.observation },
59            entry,
60            keyword,
61        })
62    }
63}
64
65impl<'p, 'a> OriginalDefaultExport<'p, 'a> {
66    /// The same complete original Program/source/profile/options observation.
67    #[must_use]
68    pub fn program(&self) -> &AdmittedProgram<'p, 'a> {
69        &self.program
70    }
71
72    #[must_use]
73    pub fn statement_span(&self) -> Span {
74        self.entry.statement_span
75    }
76
77    /// The original declaration or expression range, without claiming its kind.
78    #[must_use]
79    pub fn declaration_span(&self) -> Span {
80        self.entry.span
81    }
82
83    #[must_use]
84    pub fn default_keyword_span(&self) -> Span {
85        *self.keyword
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::{ParseOptions, Parser};
93    use oxc_allocator::Allocator;
94    use oxc_ast::ast::Statement;
95    use oxc_span::{GetSpan, SourceType};
96
97    #[test]
98    fn original_default_entry_keeps_actual_utf8_crlf_keyword_and_ast_ranges() {
99        let arena = Allocator::default();
100        let source = "/*雪🌸*/\r\nexport /*🌸*/ default { /*retained*/ }; //尾\r\n";
101        for profile in [SourceType::mjs(), SourceType::ts()] {
102            let observation = Parser::new(&arena, source, profile).parse_observed();
103            let admitted = observation.admitted().expect("original admitted syntax");
104            let receipt = admitted.sole_default_export().expect("original default entry");
105            let Statement::ExportDefaultDeclaration(export) = &admitted.program().body[0] else {
106                panic!("original default AST");
107            };
108            assert_eq!(receipt.statement_span(), export.span);
109            assert_eq!(receipt.declaration_span(), export.declaration.span());
110            assert_eq!(
111                &source[receipt.default_keyword_span().start as usize
112                    ..receipt.default_keyword_span().end as usize],
113                "default"
114            );
115            assert_eq!(
116                &source[receipt.declaration_span().start as usize
117                    ..receipt.declaration_span().end as usize],
118                "{ /*retained*/ }"
119            );
120            assert!(core::ptr::eq(
121                receipt.entry,
122                &observation.parsed.module_record.local_export_entries[0]
123            ));
124            let ExportExportName::Default(keyword) = &receipt.entry.export_name else {
125                panic!("actual default variant");
126            };
127            assert!(core::ptr::eq(receipt.keyword, keyword));
128            assert!(core::ptr::eq(receipt.program().program(), admitted.program()));
129            assert!(core::ptr::eq(receipt.program().source(), source));
130            assert_eq!(receipt.program().source_type(), profile);
131            assert_eq!(receipt.program().options(), ParseOptions::default());
132            assert_eq!(receipt.program().program().comments.len(), 4);
133            assert!(observation.diagnostics().is_empty());
134        }
135    }
136
137    #[test]
138    fn receipt_does_not_claim_empty_objects_or_default_options() {
139        let arena = Allocator::default();
140        for source in [
141            "export default 42;",
142            "export default function f() {}",
143            "export default class C {}",
144            "import value from 'dep'; export default value;",
145        ] {
146            let observation = Parser::new(&arena, source, SourceType::mjs()).parse_observed();
147            let receipt = observation.admitted().unwrap().sole_default_export().unwrap();
148            assert_eq!(receipt.program().source(), source);
149            assert!(receipt.declaration_span().start < receipt.declaration_span().end);
150        }
151        let options = ParseOptions { preserve_parens: false, ..ParseOptions::default() };
152        let observation = Parser::new(&arena, "export default ({});", SourceType::ts())
153            .with_options(options)
154            .parse_observed();
155        let receipt = observation.admitted().unwrap().sole_default_export().unwrap();
156        assert_eq!(receipt.program().options(), options);
157        let observation =
158            Parser::new(&arena, "const value=010; export default {};", SourceType::mjs())
159                .parse_observed();
160        let receipt = observation.admitted().unwrap().sole_default_export().unwrap();
161        assert!(receipt.program().has_legacy_literals());
162        assert!(observation.diagnostics().is_empty());
163    }
164
165    #[test]
166    fn named_indirect_star_type_and_ts_duplicate_exports_cannot_supply_receipt() {
167        let arena = Allocator::default();
168        for source in [
169            "const value=1; export { value as default };",
170            "export { default } from 'dep';",
171            "export * from 'dep';",
172            "export default {}; export const other=1;",
173            "export default {}; export * from 'dep';",
174            "export default {}; export { other } from 'dep';",
175            "export default interface Value {}",
176            "export default {}; export default {};",
177            "const value=1; export default {}; export { value as default };",
178            "const value=1;",
179        ] {
180            let observation = Parser::new(&arena, source, SourceType::ts()).parse_observed();
181            let admitted = observation.admitted().expect(source);
182            assert!(admitted.sole_default_export().is_none(), "{source}");
183            assert!(observation.diagnostics().is_empty(), "unchanged TS diagnostics: {source}");
184        }
185    }
186
187    #[test]
188    fn original_owner_move_retains_module_entry_ast_and_comment_identity() {
189        let arena = Allocator::default();
190        let source = "/*雪*/ export default {}; //🌸\r\n";
191        let observation = Parser::new(&arena, source, SourceType::mjs()).parse_observed();
192        let admitted = observation.admitted().unwrap();
193        let receipt = admitted.sole_default_export().unwrap();
194        let entry = receipt.entry as *const _;
195        let body = receipt.program().program().body.as_ptr();
196        let comments = receipt.program().program().comments.as_ptr();
197        let statement_span = receipt.statement_span();
198        let observation = Box::new(observation);
199        let receipt = observation.admitted().unwrap().sole_default_export().unwrap();
200        assert_eq!(receipt.entry as *const _, entry);
201        assert_eq!(receipt.program().program().body.as_ptr(), body);
202        assert_eq!(receipt.program().program().comments.as_ptr(), comments);
203        assert_eq!(receipt.statement_span(), statement_span);
204        let foreign = Parser::new(&arena, source, SourceType::mjs()).parse_observed();
205        let foreign = foreign.admitted().unwrap().sole_default_export().unwrap();
206        assert!(!core::ptr::eq(receipt.program().program(), foreign.program().program()));
207        assert!(!core::ptr::eq(receipt.entry, foreign.entry));
208    }
209
210    #[test]
211    fn recovered_duplicate_and_escaped_keyword_syntax_keep_original_refusal() {
212        let arena = Allocator::default();
213        for source in [
214            "export default {}; export default {};",
215            "export default {",
216            "export def\\u0061ult {};",
217        ] {
218            let observation = Parser::new(&arena, source, SourceType::mjs()).parse_observed();
219            assert!(observation.admitted().is_none(), "{source}");
220            assert!(observation.diagnostics().has_errors(), "{source}");
221        }
222    }
223}