diff --git a/crates/oxc_linter/src/module_record.rs b/crates/oxc_linter/src/module_record.rs index 05343546f..f0ad951b9 100644 --- a/crates/oxc_linter/src/module_record.rs +++ b/crates/oxc_linter/src/module_record.rs @@ -464,7 +464,17 @@ impl ModuleRecord { .iter() .map(|(name, span)| (CompactStr::from(name.as_str()), *span)) .collect(), - export_default: other.export_default, + export_default: other + .local_export_entries + .iter() + .filter_map(|export_entry| export_entry.export_name.default_export_span()) + .chain( + other + .indirect_export_entries + .iter() + .filter_map(|export_entry| export_entry.export_name.default_export_span()), + ) + .next(), ..ModuleRecord::default() } } diff --git a/crates/oxc_linter/src/rules/import/no_default_export.rs b/crates/oxc_linter/src/rules/import/no_default_export.rs index f3d7c292c..d4bb993d1 100644 --- a/crates/oxc_linter/src/rules/import/no_default_export.rs +++ b/crates/oxc_linter/src/rules/import/no_default_export.rs @@ -49,9 +49,6 @@ impl Rule for NoDefaultExport { if let Some(span) = module_record.export_default { ctx.diagnostic(no_default_export_diagnostic(span)); } - if let Some(span) = module_record.exported_bindings.get("default") { - ctx.diagnostic(no_default_export_diagnostic(*span)); - } } } diff --git a/crates/oxc_parser/src/lib.rs b/crates/oxc_parser/src/lib.rs index d6f3ea9f7..b3658dc72 100644 --- a/crates/oxc_parser/src/lib.rs +++ b/crates/oxc_parser/src/lib.rs @@ -438,20 +438,21 @@ impl<'a> ParserImpl<'a> { errors.push(error); } } + let (module_record, module_record_errors) = self.module_record_builder.build(); if errors.len() != 1 { errors.reserve(self.lexer.errors.len() + self.errors.len()); errors.extend(self.lexer.errors); errors.extend(self.errors); // Skip checking for exports in TypeScript { if !self.source_type.is_typescript() { - errors.extend(self.module_record_builder.errors()); + errors.extend(module_record_errors); } } let irregular_whitespaces = self.lexer.trivia_builder.irregular_whitespaces.into_boxed_slice(); ParserReturn { program, - module_record: self.module_record_builder.build(), + module_record, errors, irregular_whitespaces, panicked, diff --git a/crates/oxc_parser/src/module_record.rs b/crates/oxc_parser/src/module_record.rs index a3ae47078..f1754da08 100644 --- a/crates/oxc_parser/src/module_record.rs +++ b/crates/oxc_parser/src/module_record.rs @@ -11,7 +11,6 @@ pub struct ModuleRecordBuilder<'a> { allocator: &'a Allocator, module_record: ModuleRecord<'a>, export_entries: Vec>, - export_default_duplicated: Vec, exported_bindings_duplicated: Vec>, } @@ -21,16 +20,16 @@ impl<'a> ModuleRecordBuilder<'a> { allocator, module_record: ModuleRecord::new(allocator), export_entries: vec![], - export_default_duplicated: vec![], exported_bindings_duplicated: vec![], } } - pub fn build(mut self) -> ModuleRecord<'a> { + pub fn build(mut self) -> (ModuleRecord<'a>, Vec) { // The `ParseModule` algorithm requires `importedBoundNames` (import entries) to be // resolved before resolving export entries. self.resolve_export_entries(); - self.module_record + let errors = self.errors(); + (self.module_record, errors) } pub fn errors(&self) -> Vec { @@ -44,19 +43,25 @@ impl<'a> ModuleRecordBuilder<'a> { errors.push(diagnostics::duplicate_export(&name_span.name, name_span.span, old_span)); } - for span in &self.export_default_duplicated { - let old_span = module_record.export_default.unwrap(); - errors.push(diagnostics::duplicate_export("default", *span, old_span)); + // Multiple default exports + // `export default foo` + // `export { default }` + let default_exports = module_record + .local_export_entries + .iter() + .filter_map(|export_entry| export_entry.export_name.default_export_span()) + .chain( + module_record + .indirect_export_entries + .iter() + .filter_map(|export_entry| export_entry.export_name.default_export_span()), + ) + .collect::>(); + if default_exports.len() > 1 { + errors.push( + OxcDiagnostic::error("Duplicated default export").with_labels(default_exports), + ); } - - // `export default x;` - // `export { y as default };` - if let (Some(span), Some(default_span)) = - (module_record.exported_bindings.get("default"), &module_record.export_default) - { - errors.push(diagnostics::duplicate_export("default", *default_span, *span)); - } - errors } @@ -94,12 +99,6 @@ impl<'a> ModuleRecordBuilder<'a> { } } - fn add_default_export(&mut self, span: Span) { - if let Some(old_node) = self.module_record.export_default.replace(span) { - self.export_default_duplicated.push(old_node); - } - } - /// [ParseModule](https://tc39.es/ecma262/#sec-parsemodule) /// Step 10. fn resolve_export_entries(&mut self) { @@ -264,8 +263,6 @@ impl<'a> ModuleRecordBuilder<'a> { return; } let exported_name = &decl.exported; - let exported_name_span = decl.exported.span(); - self.add_default_export(exported_name_span); let local_name = match &decl.declaration { ExportDefaultDeclarationKind::Identifier(ident) => { diff --git a/crates/oxc_syntax/src/module_record.rs b/crates/oxc_syntax/src/module_record.rs index c81c6d3ac..a43cfceac 100644 --- a/crates/oxc_syntax/src/module_record.rs +++ b/crates/oxc_syntax/src/module_record.rs @@ -55,10 +55,6 @@ pub struct ModuleRecord<'a> { /// Local exported bindings pub exported_bindings: FxHashMap, Span>, - - /// `export default name` - /// ^^^^^^^ span - pub export_default: Option, } impl<'a> ModuleRecord<'a> { @@ -72,7 +68,6 @@ impl<'a> ModuleRecord<'a> { indirect_export_entries: Vec::new_in(allocator), star_export_entries: Vec::new_in(allocator), exported_bindings: FxHashMap::default(), - export_default: None, } } } @@ -287,6 +282,17 @@ impl ExportExportName<'_> { Self::Null => None, } } + + /// Get default export span + /// `export default foo` + /// `export { default }` + pub fn default_export_span(&self) -> Option { + match self { + Self::Default(span) => Some(*span), + Self::Name(name_span) if name_span.name == "default" => Some(name_span.span), + _ => None, + } + } } /// `LocalName` for `ExportEntry` diff --git a/tasks/coverage/snapshots/parser_babel.snap b/tasks/coverage/snapshots/parser_babel.snap index b12f7f3f4..a52851594 100644 --- a/tasks/coverage/snapshots/parser_babel.snap +++ b/tasks/coverage/snapshots/parser_babel.snap @@ -3031,24 +3031,20 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typesc · ─────────── ╰──── - × Duplicated export 'default' + × Duplicated default export ╭─[babel/packages/babel-parser/test/fixtures/es2015/modules/duplicate-export-default/input.js:1:8] 1 │ export default {}; - · ───┬─── - · ╰── Export has already been declared here + · ─────── 2 │ export default function() {}; - · ───┬─── - · ╰── It cannot be redeclared here + · ─────── ╰──── - × Duplicated export 'default' + × Duplicated default export ╭─[babel/packages/babel-parser/test/fixtures/es2015/modules/duplicate-export-default-and-export-as-default/input.js:1:8] 1 │ export default function() {}; - · ───┬─── - · ╰── Export has already been declared here + · ─────── 2 │ export { foo as default }; - · ───┬─── - · ╰── It cannot be redeclared here + · ─────── ╰──── × Export 'foo' is not defined diff --git a/tasks/coverage/snapshots/parser_test262.snap b/tasks/coverage/snapshots/parser_test262.snap index b069ccaa6..9eef88277 100644 --- a/tasks/coverage/snapshots/parser_test262.snap +++ b/tasks/coverage/snapshots/parser_test262.snap @@ -22158,15 +22158,13 @@ Expect Syntax Error: tasks/coverage/test262/test/language/import/import-attribut · ╰── It can not be redeclared here ╰──── - × Duplicated export 'default' + × Duplicated default export ╭─[test262/test/language/module-code/early-dup-export-dflt-id.js:18:8] 17 │ var x, y; 18 │ export default x; - · ───┬─── - · ╰── Export has already been declared here + · ─────── 19 │ export { y as default }; - · ───┬─── - · ╰── It cannot be redeclared here + · ─────── ╰──── × Unexpected token @@ -22199,15 +22197,13 @@ Expect Syntax Error: tasks/coverage/test262/test/language/import/import-attribut · ╰── It cannot be redeclared here ╰──── - × Duplicated export 'default' + × Duplicated default export ╭─[test262/test/language/module-code/early-dup-export-star-as-dflt.js:18:8] 17 │ var x; 18 │ export default x; - · ───┬─── - · ╰── Export has already been declared here + · ─────── 19 │ export * as default from './early-dup-export-start-as-dflt.js'; - · ───┬─── - · ╰── It cannot be redeclared here + · ─────── ╰──── × Label `label` has already been declared diff --git a/tasks/coverage/snapshots/parser_typescript.snap b/tasks/coverage/snapshots/parser_typescript.snap index cb5e13b83..42d7ab157 100644 --- a/tasks/coverage/snapshots/parser_typescript.snap +++ b/tasks/coverage/snapshots/parser_typescript.snap @@ -22418,14 +22418,12 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/salsa/private 13 │ deleted() { ╰──── - × Duplicated export 'default' + × Duplicated default export ╭─[typescript/tests/cases/conformance/salsa/plainJSBinderErrors.ts:1:8] 1 │ export default 12 - · ───┬─── - · ╰── Export has already been declared here + · ─────── 2 │ export default 13 - · ───┬─── - · ╰── It cannot be redeclared here + · ─────── 3 │ const await = 1 ╰────