Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/oxc_ast/src/ast_impl/js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1874,6 +1874,16 @@ impl<'a> ModuleExportName<'a> {
Self::StringLiteral(_) => None,
}
}

/// Returns `true` if this module export name is an identifier, and not a string literal.
///
/// ## Example
///
/// - `export { foo }` => `true`
/// - `export { "foo" }` => `false`
pub fn is_identifier(&self) -> bool {
matches!(self, Self::IdentifierName(_) | Self::IdentifierReference(_))
}
}

impl ImportPhase {
Expand Down
5 changes: 5 additions & 0 deletions crates/oxc_parser/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,11 @@ pub fn identifier_generator(x0: &str, span1: Span) -> OxcDiagnostic {
.with_label(span1)
}

#[cold]
pub fn identifier_expected(span: Span) -> OxcDiagnostic {
OxcDiagnostic::error("Identifier expected.").with_label(span)
}

#[cold]
pub fn identifier_reserved_word(span: Span, reserved: &str) -> OxcDiagnostic {
OxcDiagnostic::error(format!(
Expand Down
243 changes: 160 additions & 83 deletions crates/oxc_parser/src/js/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ use rustc_hash::FxHashMap;
use super::FunctionKind;
use crate::{Context, ParserImpl, diagnostics, lexer::Kind, modifiers::Modifiers};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ImportOrExport {
/// Some kind of `import` statement/declaration
Import,
/// Some kind of `export` statement/declaration
Export,
}

#[derive(Debug)]
enum ImportOrExportSpecifier<'a> {
/// An import specifier, such as `import { a } from 'b'`
Import(ImportSpecifier<'a>),
/// An export specifier, such as `export { a } from 'b'`
Export(ExportSpecifier<'a>),
}

impl<'a> ParserImpl<'a> {
/// [Import Call](https://tc39.es/ecma262/#sec-import-calls)
/// `ImportCall` : import ( `AssignmentExpression` )
Expand Down Expand Up @@ -322,7 +338,7 @@ impl<'a> ParserImpl<'a> {
self.expect(Kind::LCurly);
let (mut specifiers, _) = self.context(Context::empty(), self.ctx, |p| {
p.parse_delimited_list(Kind::RCurly, Kind::Comma, |parser| {
parser.parse_export_named_specifier(export_kind)
parser.parse_export_specifier(export_kind)
})
});
self.expect(Kind::RCurly);
Expand Down Expand Up @@ -503,60 +519,162 @@ impl<'a> ParserImpl<'a> {
&mut self,
parent_import_kind: ImportOrExportKind,
) -> ImportDeclarationSpecifier<'a> {
match self.parse_import_or_export_specifier(ImportOrExport::Import, parent_import_kind) {
ImportOrExportSpecifier::Import(specifier) => {
self.ast.import_declaration_specifier_import_specifier(
specifier.span,
specifier.imported,
specifier.local,
specifier.import_kind,
)
}
ImportOrExportSpecifier::Export(_) => unreachable!(),
}
}

fn parse_import_or_export_specifier(
&mut self,
specifier_type: ImportOrExport,
parent_kind: ImportOrExportKind,
) -> ImportOrExportSpecifier<'a> {
let specifier_span = self.start_span();
let mut import_kind = ImportOrExportKind::Value;
let checkpoint = self.checkpoint();
if self.is_ts && self.at(Kind::Type) {
self.bump_any();
let type_or_name_token = self.cur_token();
let type_or_name_token_kind = type_or_name_token.kind();
let mut check_identifier_token = self.cur_token();
let mut check_identifier_is_keyword =
type_or_name_token_kind.is_any_keyword() && !type_or_name_token_kind.is_identifier();

let mut kind = ImportOrExportKind::Value;
let mut can_parse_as_keyword = true;
let mut property_name: Option<ModuleExportName<'a>> = None;
let mut name = self.parse_module_export_name();

if self.is_ts && name.is_identifier() && type_or_name_token_kind == Kind::Type {
// If the first token of an import/export specifier is 'type', there are a lot of possibilities,
// especially if we see 'as' afterwards:
//
// import { type } from "mod"; - isTypeOnly: false, name: type
// import { type as } from "mod"; - isTypeOnly: true, name: as
// import { type as as } from "mod"; - isTypeOnly: false, name: as, propertyName: type
// import { type as as as } from "mod"; - isTypeOnly: true, name: as, propertyName: as
if self.at(Kind::As) {
// { type as ...? }
self.bump_any();
let first_as = self.parse_identifier_name();
if self.at(Kind::As) {
// { type as as ...? }
self.bump_any();
let second_as = self.parse_identifier_name();
if self.can_parse_module_export_name() {
// { type as as something }
// { type as as "something" }
import_kind = ImportOrExportKind::Type;
kind = ImportOrExportKind::Type;
property_name = Some(
self.ast
.module_export_name_identifier_name(second_as.span, second_as.name),
);
check_identifier_token = self.cur_token();
check_identifier_is_keyword =
self.cur_kind().is_any_keyword() && !self.cur_kind().is_identifier();
name = self.parse_module_export_name();
can_parse_as_keyword = false;
} else {
// { type as as }
property_name = Some(
self.ast
.module_export_name_identifier_name(first_as.span, first_as.name),
);
name = self
.ast
.module_export_name_identifier_name(second_as.span, second_as.name);
can_parse_as_keyword = false;
}
} else if !self.can_parse_module_export_name() {
} else if self.can_parse_module_export_name() {
// { type as something }
// { type as "something" }
property_name = Some(name);
can_parse_as_keyword = false;
check_identifier_token = self.cur_token();
check_identifier_is_keyword =
self.cur_kind().is_any_keyword() && !self.cur_kind().is_identifier();
name = self.parse_module_export_name();
} else {
// { type as }
import_kind = ImportOrExportKind::Type;
kind = ImportOrExportKind::Type;
name =
self.ast.module_export_name_identifier_name(first_as.span, first_as.name);
}
} else if self.can_parse_module_export_name() {
// { type something }
import_kind = ImportOrExportKind::Type;
// { type something ...? }
// { type "something" ...? }
kind = ImportOrExportKind::Type;
check_identifier_token = self.cur_token();
check_identifier_is_keyword =
self.cur_kind().is_any_keyword() && !self.cur_kind().is_identifier();
name = self.parse_module_export_name();
}
}
self.rewind(checkpoint);

// `import type { type bar } from 'foo';`
if parent_import_kind == ImportOrExportKind::Type && import_kind == ImportOrExportKind::Type
{
self.error(diagnostics::type_modifier_on_named_type_import(self.cur_token().span()));
if can_parse_as_keyword && self.at(Kind::As) {
property_name = Some(name);
self.expect(Kind::As);
check_identifier_token = self.cur_token();
check_identifier_is_keyword =
self.cur_kind().is_any_keyword() && !self.cur_kind().is_identifier();
name = self.parse_module_export_name();
}
if import_kind == ImportOrExportKind::Type {
self.bump_any();

if self.is_ts && type_or_name_token_kind == Kind::Type && type_or_name_token.escaped() {
self.error(diagnostics::escaped_keyword(type_or_name_token.span()));
}

match specifier_type {
ImportOrExport::Import => {
// `import type { type } from 'mod';`
if parent_kind == ImportOrExportKind::Type && kind == ImportOrExportKind::Type {
self.error(diagnostics::type_modifier_on_named_type_import(
type_or_name_token.span(),
));
}

if !name.is_identifier() {
self.error(diagnostics::identifier_expected(name.span()));
} else if check_identifier_is_keyword {
if check_identifier_token.kind().is_reserved_keyword() {
self.error(diagnostics::identifier_reserved_word(
check_identifier_token.span(),
check_identifier_token.kind().to_str(),
));
} else {
self.error(diagnostics::identifier_expected(check_identifier_token.span()));
}
}

ImportOrExportSpecifier::Import(self.ast.import_specifier(
self.end_span(specifier_span),
property_name.unwrap_or_else(|| name.clone()),
self.ast.binding_identifier(name.span(), name.name()),
kind,
))
}
ImportOrExport::Export => {
// `export type { type } from 'mod';`
if parent_kind == ImportOrExportKind::Type && kind == ImportOrExportKind::Type {
self.error(diagnostics::type_modifier_on_named_type_export(
type_or_name_token.span(),
));
}

let exported = match property_name {
Some(property_name) => property_name,
None => name.clone(),
};
ImportOrExportSpecifier::Export(self.ast.export_specifier(
self.end_span(specifier_span),
exported,
name,
kind,
))
}
}
let is_next_token_as = self.lookahead(|p| {
p.bump_any();
p.at(Kind::As)
});
let (imported, local) = if is_next_token_as {
let imported = self.parse_module_export_name();
self.bump(Kind::As);
let local = self.parse_binding_identifier();
(imported, local)
} else {
let local = self.parse_binding_identifier();
(self.ast.module_export_name_identifier_name(local.span, local.name), local)
};
self.ast.import_declaration_specifier_import_specifier(
self.end_span(specifier_span),
imported,
local,
import_kind,
)
}

// ModuleExportName :
Expand Down Expand Up @@ -624,55 +742,14 @@ impl<'a> ParserImpl<'a> {
ImportOrExportKind::Value
}

fn parse_export_named_specifier(
fn parse_export_specifier(
&mut self,
parent_export_kind: ImportOrExportKind,
) -> ExportSpecifier<'a> {
let specifier_span = self.start_span();
let checkpoint = self.checkpoint();
// export { type} // name: `type`
// export { type type } // name: `type` type-export: `true`
// export { type as } // name: `as` type-export: `true`
// export { type as as } // name: `type` type-export: `false` (aliased to `as`)
// export { type as as as } // name: `as` type-export: `true`, aliased to `as`
let mut export_kind = ImportOrExportKind::Value;
if self.is_ts && self.at(Kind::Type) {
self.bump_any();
if self.at(Kind::As) {
// { type as ...? }
self.bump_any();
if self.at(Kind::As) {
// { type as as ...? }
self.bump_any();
if self.can_parse_module_export_name() {
// { type as as something }
// { type as as "something" }
export_kind = ImportOrExportKind::Type;
}
} else if !self.can_parse_module_export_name() {
export_kind = ImportOrExportKind::Type;
}
} else if self.can_parse_module_export_name() {
// { type something }
export_kind = ImportOrExportKind::Type;
}
match self.parse_import_or_export_specifier(ImportOrExport::Export, parent_export_kind) {
ImportOrExportSpecifier::Export(specifier) => specifier,
ImportOrExportSpecifier::Import(_) => unreachable!(),
}
self.rewind(checkpoint);

// `export type { type bar } from 'foo';`
if parent_export_kind == ImportOrExportKind::Type && export_kind == ImportOrExportKind::Type
{
self.error(diagnostics::type_modifier_on_named_type_export(self.cur_token().span()));
}

if export_kind == ImportOrExportKind::Type {
self.bump_any();
}

let local = self.parse_module_export_name();
let exported =
if self.eat(Kind::As) { self.parse_module_export_name() } else { local.clone() };
self.ast.export_specifier(self.end_span(specifier_span), local, exported, export_kind)
}

fn can_parse_module_export_name(&self) -> bool {
Expand Down
4 changes: 2 additions & 2 deletions tasks/coverage/snapshots/parser_babel.snap
Original file line number Diff line number Diff line change
Expand Up @@ -8515,7 +8515,7 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typesc
3 │ export { bar as "\udbff\udfff" } // should not throw
╰────

× Unexpected token
× Identifier expected.
╭─[babel/packages/babel-parser/test/fixtures/es2022/module-string-names/import-local-is-string/input.js:1:10]
1 │ import { "foo" } from "foo";
· ─────
Expand Down Expand Up @@ -13268,7 +13268,7 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typesc
· ──
╰────

× Unexpected token
× Identifier expected.
╭─[babel/packages/babel-parser/test/fixtures/typescript/type-only-import-export-specifiers/import-invalid-type-only-as-string/input.ts:1:22]
1 │ import { type foo as "bar" } from "mod";
· ─────
Expand Down
Loading
Loading