oxc_parser/observation/
default_export.rs1use oxc_span::Span;
4use oxc_syntax::module_record::{ExportEntry, ExportExportName};
5
6use super::AdmittedProgram;
7
8pub 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 #[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 #[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 #[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}