mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
Reuse the classified export kind to avoid duplicate lookahead for type-only exports; remove the obsolete classifier. Local arm64: type-export fixtures take 4–9% less parse time; ordinary corpus is flat (−0.36%, −0.24% with tokens). Largest slower control: +1.5%. Validated parser and Node suites, unchanged conformance and allocations, and 63,568 differential comparisons. AI assistance: Codex.
1446 lines
62 KiB
Rust
1446 lines
62 KiB
Rust
use oxc_allocator::{ArenaBox, ArenaVec, GetAllocator};
|
|
use oxc_ast::ast::*;
|
|
use oxc_span::GetSpan;
|
|
use rustc_hash::FxHashMap;
|
|
|
|
use super::FunctionKind;
|
|
use crate::{
|
|
ParserConfig as Config, ParserImpl, StatementContext, diagnostics,
|
|
lexer::Kind,
|
|
modifiers::{Modifier, ModifierKind, 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, C: Config> ParserImpl<'a, C> {
|
|
/// [Import Call](https://tc39.es/ecma262/#sec-import-calls)
|
|
/// `ImportCall` : import ( `AssignmentExpression` )
|
|
pub(crate) fn parse_import_expression(
|
|
&mut self,
|
|
start: u32,
|
|
phase: Option<ImportPhase>,
|
|
) -> Expression<'a> {
|
|
self.expect(Kind::LParen);
|
|
if self.eat(Kind::RParen) {
|
|
let error = diagnostics::import_requires_a_specifier(self.end_span(start));
|
|
return self.fatal_error(error);
|
|
}
|
|
let has_in = self.ctx.has_in();
|
|
self.ctx = self.ctx.and_in(true);
|
|
let expression = self.parse_import_argument();
|
|
let arguments = if self.eat(Kind::Comma) && !self.at(Kind::RParen) {
|
|
Some(self.parse_import_argument())
|
|
} else {
|
|
None
|
|
};
|
|
// Allow trailing comma
|
|
self.bump(Kind::Comma);
|
|
if !self.eat(Kind::RParen) {
|
|
let error = diagnostics::import_arguments(self.end_span(start));
|
|
return self.fatal_error(error);
|
|
}
|
|
self.ctx = self.ctx.and_in(has_in);
|
|
let expr =
|
|
ImportExpression::boxed(self.end_span(start), expression, arguments, phase, self);
|
|
self.module_record_builder.visit_import_expression(&expr);
|
|
Expression::ImportExpression(expr)
|
|
}
|
|
|
|
/// An argument of `ImportCall`. Spread elements are not allowed.
|
|
fn parse_import_argument(&mut self) -> Expression<'a> {
|
|
if self.at(Kind::Dot3) {
|
|
let error = diagnostics::dynamic_import_argument_spread(self.cur_token().span());
|
|
return self.fatal_error(error);
|
|
}
|
|
self.parse_assignment_expression_or_higher()
|
|
}
|
|
|
|
/// Section 16.2.2 Import Declaration
|
|
pub(crate) fn parse_import_declaration(
|
|
&mut self,
|
|
start: u32,
|
|
should_record_module_record: bool,
|
|
) -> Statement<'a> {
|
|
let token_after_import = self.cur_token();
|
|
let mut identifier_after_import: Option<BindingIdentifier<'_>> =
|
|
if self.cur_kind().is_binding_identifier() {
|
|
// `import something ...`
|
|
Some(self.parse_known_binding_identifier())
|
|
} else {
|
|
// `import ...`
|
|
None
|
|
};
|
|
let mut has_default_specifier = identifier_after_import.is_some();
|
|
let mut should_parse_specifiers = true;
|
|
|
|
let mut phase = None;
|
|
let mut import_kind = ImportOrExportKind::Value;
|
|
|
|
if self.at(Kind::Eq)
|
|
&& let Some(identifier_after_import) = identifier_after_import
|
|
{
|
|
// `import something = ...`
|
|
let decl = self.parse_ts_import_equals_declaration(
|
|
ImportOrExportKind::Value,
|
|
identifier_after_import,
|
|
start,
|
|
);
|
|
return Statement::from(decl);
|
|
} else if self.is_ts && token_after_import.kind() == Kind::Type {
|
|
// `import type ...`
|
|
|
|
let kind = self.cur_kind();
|
|
if kind == Kind::LCurly || kind == Kind::Star {
|
|
// `import type { ...`
|
|
// `import type * ...`
|
|
if token_after_import.escaped() {
|
|
self.error(diagnostics::escaped_keyword(token_after_import.span()));
|
|
}
|
|
import_kind = ImportOrExportKind::Type;
|
|
has_default_specifier = false;
|
|
} else if kind.is_binding_identifier() {
|
|
// `import type something ...`
|
|
let token = self.cur_token();
|
|
let identifier_after_type = self.parse_known_binding_identifier();
|
|
if token.kind() == Kind::From && self.at(Kind::Str) {
|
|
// `import type from 'source'`
|
|
if token.escaped() {
|
|
self.error(diagnostics::escaped_keyword(token.span()));
|
|
}
|
|
has_default_specifier = true;
|
|
import_kind = ImportOrExportKind::Value;
|
|
should_parse_specifiers = false;
|
|
} else {
|
|
if token_after_import.escaped() {
|
|
self.error(diagnostics::escaped_keyword(token_after_import.span()));
|
|
}
|
|
identifier_after_import = Some(identifier_after_type);
|
|
import_kind = ImportOrExportKind::Type;
|
|
|
|
if self.at(Kind::Eq) {
|
|
// `import type something = ...`
|
|
let decl = self.parse_ts_import_equals_declaration(
|
|
ImportOrExportKind::Type,
|
|
identifier_after_import.unwrap(),
|
|
start,
|
|
);
|
|
return Statement::from(decl);
|
|
} else if self.at(Kind::From) {
|
|
// `import type something from ...`
|
|
has_default_specifier = true;
|
|
should_parse_specifiers = false;
|
|
}
|
|
}
|
|
}
|
|
} else if token_after_import.kind() == Kind::Defer {
|
|
if self.at(Kind::Star) {
|
|
// `import defer * ...`
|
|
phase = Some(ImportPhase::Defer);
|
|
has_default_specifier = false;
|
|
} else if self.at(Kind::LCurly) {
|
|
// `import defer { ... } from 'source'`
|
|
self.error(diagnostics::named_import_not_allowed_in_defer(
|
|
token_after_import.span(),
|
|
));
|
|
phase = Some(ImportPhase::Defer);
|
|
has_default_specifier = false;
|
|
} else if self.cur_kind().is_binding_identifier()
|
|
&& self.cur_kind() != Kind::From
|
|
&& self.cur_kind() != Kind::As
|
|
{
|
|
// `import defer x from 'source'`
|
|
self.error(diagnostics::default_import_not_allowed_in_defer(
|
|
token_after_import.span(),
|
|
));
|
|
// Parse the identifier as the default specifier to continue recovery
|
|
identifier_after_import = Some(self.parse_known_binding_identifier());
|
|
phase = Some(ImportPhase::Defer);
|
|
}
|
|
// else: `import defer from 'source'` - defer is the binding name, no phase
|
|
} else if token_after_import.kind() == Kind::Source {
|
|
if self.cur_kind().is_binding_identifier() {
|
|
// `import source something ...`
|
|
let token = self.cur_token();
|
|
let identifier_after_source = self.parse_known_binding_identifier();
|
|
if token.kind() == Kind::From {
|
|
// `import source from ...`
|
|
if self.at(Kind::From) {
|
|
// `import source from from ...`
|
|
identifier_after_import = Some(identifier_after_source);
|
|
phase = Some(ImportPhase::Source);
|
|
has_default_specifier = true;
|
|
} else if self.at(Kind::Str) {
|
|
// `import source from 'source'`
|
|
if token.escaped() {
|
|
self.error(diagnostics::escaped_keyword(token.span()));
|
|
}
|
|
has_default_specifier = true;
|
|
should_parse_specifiers = false;
|
|
}
|
|
} else {
|
|
self.expect_without_advance(Kind::From);
|
|
// `import source something from ...`
|
|
identifier_after_import = Some(identifier_after_source);
|
|
phase = Some(ImportPhase::Source);
|
|
has_default_specifier = true;
|
|
}
|
|
} else if self.at(Kind::Star) {
|
|
// `import source * as ns from 'source'`
|
|
self.error(diagnostics::only_default_import_allowed_in_source_phase(
|
|
token_after_import.span(),
|
|
));
|
|
phase = Some(ImportPhase::Source);
|
|
has_default_specifier = false;
|
|
} else if self.at(Kind::LCurly) {
|
|
// `import source { ... } from 'source'`
|
|
self.error(diagnostics::only_default_import_allowed_in_source_phase(
|
|
token_after_import.span(),
|
|
));
|
|
phase = Some(ImportPhase::Source);
|
|
has_default_specifier = false;
|
|
}
|
|
// else: `import source from 'source'` - source is the binding name, no phase
|
|
}
|
|
|
|
// The first identifier is a keyword only after resolving the import phase.
|
|
if phase.is_some() && token_after_import.escaped() {
|
|
self.error(diagnostics::escaped_keyword(token_after_import.span()));
|
|
}
|
|
|
|
let specifiers = if self.at(Kind::Str) {
|
|
if has_default_specifier && !should_parse_specifiers {
|
|
match identifier_after_import {
|
|
Some(identifier_after_import) => {
|
|
// Special case: `import type from 'source'` where we already consumed `type` and `from`
|
|
Some(ArenaVec::from_value_in(
|
|
ImportDeclarationSpecifier::new_import_default_specifier(
|
|
identifier_after_import.span,
|
|
identifier_after_import,
|
|
self,
|
|
),
|
|
self,
|
|
))
|
|
}
|
|
None => unreachable!(),
|
|
}
|
|
} else {
|
|
if has_default_specifier {
|
|
// `import something 'source'`
|
|
self.expect_without_advance(Kind::From);
|
|
}
|
|
None
|
|
}
|
|
} else {
|
|
let default_specifier =
|
|
if has_default_specifier { identifier_after_import } else { None };
|
|
|
|
Some(self.parse_import_declaration_specifiers(default_specifier, import_kind))
|
|
};
|
|
|
|
let source = self.parse_literal_string();
|
|
let with_clause = self.parse_import_attributes();
|
|
self.asi();
|
|
let span = self.end_span(start);
|
|
|
|
let import_decl = ImportDeclaration::boxed(
|
|
span,
|
|
specifiers,
|
|
source,
|
|
phase,
|
|
with_clause,
|
|
import_kind,
|
|
self,
|
|
);
|
|
|
|
if should_record_module_record {
|
|
self.module_record_builder.visit_import_declaration(&import_decl);
|
|
}
|
|
|
|
Statement::ImportDeclaration(import_decl)
|
|
}
|
|
|
|
// Full Syntax: <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#syntax>
|
|
fn parse_import_declaration_specifiers(
|
|
&mut self,
|
|
// A default specifier, if we already saw any identifier after `import`
|
|
default_specifier: Option<BindingIdentifier<'a>>,
|
|
import_kind: ImportOrExportKind,
|
|
) -> ArenaVec<'a, ImportDeclarationSpecifier<'a>> {
|
|
// If there is a default specifier, create a Vec with the default specifier in it,
|
|
// otherwise, create an empty Vec.
|
|
let mut specifiers = if default_specifier.is_some() {
|
|
ArenaVec::with_capacity_in(1, self)
|
|
} else {
|
|
ArenaVec::new_in(self)
|
|
};
|
|
|
|
if let Some(default_specifier) = default_specifier {
|
|
let default_span = default_specifier.span;
|
|
let specifier = ImportDeclarationSpecifier::new_import_default_specifier(
|
|
default_specifier.span,
|
|
default_specifier,
|
|
self,
|
|
);
|
|
specifiers.push(specifier);
|
|
if self.eat(Kind::Comma) {
|
|
match self.cur_kind() {
|
|
// import defaultExport, * as name from "module-name";
|
|
Kind::Star => {
|
|
if self.is_ts && import_kind == ImportOrExportKind::Type {
|
|
self.error(diagnostics::type_only_import_default_and_named(
|
|
default_span,
|
|
));
|
|
}
|
|
let specifier = self.parse_import_namespace_specifier();
|
|
specifiers.push(specifier);
|
|
}
|
|
// import defaultExport, { export1 [ , [...] ] } from "module-name";
|
|
Kind::LCurly => {
|
|
if self.is_ts && import_kind == ImportOrExportKind::Type {
|
|
self.error(diagnostics::type_only_import_default_and_named(
|
|
default_span,
|
|
));
|
|
}
|
|
self.parse_import_specifiers_into(&mut specifiers, import_kind);
|
|
}
|
|
_ => return self.unexpected(),
|
|
}
|
|
}
|
|
} else if self.at(Kind::Star) {
|
|
// import * as name from "module-name";
|
|
let specifier = self.parse_import_namespace_specifier();
|
|
specifiers.push(specifier);
|
|
} else if self.at(Kind::LCurly) {
|
|
// import { export1 , export2 as alias2 , [...] } from "module-name";
|
|
self.parse_import_specifiers_into(&mut specifiers, import_kind);
|
|
}
|
|
|
|
self.expect(Kind::From);
|
|
specifiers
|
|
}
|
|
|
|
// import * as name from "module-name"
|
|
fn parse_import_namespace_specifier(&mut self) -> ImportDeclarationSpecifier<'a> {
|
|
let start = self.cur_start();
|
|
self.bump_any(); // advance `*`
|
|
self.expect(Kind::As);
|
|
let local = self.parse_binding_identifier();
|
|
let span = self.end_span(start);
|
|
ImportDeclarationSpecifier::new_import_namespace_specifier(span, local, self)
|
|
}
|
|
|
|
// import { export1 , export2 as alias2 , [...] } from "module-name";
|
|
fn parse_import_specifiers_into(
|
|
&mut self,
|
|
specifiers: &mut ArenaVec<'a, ImportDeclarationSpecifier<'a>>,
|
|
import_kind: ImportOrExportKind,
|
|
) {
|
|
debug_assert!(self.at(Kind::LCurly));
|
|
let opening_span = self.cur_token().span();
|
|
self.advance(Kind::LCurly);
|
|
self.context_remove(self.ctx, |p| {
|
|
let _ = p.parse_delimited_list_into(
|
|
specifiers,
|
|
Kind::RCurly,
|
|
Kind::Comma,
|
|
opening_span,
|
|
|parser| parser.parse_import_specifier(import_kind),
|
|
);
|
|
});
|
|
self.expect(Kind::RCurly);
|
|
}
|
|
|
|
/// [Import Attributes](https://tc39.es/proposal-import-attributes)
|
|
fn parse_import_attributes(&mut self) -> Option<ArenaBox<'a, WithClause<'a>>> {
|
|
let keyword_kind = self.cur_kind();
|
|
let keyword = match keyword_kind {
|
|
Kind::With => WithClauseKeyword::With,
|
|
Kind::Assert if !self.cur_token().is_on_new_line() => WithClauseKeyword::Assert,
|
|
_ => return None,
|
|
};
|
|
self.advance(keyword_kind);
|
|
|
|
let start = self.cur_start();
|
|
let opening_span = self.cur_token().span();
|
|
self.expect(Kind::LCurly);
|
|
let (with_entries, _) = self.context_remove(self.ctx, |p| {
|
|
p.parse_delimited_list(
|
|
Kind::RCurly,
|
|
Kind::Comma,
|
|
opening_span,
|
|
Self::parse_import_attribute,
|
|
)
|
|
});
|
|
self.expect(Kind::RCurly);
|
|
|
|
let mut keys = FxHashMap::default();
|
|
for e in &with_entries {
|
|
let key = e.key.as_js_str();
|
|
let span = e.key.span();
|
|
if let Some(old_span) = keys.insert(key, span) {
|
|
let name = key.as_str().unwrap_or_else(|| span.source_text(self.source_text));
|
|
self.error(diagnostics::duplicate_import_attribute_key(name, old_span, span));
|
|
}
|
|
}
|
|
|
|
Some(WithClause::boxed(self.end_span(start), keyword, with_entries, self))
|
|
}
|
|
|
|
fn parse_import_attribute(&mut self) -> ImportAttribute<'a> {
|
|
let start = self.cur_start();
|
|
let key = match self.cur_kind() {
|
|
Kind::Str => ImportAttributeKey::StringLiteral(self.parse_known_literal_string()),
|
|
_ => ImportAttributeKey::Identifier(self.parse_identifier_name()),
|
|
};
|
|
self.expect(Kind::Colon);
|
|
if !self.at(Kind::Str) {
|
|
return self.fatal_error(diagnostics::import_attribute_value_must_be_string_literal(
|
|
self.cur_token().span(),
|
|
));
|
|
}
|
|
let value = self.parse_known_literal_string();
|
|
ImportAttribute::new(self.end_span(start), key, value, self)
|
|
}
|
|
|
|
pub(crate) fn parse_ts_export_assignment_declaration(
|
|
&mut self,
|
|
start: u32,
|
|
) -> ArenaBox<'a, TSExportAssignment<'a>> {
|
|
debug_assert!(self.at(Kind::Eq));
|
|
self.advance(Kind::Eq);
|
|
let expression = self.parse_assignment_expression_or_higher();
|
|
self.asi();
|
|
if self.ctx.has_top_level() {
|
|
self.module_record_builder.set_module_syntax();
|
|
}
|
|
TSExportAssignment::boxed(self.end_span(start), expression, self)
|
|
}
|
|
|
|
pub(crate) fn parse_ts_export_namespace(
|
|
&mut self,
|
|
start: u32,
|
|
) -> ArenaBox<'a, TSNamespaceExportDeclaration<'a>> {
|
|
debug_assert!(self.at(Kind::As));
|
|
self.advance(Kind::As);
|
|
self.expect(Kind::Namespace);
|
|
let id = self.parse_identifier_name();
|
|
self.asi();
|
|
if self.ctx.has_top_level() {
|
|
self.module_record_builder.set_module_syntax();
|
|
}
|
|
TSNamespaceExportDeclaration::boxed(self.end_span(start), id, self)
|
|
}
|
|
|
|
/// Check the parse goal and placement of the `import` / `export` declaration at the current token.
|
|
pub(crate) fn check_module_declaration(&mut self, stmt_ctx: StatementContext) {
|
|
// TypeScript cannot tell script from module by syntax,
|
|
// and permits module declarations in namespaces and ambient modules.
|
|
if self.is_ts {
|
|
return;
|
|
}
|
|
let statement = if self.at(Kind::Import) { "import statement" } else { "export statement" };
|
|
let span = self.cur_token().span();
|
|
if self.source_type.is_script() || self.source_type.is_commonjs() {
|
|
self.error(diagnostics::module_code(statement, span));
|
|
} else if stmt_ctx != StatementContext::Program {
|
|
self.error(diagnostics::top_level(statement, span));
|
|
}
|
|
}
|
|
|
|
/// [Exports](https://tc39.es/ecma262/#sec-exports)
|
|
pub(crate) fn parse_export_declaration(
|
|
&mut self,
|
|
start: u32,
|
|
mut decorators: ArenaVec<'a, Decorator<'a>>,
|
|
stmt_ctx: StatementContext,
|
|
) -> Statement<'a> {
|
|
self.check_module_declaration(stmt_ctx);
|
|
self.bump_any(); // bump `export`
|
|
// export /* @__NO_SIDE_EFFECTS__ */ ...
|
|
let no_side_effects_comments =
|
|
self.lexer.trivia_builder.previous_token_no_side_effects_comments();
|
|
// `export` is unambiguously module syntax (ECMA-262 §16.2.3): commit to the
|
|
// Module goal so the declaration parses under `Await` on the first pass and
|
|
// isn't reparsed. e.g. `@foo export default class C { x = await + 1 }`
|
|
if self.source_type.is_unambiguous() && self.ctx.has_top_level() {
|
|
self.ctx = self.ctx.and_await(true);
|
|
}
|
|
let decl = match self.cur_kind() {
|
|
// `export import A = B`
|
|
Kind::Import => {
|
|
let import_start = self.cur_start();
|
|
self.bump_any();
|
|
// Pass `should_record_module_record: false` to prevent an `import` module record
|
|
// being created. It's an export not an import.
|
|
let stmt = self.parse_import_declaration(import_start, false);
|
|
if stmt.is_declaration() {
|
|
let export_decl = ExportDeclaration::boxed(
|
|
self.end_span(start),
|
|
stmt.into_declaration(),
|
|
self,
|
|
);
|
|
if self.ctx.has_top_level() {
|
|
self.module_record_builder.visit_export_declaration(&export_decl);
|
|
}
|
|
ModuleDeclaration::ExportDeclaration(export_decl)
|
|
} else {
|
|
return self.fatal_error(diagnostics::unexpected_export(stmt.span()));
|
|
}
|
|
}
|
|
Kind::At => {
|
|
let class_start = self.cur_start();
|
|
let after_export_decorators = self.parse_decorators();
|
|
if !decorators.is_empty() {
|
|
for decorator in &after_export_decorators {
|
|
self.error(diagnostics::decorators_in_export_and_class(decorator.span));
|
|
}
|
|
}
|
|
decorators.extend(after_export_decorators);
|
|
let modifiers = self.parse_modifiers(false, false);
|
|
let class_decl = self.parse_class_declaration(class_start, &modifiers, decorators);
|
|
let decl = Declaration::ClassDeclaration(class_decl);
|
|
let export_decl = ExportDeclaration::boxed(self.end_span(start), decl, self);
|
|
if self.ctx.has_top_level() {
|
|
self.module_record_builder.visit_export_declaration(&export_decl);
|
|
}
|
|
ModuleDeclaration::ExportDeclaration(export_decl)
|
|
}
|
|
Kind::Eq if self.is_ts => ModuleDeclaration::TSExportAssignment(
|
|
self.parse_ts_export_assignment_declaration(start),
|
|
),
|
|
Kind::As if self.is_ts && self.lexer.peek_token().kind() == Kind::Namespace => {
|
|
// `export as namespace ...`
|
|
ModuleDeclaration::TSNamespaceExportDeclaration(
|
|
self.parse_ts_export_namespace(start),
|
|
)
|
|
}
|
|
Kind::Default => ModuleDeclaration::ExportDefaultDeclaration(
|
|
self.parse_export_default_declaration(start, decorators),
|
|
),
|
|
Kind::Star => ModuleDeclaration::ExportAllDeclaration(
|
|
self.parse_export_all_declaration(start, ImportOrExportKind::Value),
|
|
),
|
|
Kind::LCurly => self.parse_export_named_specifiers(start, ImportOrExportKind::Value),
|
|
Kind::Type if self.is_ts => {
|
|
let next_kind = self.lexer.peek_token().kind();
|
|
|
|
match next_kind {
|
|
// `export type { ...`
|
|
Kind::LCurly => {
|
|
self.bump_any(); // bump `type`
|
|
self.parse_export_named_specifiers(start, ImportOrExportKind::Type)
|
|
}
|
|
// `export type * as ...`
|
|
Kind::Star => {
|
|
self.bump_any(); // bump `type`
|
|
ModuleDeclaration::ExportAllDeclaration(
|
|
self.parse_export_all_declaration(start, ImportOrExportKind::Type),
|
|
)
|
|
}
|
|
_ => ModuleDeclaration::ExportDeclaration(
|
|
self.parse_exported_declaration(start, decorators),
|
|
),
|
|
}
|
|
}
|
|
_ => {
|
|
if self.at(Kind::Export) {
|
|
self.error(diagnostics::modifier_already_seen(Modifier::new(
|
|
self.cur_token().start(),
|
|
ModifierKind::Export,
|
|
)));
|
|
self.bump_any();
|
|
}
|
|
ModuleDeclaration::ExportDeclaration(
|
|
self.parse_exported_declaration(start, decorators),
|
|
)
|
|
}
|
|
};
|
|
let mut stmt = Statement::from(decl);
|
|
if let Some(comments) = no_side_effects_comments {
|
|
self.apply_no_side_effects_comments_to_statement(comments, &mut stmt);
|
|
}
|
|
stmt
|
|
}
|
|
|
|
// export NamedExports ;
|
|
// NamedExports :
|
|
// { }
|
|
// { ExportsList }
|
|
// { ExportsList , }
|
|
// ExportsList :
|
|
// ExportSpecifier
|
|
// ExportsList , ExportSpecifier
|
|
// ExportSpecifier :
|
|
// ModuleExportName
|
|
// ModuleExportName as ModuleExportName
|
|
fn parse_export_named_specifiers(
|
|
&mut self,
|
|
start: u32,
|
|
export_kind: ImportOrExportKind,
|
|
) -> ModuleDeclaration<'a> {
|
|
let opening_span = self.cur_token().span();
|
|
self.expect(Kind::LCurly);
|
|
let (mut specifiers, _) = self.context_remove(self.ctx, |p| {
|
|
p.parse_delimited_list(Kind::RCurly, Kind::Comma, opening_span, |parser| {
|
|
parser.parse_export_specifier(export_kind)
|
|
})
|
|
});
|
|
self.expect(Kind::RCurly);
|
|
let (source, with_clause) = if self.eat(Kind::From) {
|
|
let source = self.parse_literal_string();
|
|
(Some(source), self.parse_import_attributes())
|
|
} else {
|
|
(None, None)
|
|
};
|
|
|
|
// ExportDeclaration : export NamedExports ;
|
|
if source.is_none() {
|
|
for specifier in &mut specifiers {
|
|
match &specifier.local {
|
|
// It is a Syntax Error if ReferencedBindings of NamedExports contains any StringLiterals.
|
|
ModuleExportName::StringLiteral(literal) => {
|
|
let local = self.allocator().alloc_str(&specifier.local.to_string());
|
|
let exported = self.allocator().alloc_str(&specifier.exported.to_string());
|
|
self.error(diagnostics::export_named_string(local, exported, literal.span));
|
|
}
|
|
// For each IdentifierName n in ReferencedBindings of NamedExports:
|
|
// It is a Syntax Error if StringValue of n is a ReservedWord or the StringValue of n
|
|
// is one of "implements", "interface", "let", "package", "private", "protected", "public", or "static".
|
|
ModuleExportName::IdentifierName(ident) => {
|
|
let match_result = Kind::match_keyword(&ident.name);
|
|
if match_result.is_reserved_keyword()
|
|
|| match_result.is_future_reserved_keyword()
|
|
{
|
|
let local = self.allocator().alloc_str(&specifier.local.to_string());
|
|
let exported =
|
|
self.allocator().alloc_str(&specifier.exported.to_string());
|
|
self.error(diagnostics::export_reserved_word(
|
|
local, exported, ident.span,
|
|
));
|
|
}
|
|
|
|
// `local` becomes a reference for `export { local }`.
|
|
specifier.local = ModuleExportName::new_identifier_reference(
|
|
ident.span, ident.name, self,
|
|
);
|
|
}
|
|
// No prior code path should lead to parsing `ModuleExportName` as `IdentifierReference`.
|
|
ModuleExportName::IdentifierReference(_) => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
self.asi();
|
|
let span = self.end_span(start);
|
|
if let Some(source) = source {
|
|
let export_from_decl = ExportFromDeclaration::boxed(
|
|
span,
|
|
specifiers,
|
|
source,
|
|
export_kind,
|
|
with_clause,
|
|
self,
|
|
);
|
|
if self.ctx.has_top_level() {
|
|
self.module_record_builder.visit_export_from_declaration(&export_from_decl);
|
|
}
|
|
ModuleDeclaration::ExportFromDeclaration(export_from_decl)
|
|
} else {
|
|
let export_named_decl =
|
|
ExportNamedDeclaration::boxed(span, specifiers, export_kind, self);
|
|
if self.ctx.has_top_level() {
|
|
self.module_record_builder.visit_export_named_declaration(&export_named_decl);
|
|
}
|
|
ModuleDeclaration::ExportNamedDeclaration(export_named_decl)
|
|
}
|
|
}
|
|
|
|
// export Declaration
|
|
fn parse_exported_declaration(
|
|
&mut self,
|
|
start: u32,
|
|
decorators: ArenaVec<'a, Decorator<'a>>,
|
|
) -> ArenaBox<'a, ExportDeclaration<'a>> {
|
|
let decl_start = self.cur_start();
|
|
let reserved_ctx = self.ctx;
|
|
let modifiers =
|
|
if self.is_ts { self.eat_modifiers_before_declaration() } else { Modifiers::empty() };
|
|
self.ctx = self.ctx.union_ambient_if(modifiers.contains_declare());
|
|
|
|
let declaration = self.parse_declaration(decl_start, &modifiers, decorators);
|
|
self.ctx = reserved_ctx;
|
|
let export_decl = ExportDeclaration::boxed(self.end_span(start), declaration, self);
|
|
if self.ctx.has_top_level() {
|
|
self.module_record_builder.visit_export_declaration(&export_decl);
|
|
}
|
|
export_decl
|
|
}
|
|
|
|
// export default HoistableDeclaration[~Yield, +Await, +Default]
|
|
// export default ClassDeclaration[~Yield, +Await, +Default]
|
|
// export default AssignmentExpression[+In, ~Yield, +Await] ;
|
|
fn parse_export_default_declaration(
|
|
&mut self,
|
|
start: u32,
|
|
decorators: ArenaVec<'a, Decorator<'a>>,
|
|
) -> ArenaBox<'a, ExportDefaultDeclaration<'a>> {
|
|
let default_keyword_span = self.cur_token().span();
|
|
self.advance(Kind::Default);
|
|
let declaration = self.parse_export_default_declaration_kind(decorators);
|
|
let span = self.end_span(start);
|
|
let export_default_decl = ExportDefaultDeclaration::boxed(span, declaration, self);
|
|
if self.ctx.has_top_level() {
|
|
self.module_record_builder
|
|
.visit_export_default_declaration(&export_default_decl, default_keyword_span);
|
|
}
|
|
export_default_decl
|
|
}
|
|
|
|
fn parse_export_default_declaration_kind(
|
|
&mut self,
|
|
mut decorators: ArenaVec<'a, Decorator<'a>>,
|
|
) -> ExportDefaultDeclarationKind<'a> {
|
|
let decl_start = self.cur_start();
|
|
|
|
// export default /* @__NO_SIDE_EFFECTS__ */ ...
|
|
let no_side_effects_comments =
|
|
self.lexer.trivia_builder.previous_token_no_side_effects_comments();
|
|
|
|
// export default @decorator ...
|
|
if self.at(Kind::At) {
|
|
let after_export_decorators = self.parse_decorators();
|
|
// @decorator export default @decorator ...
|
|
if !decorators.is_empty() {
|
|
for decorator in &after_export_decorators {
|
|
self.error(diagnostics::decorators_in_export_and_class(decorator.span));
|
|
}
|
|
}
|
|
decorators.extend(after_export_decorators);
|
|
}
|
|
|
|
let function_start = self.cur_start();
|
|
|
|
// ExportDeclaration :
|
|
// export default HoistableDeclaration
|
|
// export default ClassDeclaration
|
|
// export default [lookahead ∉ { function, async [no LineTerminator here] function, class }]
|
|
// AssignmentExpression ;
|
|
// <https://tc39.es/ecma262/#prod-ExportDeclaration>
|
|
//
|
|
// `abstract`/`interface` (TS) and `async` are contextual keywords, so each can be the start
|
|
// of the `AssignmentExpression` (`export default async;`). Peek the token after the keyword
|
|
// to apply the lookahead restriction without consuming it, committing only when it confirms
|
|
// a declaration. `[no LineTerminator here]` maps to `!next.is_on_new_line()`.
|
|
let kind = self.cur_kind();
|
|
if matches!(kind, Kind::Abstract | Kind::Async | Kind::Interface) {
|
|
let modifier_start = self.cur_start();
|
|
let next = self.lexer.peek_token();
|
|
if !next.is_on_new_line() {
|
|
// export default abstract class C {}
|
|
if kind == Kind::Abstract && next.kind() == Kind::Class {
|
|
self.bump_any();
|
|
let modifiers = Modifiers::new_single(ModifierKind::Abstract, modifier_start);
|
|
return ExportDefaultDeclarationKind::ClassDeclaration(
|
|
self.parse_class_declaration(decl_start, &modifiers, decorators),
|
|
);
|
|
}
|
|
// export default async function f() {}
|
|
if kind == Kind::Async && next.kind() == Kind::Function {
|
|
self.bump_any();
|
|
for decorator in &decorators {
|
|
self.error(diagnostics::decorators_are_not_valid_here(decorator.span));
|
|
}
|
|
let mut func = self.parse_function_impl(
|
|
function_start,
|
|
/* r#async */ true,
|
|
FunctionKind::DefaultExport,
|
|
);
|
|
if let Some(comments) = no_side_effects_comments {
|
|
func.pure = true;
|
|
self.lexer.trivia_builder.mark_no_side_effects_comments_applied(comments);
|
|
}
|
|
return ExportDefaultDeclarationKind::FunctionDeclaration(func);
|
|
}
|
|
// export default interface I {}
|
|
if kind == Kind::Interface {
|
|
self.bump_any();
|
|
for decorator in &decorators {
|
|
self.error(diagnostics::decorators_are_not_valid_here(decorator.span));
|
|
}
|
|
if let Declaration::TSInterfaceDeclaration(decl) =
|
|
self.parse_ts_interface_declaration(modifier_start, &Modifiers::empty())
|
|
{
|
|
return ExportDefaultDeclarationKind::TSInterfaceDeclaration(decl);
|
|
}
|
|
}
|
|
}
|
|
// Used as an identifier (`export default async;`); nothing consumed, so fall through.
|
|
}
|
|
|
|
let kind = self.cur_kind();
|
|
// export default class C {}
|
|
if kind == Kind::Class {
|
|
return ExportDefaultDeclarationKind::ClassDeclaration(self.parse_class_declaration(
|
|
decl_start,
|
|
&Modifiers::empty(),
|
|
decorators,
|
|
));
|
|
}
|
|
|
|
for decorator in &decorators {
|
|
self.error(diagnostics::decorators_are_not_valid_here(decorator.span));
|
|
}
|
|
|
|
// export default function f() {}
|
|
if kind == Kind::Function {
|
|
let mut func = self.parse_function_impl(
|
|
function_start,
|
|
/* r#async */ false,
|
|
FunctionKind::DefaultExport,
|
|
);
|
|
if let Some(comments) = no_side_effects_comments {
|
|
func.pure = true;
|
|
self.lexer.trivia_builder.mark_no_side_effects_comments_applied(comments);
|
|
}
|
|
return ExportDefaultDeclarationKind::FunctionDeclaration(func);
|
|
}
|
|
|
|
// export default 1 + 1;
|
|
let decl = ExportDefaultDeclarationKind::from(self.parse_assignment_expression_or_higher());
|
|
self.asi();
|
|
decl
|
|
}
|
|
|
|
// export ExportFromClause FromClause ;
|
|
// ExportFromClause :
|
|
// *
|
|
// * as ModuleExportName
|
|
// NamedExports
|
|
fn parse_export_all_declaration(
|
|
&mut self,
|
|
start: u32,
|
|
export_kind: ImportOrExportKind,
|
|
) -> ArenaBox<'a, ExportAllDeclaration<'a>> {
|
|
self.bump_any(); // bump `star`
|
|
let exported = self.eat(Kind::As).then(|| self.parse_module_export_name());
|
|
self.expect(Kind::From);
|
|
let source = self.parse_literal_string();
|
|
let with_clause = self.parse_import_attributes();
|
|
self.asi();
|
|
let span = self.end_span(start);
|
|
let export_all_decl =
|
|
ExportAllDeclaration::boxed(span, exported, source, with_clause, export_kind, self);
|
|
if self.ctx.has_top_level() {
|
|
self.module_record_builder.visit_export_all_declaration(&export_all_decl);
|
|
}
|
|
export_all_decl
|
|
}
|
|
|
|
// ImportSpecifier :
|
|
// ImportedBinding
|
|
// ModuleExportName as ImportedBinding
|
|
pub(crate) fn parse_import_specifier(
|
|
&mut self,
|
|
parent_import_kind: ImportOrExportKind,
|
|
) -> ImportDeclarationSpecifier<'a> {
|
|
match self.parse_import_or_export_specifier(ImportOrExport::Import, parent_import_kind) {
|
|
ImportOrExportSpecifier::Import(specifier) => {
|
|
ImportDeclarationSpecifier::ImportSpecifier(self.alloc(specifier))
|
|
}
|
|
ImportOrExportSpecifier::Export(_) => unreachable!(),
|
|
}
|
|
}
|
|
|
|
fn parse_import_or_export_specifier(
|
|
&mut self,
|
|
specifier_type: ImportOrExport,
|
|
parent_kind: ImportOrExportKind,
|
|
) -> ImportOrExportSpecifier<'a> {
|
|
let specifier_start = self.cur_start();
|
|
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 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 && 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 ...? }
|
|
let first_as = self.parse_known_identifier_name();
|
|
if self.at(Kind::As) {
|
|
// { type as as ...? }
|
|
let second_as = self.parse_known_identifier_name();
|
|
if self.can_parse_module_export_name() {
|
|
// { type as as something }
|
|
// { type as as "something" }
|
|
kind = ImportOrExportKind::Type;
|
|
property_name = Some(ModuleExportName::new_identifier_name(
|
|
first_as.span,
|
|
first_as.name,
|
|
self,
|
|
));
|
|
check_identifier_token = self.cur_token();
|
|
name = self.parse_module_export_name();
|
|
can_parse_as_keyword = false;
|
|
} else {
|
|
// { type as as }
|
|
property_name = Some(name);
|
|
name = ModuleExportName::new_identifier_name(
|
|
second_as.span,
|
|
second_as.name,
|
|
self,
|
|
);
|
|
can_parse_as_keyword = false;
|
|
}
|
|
} 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();
|
|
name = self.parse_module_export_name();
|
|
} else {
|
|
// { type as }
|
|
kind = ImportOrExportKind::Type;
|
|
name =
|
|
ModuleExportName::new_identifier_name(first_as.span, first_as.name, self);
|
|
}
|
|
} else if self.can_parse_module_export_name() {
|
|
// { type something ...? }
|
|
// { type "something" ...? }
|
|
kind = ImportOrExportKind::Type;
|
|
check_identifier_token = self.cur_token();
|
|
name = self.parse_module_export_name();
|
|
}
|
|
}
|
|
|
|
if can_parse_as_keyword && self.eat(Kind::As) {
|
|
property_name = Some(name);
|
|
check_identifier_token = self.cur_token();
|
|
name = self.parse_module_export_name();
|
|
}
|
|
|
|
if kind == ImportOrExportKind::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_token.kind().is_reserved_keyword() {
|
|
self.error(diagnostics::identifier_reserved_word(
|
|
check_identifier_token.span(),
|
|
check_identifier_token.kind().to_str(),
|
|
));
|
|
}
|
|
|
|
let local_name = name.name().as_str();
|
|
let local = BindingIdentifier::new(name.span(), self.ident(local_name), self);
|
|
let imported = property_name.unwrap_or(name);
|
|
ImportOrExportSpecifier::Import(ImportSpecifier::new(
|
|
self.end_span(specifier_start),
|
|
imported,
|
|
local,
|
|
kind,
|
|
self,
|
|
))
|
|
}
|
|
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 local = match property_name {
|
|
Some(property_name) => property_name,
|
|
None => self.duplicate_module_export_name(&name),
|
|
};
|
|
ImportOrExportSpecifier::Export(ExportSpecifier::new(
|
|
self.end_span(specifier_start),
|
|
local,
|
|
name,
|
|
kind,
|
|
self,
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
// ModuleExportName :
|
|
// IdentifierName
|
|
// StringLiteral
|
|
pub(crate) fn parse_module_export_name(&mut self) -> ModuleExportName<'a> {
|
|
match self.cur_kind() {
|
|
Kind::Str => {
|
|
let literal = self.parse_known_literal_string();
|
|
// ModuleExportName : StringLiteral
|
|
// It is a Syntax Error if IsStringWellFormedUnicode(the SV of StringLiteral) is false.
|
|
// The error is fatal so that no AST ever carries an ill-formed export name.
|
|
if !literal.is_string_well_formed_unicode() {
|
|
let error = diagnostics::export_lone_surrogate(literal.span);
|
|
return self.fatal_error(error);
|
|
}
|
|
ModuleExportName::StringLiteral(literal)
|
|
}
|
|
_ => ModuleExportName::IdentifierName(self.parse_identifier_name()),
|
|
}
|
|
}
|
|
|
|
/// Build a 2nd [`ModuleExportName`] node matching `name`.
|
|
///
|
|
/// Not `name.clone()` - `clone` copies `node_id` and the ID cells, bypassing the AST builder.
|
|
fn duplicate_module_export_name(&self, name: &ModuleExportName<'a>) -> ModuleExportName<'a> {
|
|
match name {
|
|
ModuleExportName::IdentifierName(ident) => {
|
|
ModuleExportName::new_identifier_name(ident.span, ident.name, self)
|
|
}
|
|
ModuleExportName::IdentifierReference(ident) => {
|
|
ModuleExportName::new_identifier_reference(ident.span, ident.name, self)
|
|
}
|
|
ModuleExportName::StringLiteral(literal) => {
|
|
ModuleExportName::new_string_literal(literal.span, literal.value, literal.raw, self)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_export_specifier(
|
|
&mut self,
|
|
parent_export_kind: ImportOrExportKind,
|
|
) -> ExportSpecifier<'a> {
|
|
match self.parse_import_or_export_specifier(ImportOrExport::Export, parent_export_kind) {
|
|
ImportOrExportSpecifier::Export(specifier) => specifier,
|
|
ImportOrExportSpecifier::Import(_) => unreachable!(),
|
|
}
|
|
}
|
|
|
|
fn can_parse_module_export_name(&self) -> bool {
|
|
self.cur_kind().is_identifier_name() || self.at(Kind::Str)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use oxc_allocator::{Allocator, ArenaBox};
|
|
use oxc_ast::ast::{ImportDeclarationSpecifier, ImportOrExportKind, ImportPhase, Statement};
|
|
use oxc_span::{GetSpan, SourceType, Span};
|
|
|
|
use crate::Parser;
|
|
#[test]
|
|
fn test_parse_import_declaration() {
|
|
let src = "import foo from 'bar';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "foo");
|
|
});
|
|
|
|
let src = "import type foo from 'bar';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Type);
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "foo");
|
|
});
|
|
|
|
let src = "import type from 'bar';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "type");
|
|
});
|
|
|
|
let src = "import type from from 'bar';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Type);
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "from");
|
|
assert!(matches!(specifiers[0], ImportDeclarationSpecifier::ImportDefaultSpecifier(_)));
|
|
});
|
|
|
|
let src = "import type type from 'bar';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Type);
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "type");
|
|
assert!(matches!(specifiers[0], ImportDeclarationSpecifier::ImportDefaultSpecifier(_)));
|
|
});
|
|
|
|
let src = "import type { type } from 'bar';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Type);
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "type");
|
|
});
|
|
|
|
let src = "import './a'";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert!(decl.specifiers.is_none());
|
|
});
|
|
|
|
let src = "import type, { bar } from 'foo';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 2);
|
|
assert_eq!(specifiers[0].name(), "type");
|
|
assert_eq!(specifiers[1].name(), "bar");
|
|
});
|
|
|
|
let src = "import { foo, bar } from 'baz';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert!(decl.specifiers.is_some());
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 2);
|
|
assert_eq!(specifiers[0].name(), "foo");
|
|
assert_eq!(specifiers[1].name(), "bar");
|
|
});
|
|
|
|
let src = "import type { foo, bar } from 'baz';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Type);
|
|
assert!(decl.specifiers.is_some());
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 2);
|
|
assert_eq!(specifiers[0].name(), "foo");
|
|
assert_eq!(specifiers[1].name(), "bar");
|
|
});
|
|
|
|
let src = "import type { from, type } from 'baz';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Type);
|
|
assert!(decl.specifiers.is_some());
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 2);
|
|
assert_eq!(specifiers[0].name(), "from");
|
|
assert_eq!(specifiers[1].name(), "type");
|
|
});
|
|
|
|
let src = "import defaultItem, { type from, type } from 'baz';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert!(decl.specifiers.is_some());
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 3);
|
|
assert_eq!(specifiers[0].name(), "defaultItem");
|
|
assert_eq!(specifiers[1].name(), "from");
|
|
assert_eq!(specifiers[2].name(), "type");
|
|
});
|
|
|
|
let src = "import { type as as } from 'baz';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert!(decl.specifiers.is_some());
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
if let ImportDeclarationSpecifier::ImportSpecifier(specifier) = &specifiers[0] {
|
|
assert_eq!(specifier.local.name, "as");
|
|
assert_eq!(specifier.imported.name(), "type");
|
|
} else {
|
|
panic!("Expected ImportSpecifier, found: {:?}", specifiers[0]);
|
|
}
|
|
});
|
|
|
|
let src = "import { type as as x } from 'baz';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert!(decl.specifiers.is_some());
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
if let ImportDeclarationSpecifier::ImportSpecifier(specifier) = &specifiers[0] {
|
|
assert_eq!(specifier.import_kind, ImportOrExportKind::Type);
|
|
assert_eq!(specifier.imported.name(), "as");
|
|
// The first `as` (14-16) is the imported name; the second `as` (17-19) is the keyword
|
|
assert_eq!(specifier.imported.span(), Span::new(14, 16));
|
|
assert_eq!(specifier.local.name, "x");
|
|
assert_eq!(specifier.local.span, Span::new(20, 21));
|
|
} else {
|
|
panic!("Expected ImportSpecifier, found: {:?}", specifiers[0]);
|
|
}
|
|
});
|
|
|
|
let src = "import * as foo from 'bar';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "foo");
|
|
});
|
|
|
|
let src = "import type * as foo from 'bar';";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Type);
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "foo");
|
|
});
|
|
|
|
let src = "import defer * as ns from 'x'";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert_eq!(decl.phase, Some(ImportPhase::Defer));
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "ns");
|
|
});
|
|
|
|
let src = "import source x from 'source'";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert_eq!(decl.phase, Some(ImportPhase::Source));
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "x");
|
|
});
|
|
|
|
let src = "import source from 'source'";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert_eq!(decl.phase, None);
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "source");
|
|
});
|
|
|
|
let src = "import source from from 'source'";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert_eq!(decl.phase, Some(ImportPhase::Source));
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "from");
|
|
});
|
|
|
|
let src = "import source as from 'source'";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert_eq!(decl.phase, Some(ImportPhase::Source));
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "as");
|
|
});
|
|
|
|
let src = "import source source from 'source'";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert_eq!(decl.phase, Some(ImportPhase::Source));
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "source");
|
|
});
|
|
|
|
let src = "import defer from 'source'";
|
|
parse_and_assert_import_declarations(src, |declarations| {
|
|
assert_eq!(declarations.len(), 1);
|
|
let decl = declarations[0];
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert_eq!(decl.phase, None);
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers.len(), 1);
|
|
assert_eq!(specifiers[0].name(), "defer");
|
|
});
|
|
|
|
let src = "import foo = bar";
|
|
parse_and_assert_statements(src, |statements| {
|
|
if let Statement::TSImportEqualsDeclaration(decl) = statements[0] {
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert_eq!(decl.id.name, "foo");
|
|
} else {
|
|
panic!("Expected TSImportEqualsDeclaration, found: {:?}", statements[0]);
|
|
}
|
|
});
|
|
|
|
// `import type foo = bar` builds the AST but is a semantic error (TS1392:
|
|
// an import alias cannot use `import type`), reported by the parser.
|
|
let src = "import type foo = bar";
|
|
parse_and_assert_statements_with_error(src, |statements| {
|
|
if let Statement::TSImportEqualsDeclaration(decl) = statements[0] {
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Type);
|
|
assert_eq!(decl.id.name, "foo");
|
|
} else {
|
|
panic!("Expected TSImportEqualsDeclaration, found: {:?}", statements[0]);
|
|
}
|
|
});
|
|
|
|
let src = "import type from = require('./a')";
|
|
parse_and_assert_statements(src, |statements| {
|
|
if let Statement::TSImportEqualsDeclaration(decl) = statements[0] {
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Type);
|
|
assert_eq!(decl.id.name, "from");
|
|
} else {
|
|
panic!("Expected TSImportEqualsDeclaration, found: {:?}", statements[0]);
|
|
}
|
|
});
|
|
|
|
let src = "import from = b";
|
|
parse_and_assert_statements(src, |statements| {
|
|
if let Statement::TSImportEqualsDeclaration(decl) = statements[0] {
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert_eq!(decl.id.name, "from");
|
|
} else {
|
|
panic!("Expected TSImportEqualsDeclaration, found: {:?}", statements[0]);
|
|
}
|
|
});
|
|
}
|
|
|
|
// https://github.com/oxc-project/oxc/issues/11505
|
|
#[test]
|
|
fn test_type_from_js_file() {
|
|
let src = "import type from '../type.js'";
|
|
let source_type = SourceType::default().with_typescript(false);
|
|
let allocator = Allocator::default();
|
|
let ret = Parser::new(&allocator, src, source_type).parse();
|
|
assert!(
|
|
ret.diagnostics.is_empty(),
|
|
"Failed to parse source: {src:?}, error: {:?}",
|
|
ret.diagnostics
|
|
);
|
|
let declarations = ret.program.body.iter().collect::<Vec<_>>();
|
|
assert_eq!(declarations.len(), 1);
|
|
let Statement::ImportDeclaration(decl) = declarations[0] else {
|
|
panic!("Expected ImportDeclaration, found: {:?}", declarations[0]);
|
|
};
|
|
assert_eq!(decl.import_kind, ImportOrExportKind::Value);
|
|
assert!(decl.specifiers.is_some());
|
|
let specifiers = decl.specifiers.as_ref().unwrap();
|
|
assert_eq!(specifiers[0].name(), "type");
|
|
}
|
|
|
|
fn parse_and_assert_statements(
|
|
src: &'static str,
|
|
// takes a function which accepts the list of statements
|
|
f: fn(Vec<&oxc_ast::ast::Statement<'_>>) -> (),
|
|
) {
|
|
let source_type = SourceType::default().with_typescript(true);
|
|
let allocator = Allocator::default();
|
|
let ret = Parser::new(&allocator, src, source_type).parse();
|
|
assert!(
|
|
ret.diagnostics.is_empty(),
|
|
"Failed to parse source: {src:?}, error: {:?}",
|
|
ret.diagnostics
|
|
);
|
|
f(ret.program.body.iter().collect::<Vec<_>>());
|
|
}
|
|
|
|
/// Like [`parse_and_assert_statements`] but for a recoverable error: the parser
|
|
/// reports a diagnostic yet still builds the AST, which `f` asserts on.
|
|
fn parse_and_assert_statements_with_error(
|
|
src: &'static str,
|
|
f: fn(Vec<&oxc_ast::ast::Statement<'_>>) -> (),
|
|
) {
|
|
let source_type = SourceType::default().with_typescript(true);
|
|
let allocator = Allocator::default();
|
|
let ret = Parser::new(&allocator, src, source_type).parse();
|
|
assert!(!ret.diagnostics.is_empty(), "Expected a parse error for source: {src:?}");
|
|
f(ret.program.body.iter().collect::<Vec<_>>());
|
|
}
|
|
|
|
fn parse_and_assert_import_declarations(
|
|
src: &'static str,
|
|
// takes a function which accepts the list of statements
|
|
f: fn(Vec<&ArenaBox<'_, oxc_ast::ast::ImportDeclaration<'_>>>) -> (),
|
|
) {
|
|
let source_type = SourceType::default().with_typescript(true);
|
|
let allocator = Allocator::default();
|
|
let ret = Parser::new(&allocator, src, source_type).parse();
|
|
assert!(
|
|
ret.diagnostics.is_empty(),
|
|
"Failed to parse source: {src:?}, error: {:?}",
|
|
ret.diagnostics
|
|
);
|
|
let statements =
|
|
ret.program
|
|
.body
|
|
.iter()
|
|
.filter_map(|s| {
|
|
if let Statement::ImportDeclaration(decl) = s { Some(decl) } else { None }
|
|
})
|
|
.collect::<Vec<_>>();
|
|
f(statements);
|
|
}
|
|
}
|