mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
fix(parser): reject module syntax in script (#27220)
Fixes #25187, closes #25204. Previously, this check was performed by semantic phase, but since it can be detected in advance, it has been moved to the parser as per our previous policy. [Babel](https://ast-explorer.dev/#eNotjDEOwjAMRa8See4JunIFNsLgRgaC0thKXASKcnfcNtt/78lukGCGN36whhJFYQIxseBCyXawTV/hoq51YzZuPjvnofJWAl1/Qh7mnY97D9OZJW3PmOvebvfhEgfUyKd9YKo0AqpieF14XSnbixF97tD/NgY0iA==) also reports this in the parser. Conformance test harness also updated: - codegen: `if() { import }` is now invalid input - => use `declare module {}` for indent instead - transformer: Revert #26495 change - => defaults to `unambiguous` (Correct me if I'm wrong 🙏🏻)
This commit is contained in:
1 parent
66545bfa3c
commit
b2793fa2d8
17 files changed
+106
-91
No files matched your search
@@ -185,17 +185,17 @@ fn indented_statement_mappings_start_after_generated_indent() {
|
||||
// gen col 0 (whitespace) instead of the start of the keyword.
|
||||
#[test]
|
||||
fn top_level_decl_mappings_start_after_generated_indent() {
|
||||
// Wrap the imports/exports in `if (true) { ... }` so the body is
|
||||
// Wrap the imports/exports in an ambient module so the body is
|
||||
// indented, exposing the order of `add_source_mapping` vs `print_indent`.
|
||||
let tokens = sourcemap_tokens(
|
||||
r#"if (true) {
|
||||
r#"declare module "m" {
|
||||
"use strict";
|
||||
import { x } from "x";
|
||||
export { x } from "x";
|
||||
export * from "x";
|
||||
export default 1;
|
||||
}"#,
|
||||
SourceType::mjs(),
|
||||
SourceType::ts(),
|
||||
);
|
||||
|
||||
// Directive `"use strict"` source col 0 of line 1 → gen col 1 (after tab),
|
||||
|
||||
@@ -174,6 +174,9 @@ impl Context {
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum StatementContext {
|
||||
/// Direct child of `Program`.
|
||||
/// Unlike `Context::TopLevel`, this excludes blocks and other nested statements.
|
||||
Program,
|
||||
StatementList,
|
||||
If,
|
||||
Label,
|
||||
@@ -185,6 +188,6 @@ pub enum StatementContext {
|
||||
|
||||
impl StatementContext {
|
||||
pub(crate) fn is_single_statement(self) -> bool {
|
||||
self != Self::StatementList
|
||||
!matches!(self, Self::Program | Self::StatementList)
|
||||
}
|
||||
}
|
||||
@@ -885,6 +885,17 @@ parser_diagnostics! {
|
||||
OxcDiagnostic::error("A rest parameter cannot have an initializer.").with_label(span)
|
||||
};
|
||||
|
||||
top_level(statement: &'static str, span: Span) => {
|
||||
OxcDiagnostic::error(format!(
|
||||
"'{statement}' declaration can only be used at the top level of a module"
|
||||
))
|
||||
.with_label(span)
|
||||
};
|
||||
|
||||
module_code(statement: &'static str, span: Span) => {
|
||||
OxcDiagnostic::error(format!("Cannot use {statement} outside a module")).with_label(span)
|
||||
};
|
||||
|
||||
import_requires_a_specifier(span: Span) => {
|
||||
OxcDiagnostic::error("import() requires a specifier.").with_label(span)
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ use rustc_hash::FxHashMap;
|
||||
|
||||
use super::FunctionKind;
|
||||
use crate::{
|
||||
ParserConfig as Config, ParserImpl, diagnostics,
|
||||
ParserConfig as Config, ParserImpl, StatementContext, diagnostics,
|
||||
lexer::Kind,
|
||||
modifiers::{Modifier, ModifierKind, Modifiers},
|
||||
};
|
||||
@@ -452,12 +452,30 @@ impl<'a, C: Config> ParserImpl<'a, C> {
|
||||
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` 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
|
||||
|
||||
@@ -38,7 +38,8 @@ impl<'a, C: Config> ParserImpl<'a, C> {
|
||||
let mut statements = ArenaVec::new_in(self);
|
||||
|
||||
let is_top_level = self.ctx.has_top_level();
|
||||
let stmt_ctx = StatementContext::StatementList;
|
||||
let stmt_ctx =
|
||||
if is_top_level { StatementContext::Program } else { StatementContext::StatementList };
|
||||
let is_ambient_block = (is_top_level || in_ts_namespace_body) && self.ctx.has_ambient();
|
||||
let mut reported_ambient_statement = false;
|
||||
|
||||
@@ -155,7 +156,9 @@ impl<'a, C: Config> ParserImpl<'a, C> {
|
||||
&Modifiers::empty(),
|
||||
ArenaVec::new_in(self),
|
||||
),
|
||||
Kind::Export => self.parse_export_declaration(self.cur_start(), ArenaVec::new_in(self)),
|
||||
Kind::Export => {
|
||||
self.parse_export_declaration(self.cur_start(), ArenaVec::new_in(self), stmt_ctx)
|
||||
}
|
||||
// [+Return] ReturnStatement[?Yield, ?Await]
|
||||
Kind::Return => self.parse_return_statement(),
|
||||
Kind::Var => {
|
||||
@@ -170,7 +173,7 @@ impl<'a, C: Config> ParserImpl<'a, C> {
|
||||
Kind::At => self.parse_decorated_statement(stmt_ctx),
|
||||
Kind::Let if !self.cur_token().escaped() => self.parse_let(stmt_ctx),
|
||||
Kind::Async => self.parse_async_statement(self.cur_start(), stmt_ctx),
|
||||
Kind::Import => self.parse_import_statement(),
|
||||
Kind::Import => self.parse_import_statement(stmt_ctx),
|
||||
Kind::Const => self.parse_const_statement(stmt_ctx),
|
||||
Kind::Using if self.is_using_declaration() => self.parse_using_statement(stmt_ctx),
|
||||
Kind::Await if self.is_using_statement() => self.parse_using_statement(stmt_ctx),
|
||||
@@ -889,12 +892,13 @@ impl<'a, C: Config> ParserImpl<'a, C> {
|
||||
}
|
||||
|
||||
/// Parse import statement or import expression.
|
||||
fn parse_import_statement(&mut self) -> Statement<'a> {
|
||||
fn parse_import_statement(&mut self, stmt_ctx: StatementContext) -> Statement<'a> {
|
||||
let start = self.cur_start();
|
||||
if matches!(self.lexer.peek_token().kind(), Kind::Dot | Kind::LParen) {
|
||||
// Parse the whole expression `import.meta.url`.
|
||||
self.parse_expression_or_labeled_statement()
|
||||
} else {
|
||||
self.check_module_declaration(stmt_ctx);
|
||||
self.bump_any(); // bump `import`
|
||||
self.parse_import_declaration(start, self.ctx.has_top_level())
|
||||
}
|
||||
@@ -920,7 +924,7 @@ impl<'a, C: Config> ParserImpl<'a, C> {
|
||||
let kind = self.cur_kind();
|
||||
if kind == Kind::Export {
|
||||
// Export span.start starts after decorators.
|
||||
return self.parse_export_declaration(self.cur_start(), decorators);
|
||||
return self.parse_export_declaration(self.cur_start(), decorators, stmt_ctx);
|
||||
}
|
||||
let modifiers = self.parse_modifiers(false, false);
|
||||
if self.at(Kind::Class) {
|
||||
|
||||
@@ -886,7 +886,7 @@ impl<'a, C: ParserConfig> ParserImpl<'a, C> {
|
||||
|
||||
// Parse the statement with await context enabled (TopLevel context is already set)
|
||||
let stmt = self.context_add(Context::Await, |p| {
|
||||
p.parse_statement_list_item(StatementContext::StatementList)
|
||||
p.parse_statement_list_item(StatementContext::Program)
|
||||
});
|
||||
|
||||
// Replace the statement if the index is valid
|
||||
|
||||
@@ -2,9 +2,9 @@ use memchr::memchr_iter;
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap};
|
||||
|
||||
use oxc_allocator::GetAddress;
|
||||
use oxc_ast::{AstKind, ModuleDeclarationKind, ast::*};
|
||||
use oxc_ast::{AstKind, ast::*};
|
||||
use oxc_ecmascript::{BoundNames, IsSimpleParameterList, PropName};
|
||||
use oxc_span::{GetSpan, ModuleKind, Span, best_match};
|
||||
use oxc_span::{Span, best_match};
|
||||
use oxc_str::Ident;
|
||||
use oxc_syntax::{
|
||||
class::{ClassId, ElementKind},
|
||||
@@ -522,44 +522,6 @@ pub fn check_directive(directive: &Directive, ctx: &SemanticBuilder<'_>) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_module_declaration(decl: &ModuleDeclarationKind, ctx: &SemanticBuilder<'_>) {
|
||||
// It is ambiguous between script and module for `TypeScript`, skipping this check for now.
|
||||
// Basically we need to "upgrade" from script to module if we see any module syntax inside the
|
||||
// semantic builder
|
||||
if ctx.source_type.is_typescript() {
|
||||
return;
|
||||
}
|
||||
|
||||
let text = match decl {
|
||||
ModuleDeclarationKind::Import(_) => "import statement",
|
||||
ModuleDeclarationKind::ExportAll(_)
|
||||
| ModuleDeclarationKind::Export(_)
|
||||
| ModuleDeclarationKind::ExportDefault(_)
|
||||
| ModuleDeclarationKind::ExportNamed(_)
|
||||
| ModuleDeclarationKind::ExportFrom(_)
|
||||
| ModuleDeclarationKind::TSExportAssignment(_)
|
||||
| ModuleDeclarationKind::TSNamespaceExport(_) => "export statement",
|
||||
};
|
||||
let start = decl.span().start;
|
||||
let span = Span::sized(start, 6);
|
||||
match ctx.source_type.module_kind() {
|
||||
ModuleKind::Unambiguous => {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("Technically unreachable, omit to avoid panic.");
|
||||
}
|
||||
// CommonJS uses require/module.exports, not import/export statements
|
||||
ModuleKind::Script | ModuleKind::CommonJS => {
|
||||
ctx.error(diagnostics::module_code(text, span));
|
||||
}
|
||||
ModuleKind::Module => {
|
||||
if matches!(ctx.ancestry().parent_kind(), AstKind::Program(_)) {
|
||||
return;
|
||||
}
|
||||
ctx.error(diagnostics::top_level(text, span));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that `using` declarations are not at the top level in script mode.
|
||||
/// `using` is allowed:
|
||||
/// - At the top level of ES modules
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use oxc_ast::{AstKind, ast::*, match_module_declaration};
|
||||
use oxc_ast::{AstKind, ast::*};
|
||||
|
||||
use crate::builder::SemanticBuilder;
|
||||
|
||||
@@ -37,10 +37,6 @@ pub fn check<'a>(kind: AstKind<'a>, ctx: &SemanticBuilder<'a>) {
|
||||
AstKind::StringLiteral(lit) => js::check_string_literal(lit, ctx),
|
||||
|
||||
AstKind::Directive(dir) => js::check_directive(dir, ctx),
|
||||
match_module_declaration!(AstKind) => {
|
||||
let mod_decl_kind = kind.as_module_declaration_kind().unwrap();
|
||||
js::check_module_declaration(&mod_decl_kind, ctx);
|
||||
}
|
||||
|
||||
AstKind::WithStatement(stmt) => {
|
||||
js::check_function_declaration(&stmt.body, false, ctx);
|
||||
|
||||
@@ -174,19 +174,6 @@ pub fn illegal_use_strict(span: Span) -> OxcDiagnostic {
|
||||
)
|
||||
}
|
||||
|
||||
#[cold]
|
||||
pub fn top_level(x0: &str, span1: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::error(format!(
|
||||
"'{x0}' declaration can only be used at the top level of a module"
|
||||
))
|
||||
.with_label(span1)
|
||||
}
|
||||
|
||||
#[cold]
|
||||
pub fn module_code(x0: &str, span1: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::error(format!("Cannot use {x0} outside a module")).with_label(span1)
|
||||
}
|
||||
|
||||
#[cold]
|
||||
pub fn using_declaration_not_allowed_in_script(span: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::error("'using' declarations are not allowed at the top level of a script")
|
||||
|
||||
@@ -47,6 +47,12 @@ describe("parse", () => {
|
||||
expect(ret.errors.length).toBe(1);
|
||||
});
|
||||
|
||||
it("rejects module syntax in script without semantic checks", () => {
|
||||
const ret = parseSync("test.js", 'import "foo";\nexport {};', { sourceType: "script" });
|
||||
// Reported at `import` and `export` keywords
|
||||
expect(ret.errors.map((error) => error.labels[0].start)).toEqual([0, 14]);
|
||||
});
|
||||
|
||||
describe("sets lang and sourceType", () => {
|
||||
const langs: ParserOptions["lang"][] = ["js", "ts", "jsx", "tsx"];
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
@dec export class C {}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
lexer_misc Summary:
|
||||
AST Parsed : 347/347 (100.00%)
|
||||
Positive Passed: 347/347 (100.00%)
|
||||
AST Parsed : 348/348 (100.00%)
|
||||
Positive Passed: 348/348 (100.00%)
|
||||
@@ -5474,18 +5474,18 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typesc
|
||||
· ──────────
|
||||
╰────
|
||||
|
||||
× Keywords cannot contain escape characters
|
||||
╭─[babel/packages/babel-parser/test/fixtures/es2017/async-functions/invalid-escape-export-async-function/input.js:1:8]
|
||||
1 │ export \u0061sync function y() { await x }
|
||||
· ──────────
|
||||
╰────
|
||||
|
||||
× Cannot use export statement outside a module
|
||||
╭─[babel/packages/babel-parser/test/fixtures/es2017/async-functions/invalid-escape-export-async-function/input.js:1:1]
|
||||
1 │ export \u0061sync function y() { await x }
|
||||
· ──────
|
||||
╰────
|
||||
|
||||
× Keywords cannot contain escape characters
|
||||
╭─[babel/packages/babel-parser/test/fixtures/es2017/async-functions/invalid-escape-export-async-function/input.js:1:8]
|
||||
1 │ export \u0061sync function y() { await x }
|
||||
· ──────────
|
||||
╰────
|
||||
|
||||
× Keywords cannot contain escape characters
|
||||
╭─[babel/packages/babel-parser/test/fixtures/es2017/async-functions/invalid-escape-export-dflt-async-function/input.js:1:16]
|
||||
1 │ export default \u0061sync function y() { await x }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
parser_misc Summary:
|
||||
AST Parsed : 111/111 (100.00%)
|
||||
Positive Passed: 111/111 (100.00%)
|
||||
Negative Passed: 238/238 (100.00%)
|
||||
Negative Passed: 239/239 (100.00%)
|
||||
|
||||
× TS(1275): 'accessor' modifier can only appear on a property declaration.
|
||||
╭─[misc/fail/accessor-modifier-abstract-method.ts:1:29]
|
||||
@@ -470,6 +470,14 @@ Negative Passed: 238/238 (100.00%)
|
||||
3 │ }
|
||||
╰────
|
||||
|
||||
× 'export statement' declaration can only be used at the top level of a module
|
||||
╭─[misc/fail/decorated-export-in-block.mjs:2:8]
|
||||
1 │ {
|
||||
2 │ @dec export class C {}
|
||||
· ──────
|
||||
3 │ }
|
||||
╰────
|
||||
|
||||
× Encountered diff marker
|
||||
╭─[misc/fail/diff-markers.js:10:1]
|
||||
9 │ function test() {
|
||||
|
||||
@@ -22379,6 +22379,13 @@ Negative Passed: 4653/4653 (100.00%)
|
||||
· ─────────
|
||||
╰────
|
||||
|
||||
× Cannot use import statement outside a module
|
||||
╭─[test262/test/language/import/import-defer/syntax/invalid-default-and-defer-namespace.js:34:1]
|
||||
33 │
|
||||
34 │ import defer x, * as ns from "./dep_FIXTURE.js";
|
||||
· ──────
|
||||
╰────
|
||||
|
||||
× TS(18058): Default imports are not allowed in a deferred import.
|
||||
╭─[test262/test/language/import/import-defer/syntax/invalid-default-and-defer-namespace.js:34:8]
|
||||
33 │
|
||||
@@ -22387,9 +22394,9 @@ Negative Passed: 4653/4653 (100.00%)
|
||||
╰────
|
||||
|
||||
× Cannot use import statement outside a module
|
||||
╭─[test262/test/language/import/import-defer/syntax/invalid-default-and-defer-namespace.js:34:1]
|
||||
╭─[test262/test/language/import/import-defer/syntax/invalid-defer-as-with-no-asterisk.js:34:1]
|
||||
33 │
|
||||
34 │ import defer x, * as ns from "./dep_FIXTURE.js";
|
||||
34 │ import defer as ns from "./dep_FIXTURE.js";
|
||||
· ──────
|
||||
╰────
|
||||
|
||||
@@ -22401,6 +22408,13 @@ Negative Passed: 4653/4653 (100.00%)
|
||||
· ╰── `from` expected
|
||||
╰────
|
||||
|
||||
× Cannot use import statement outside a module
|
||||
╭─[test262/test/language/import/import-defer/syntax/invalid-defer-default-and-namespace.js:34:1]
|
||||
33 │
|
||||
34 │ import x, defer * as ns from "./dep_FIXTURE.js";
|
||||
· ──────
|
||||
╰────
|
||||
|
||||
× Unexpected token
|
||||
╭─[test262/test/language/import/import-defer/syntax/invalid-defer-default-and-namespace.js:34:11]
|
||||
33 │
|
||||
@@ -22408,6 +22422,13 @@ Negative Passed: 4653/4653 (100.00%)
|
||||
· ─────
|
||||
╰────
|
||||
|
||||
× Cannot use import statement outside a module
|
||||
╭─[test262/test/language/import/import-defer/syntax/invalid-defer-default.js:34:1]
|
||||
33 │
|
||||
34 │ import defer x from "./dep_FIXTURE.js";
|
||||
· ──────
|
||||
╰────
|
||||
|
||||
× TS(18058): Default imports are not allowed in a deferred import.
|
||||
╭─[test262/test/language/import/import-defer/syntax/invalid-defer-default.js:34:8]
|
||||
33 │
|
||||
@@ -22416,9 +22437,9 @@ Negative Passed: 4653/4653 (100.00%)
|
||||
╰────
|
||||
|
||||
× Cannot use import statement outside a module
|
||||
╭─[test262/test/language/import/import-defer/syntax/invalid-defer-default.js:34:1]
|
||||
╭─[test262/test/language/import/import-defer/syntax/invalid-defer-named.js:34:1]
|
||||
33 │
|
||||
34 │ import defer x from "./dep_FIXTURE.js";
|
||||
34 │ import defer { default as x } from "./dep_FIXTURE.js";
|
||||
· ──────
|
||||
╰────
|
||||
|
||||
@@ -22429,10 +22450,10 @@ Negative Passed: 4653/4653 (100.00%)
|
||||
· ─────
|
||||
╰────
|
||||
|
||||
× Cannot use import statement outside a module
|
||||
╭─[test262/test/language/import/import-defer/syntax/invalid-defer-named.js:34:1]
|
||||
33 │
|
||||
34 │ import defer { default as x } from "./dep_FIXTURE.js";
|
||||
× Cannot use export statement outside a module
|
||||
╭─[test262/test/language/import/import-defer/syntax/invalid-export-defer-namespace.js:33:1]
|
||||
32 │
|
||||
33 │ export defer * as ns from "./dep_FIXTURE.js";
|
||||
· ──────
|
||||
╰────
|
||||
|
||||
|
||||
@@ -8410,7 +8410,7 @@ Expect to Parse: tasks/coverage/typescript/tsc/testdata/tests/cases/conformance/
|
||||
3 │ }
|
||||
╰────
|
||||
|
||||
× Cannot use import statement outside a module
|
||||
× 'import statement' declaration can only be used at the top level of a module
|
||||
╭─[typescript/tsc/testdata/tests/cases/compiler/importDeclarationInModuleDeclaration2.ts:2:5]
|
||||
1 │ function container() {
|
||||
2 │ import "fs";
|
||||
|
||||
@@ -90,10 +90,6 @@ impl TestCase {
|
||||
// Some babel test cases have a js extension, but contain typescript code.
|
||||
// Therefore, if the typescript plugin exists, enable typescript.
|
||||
let mut source_type = SourceType::from_path(path).unwrap().with_jsx(options.is_jsx());
|
||||
// Preserve explicit module extensions; other fixtures default to scripts.
|
||||
if !source_type.is_module() {
|
||||
source_type = source_type.with_script(true);
|
||||
}
|
||||
source_type = match options.source_type.as_deref() {
|
||||
Some("unambiguous") => source_type.with_unambiguous(true),
|
||||
Some("script") => source_type.with_script(true),
|
||||
|
||||
Reference in new issue
Block a user