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
Original file line number Diff line number Diff line change
Expand Up @@ -92,47 +92,40 @@ source: crates/oxc_linter/src/tester.rs
╰────
help: Switch to "||" or "??" operator

× `await` is only allowed within async functions and at the top levels of modules
╭─[prefer_logical_operator_over_ternary.tsx:1:13]
⚠ eslint-plugin-unicorn(prefer-logical-operator-over-ternary): Prefer using a logical operator over a ternary.
╭─[prefer_logical_operator_over_ternary.tsx:1:1]
1 │ foo ? foo : await a
· ─────
· ───────────────────
╰────
help: Either remove this `await` or add the `async` keyword to the enclosing function
help: Switch to "||" or "??" operator

× `await` is only allowed within async functions and at the top levels of modules
⚠ eslint-plugin-unicorn(prefer-logical-operator-over-ternary): Prefer using a logical operator over a ternary.
╭─[prefer_logical_operator_over_ternary.tsx:1:1]
1 │ await a ? await a : foo
· ─────
· ───────────────────────
╰────
help: Either remove this `await` or add the `async` keyword to the enclosing function

× `await` is only allowed within async functions and at the top levels of modules
╭─[prefer_logical_operator_over_ternary.tsx:1:11]
1 │ await a ? await a : foo
· ─────
╰────
help: Either remove this `await` or add the `async` keyword to the enclosing function
help: Switch to "||" or "??" operator

× `await` is only allowed within async functions and at the top levels of modules
⚠ eslint-plugin-unicorn(prefer-logical-operator-over-ternary): Prefer using a logical operator over a ternary.
╭─[prefer_logical_operator_over_ternary.tsx:1:1]
1 │ await a ? (await (a)) : (foo)
· ─────
· ─────────────────────────────
╰────
help: Either remove this `await` or add the `async` keyword to the enclosing function
help: Switch to "||" or "??" operator

× `await` is only allowed within async functions and at the top levels of modules
╭─[prefer_logical_operator_over_ternary.tsx:1:2]
⚠ eslint-plugin-unicorn(prefer-logical-operator-over-ternary): Prefer using a logical operator over a ternary.
╭─[prefer_logical_operator_over_ternary.tsx:1:1]
1 │ (await a) ? await (a) : (foo)
· ─────
· ─────────────────────────────
╰────
help: Either remove this `await` or add the `async` keyword to the enclosing function
help: Switch to "||" or "??" operator

× `await` is only allowed within async functions and at the top levels of modules
╭─[prefer_logical_operator_over_ternary.tsx:1:2]
⚠ eslint-plugin-unicorn(prefer-logical-operator-over-ternary): Prefer using a logical operator over a ternary.
╭─[prefer_logical_operator_over_ternary.tsx:1:1]
1 │ (await a) ? (await (a)) : (foo)
· ─────
· ───────────────────────────────
╰────
help: Either remove this `await` or add the `async` keyword to the enclosing function
help: Switch to "||" or "??" operator

⚠ eslint-plugin-unicorn(prefer-logical-operator-over-ternary): Prefer using a logical operator over a ternary.
╭─[prefer_logical_operator_over_ternary.tsx:2:10]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ source: crates/oxc_linter/src/tester.rs
· ──
╰────

× `await` is only allowed within async functions and at the top levels of modules
╭─[prefer_node_protocol.tsx:1:1]
⚠ eslint-plugin-unicorn(prefer-node-protocol): Prefer using the `node:` protocol when importing Node.js builtin modules.
╭─[prefer_node_protocol.tsx:1:14]
1 │ await import('assert/strict')
· ─────
· ───────────────
╰────
help: Either remove this `await` or add the `async` keyword to the enclosing function
help: Prefer `node:assert/strict` over `assert/strict`.
Original file line number Diff line number Diff line change
Expand Up @@ -337,12 +337,12 @@ source: crates/oxc_linter/src/tester.rs
╰────
help: Replace `substring` with `slice`.

× `await` is only allowed within async functions and at the top levels of modules
╭─[prefer_string_slice.tsx:1:18]
⚠ eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substring()
╭─[prefer_string_slice.tsx:1:5]
1 │ foo.substring(0, await 1)
· ─────
· ─────────
╰────
help: Either remove this `await` or add the `async` keyword to the enclosing function
help: Replace `substring` with `slice`.

⚠ eslint-plugin-unicorn(prefer-string-slice): Prefer String#slice() over String#substring()
╭─[prefer_string_slice.tsx:1:5]
Expand Down
3 changes: 3 additions & 0 deletions crates/oxc_parser/src/js/arrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,11 @@ impl<'a> ParserImpl<'a> {

let expression = !self.at(Kind::LCurly);
let body = if expression {
// Track function depth for expression bodies too
self.state.function_depth += 1;
let expr = self
.parse_assignment_expression_or_higher_impl(allow_return_type_in_arrow_function);
self.state.function_depth -= 1;
let span = expr.span();
let expr_stmt = self.ast.statement_expression(span, expr);
self.ast.alloc_function_body(span, self.ast.vec(), self.ast.vec1(expr_stmt))
Expand Down
104 changes: 82 additions & 22 deletions crates/oxc_parser/src/js/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1139,7 +1139,7 @@ impl<'a> ParserImpl<'a> {
self.unexpected()
}
}
Kind::Await if self.is_await_expression() => self.parse_await_expression(lhs_span),
Kind::Await => self.parse_await_expression(lhs_span),
_ => self.parse_update_expression(lhs_span),
}
}
Expand Down Expand Up @@ -1474,19 +1474,91 @@ impl<'a> ParserImpl<'a> {
self.ast.expression_sequence(self.end_span(span), expressions)
}

/// Check if the current `await` token is unambiguously an await expression.
///
/// Based on Babel's `isAmbiguousPrefixOrIdentifier` (inverted) and
/// TypeScript's `nextTokenIsIdentifierOrKeywordOrLiteralOnSameLine`.
///
/// Returns `true` when await is definitely an await expression (not ambiguous).
///
/// Unambiguous cases (returns `true`):
/// - Next token is identifier, keyword (except `of`), or literal on same line
///
/// Ambiguous cases (returns `false`):
/// - Line break after `await` (could be ASI)
/// - Next token is `+` `-` (could be binary operator or unary prefix)
/// - Next token is `(` `[` (could be call/member or grouping/array)
/// - Next token is template literal
/// - Next token is `of` (for-await-of ambiguity: `for (await of [])`)
/// - Next token is `/` (division or regex literal)
/// - Next token cannot start an expression (`)`, `}`, `;`, etc.)
fn is_unambiguous_await(&mut self) -> bool {
let token = self.lexer.peek_token();

// Line break after await makes it ambiguous (could be ASI)
if token.is_on_new_line() {
return false;
}

let kind = token.kind();

// Special case: `await of` is ambiguous (for-await-of loop)
// Special case: `await using` should be handled as a declaration, not `await (using)`
if matches!(kind, Kind::Of | Kind::Using) {
return false;
}

// Returns true for identifiers, keywords, and literals (not binary operators)
kind.is_after_await_or_yield()
}

/// ``AwaitExpression`[Yield]` :
/// await `UnaryExpression`[?Yield, +Await]
fn parse_await_expression(&mut self, lhs_span: u32) -> Expression<'a> {
let span = self.start_span();
if !self.ctx.has_await() {
// For `ModuleKind::Unambiguous`, defer the error until we know whether
// this is a Module (where top-level await is valid) or Script.
self.error_on_script(diagnostics::await_expression(self.cur_token().span()));
// Case 1: In await context (async function, module top-level, unambiguous mode top-level)
// Always parse as await expression
if self.ctx.has_await() {
let span = self.start_span();
self.bump_any(); // consume `await`
let argument = self.parse_unary_expression_or_higher(self.start_span());
return self.ast.expression_await(self.end_span(span), argument);
}

// Case 2: Not in await context, but unambiguously an await expression
// Parse as await expression and report error for better diagnostics
// This matches Babel's behavior: report "await only allowed in async" error
//
// At top level (function_depth == 0) in unambiguous mode: unambiguous await
// upgrades the file to ESM (like Babel's `sawUnambiguousESM`). We defer the
// error with `error_on_script` - it will be discarded when we upgrade to ESM.
//
// Inside a function (function_depth > 0): await is always invalid in non-async
// functions, even in ESM. Report error immediately.
if self.is_unambiguous_await() {
let span = self.start_span();

if self.state.function_depth == 0 {
// At top level - set flag for statement-level ESM upgrade check
self.state.encountered_unambiguous_await = true;
// Defer error - will be discarded when we upgrade to ESM
self.error_on_script(diagnostics::await_expression(self.cur_token().span()));
} else {
// Inside a function - await is always invalid in non-async function
self.error(diagnostics::await_expression(self.cur_token().span()));
}

self.bump_any(); // consume `await`
// Parse argument with await context enabled for this expression
self.ctx = self.ctx.and_await(true);
let argument = self.parse_unary_expression_or_higher(self.start_span());
self.ctx = self.ctx.and_await(false);
return self.ast.expression_await(self.end_span(span), argument);
}
self.bump_any();
let argument =
self.context_add(Context::Await, |p| p.parse_simple_unary_expression(lhs_span));
self.ast.expression_await(self.end_span(span), argument)

// Case 3: Ambiguous - parse `await` as identifier
// This applies to scripts where `await` might be identifier or keyword
// The statement-level checkpoint system handles reparsing if ESM detected
self.parse_update_expression(lhs_span)
}

fn parse_decorated_expression(&mut self) -> Expression<'a> {
Expand Down Expand Up @@ -1537,18 +1609,6 @@ impl<'a> ParserImpl<'a> {
}
}

fn is_await_expression(&mut self) -> bool {
if self.at(Kind::Await) {
if self.ctx.has_await() {
return true;
}
return self.lookahead(|p| {
Self::next_token_is_identifier_or_keyword_or_literal_on_same_line(p, true)
});
}
false
}

fn is_yield_expression(&mut self) -> bool {
if self.at(Kind::Yield) {
if self.ctx.has_yield() {
Expand Down
2 changes: 2 additions & 0 deletions crates/oxc_parser/src/js/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ impl<'a> ParserImpl<'a> {
let opening_span = self.cur_token().span();
self.expect(Kind::LCurly);

self.state.function_depth += 1;
let (directives, statements) = self.context_add(Context::Return, |p| {
p.parse_directives_and_statements(/* is_top_level */ false)
});
self.state.function_depth -= 1;

self.expect_closing(Kind::RCurly, opening_span);
self.ast.alloc_function_body(self.end_span(span), directives, statements)
Expand Down
11 changes: 10 additions & 1 deletion crates/oxc_parser/src/js/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,26 @@ impl<'a> ParserImpl<'a> {
}

// Take checkpoint for every statement when tracking await reparse.
// We reset the flag and only store the checkpoint if an await identifier
// We reset the flags and only store the checkpoint if an await identifier
// was actually encountered during parsing.
let checkpoint = if track_await_reparse {
self.state.encountered_await_identifier = false;
self.state.encountered_unambiguous_await = false;
Some((statements.len(), self.checkpoint()))
} else {
None
};

let stmt = self.parse_statement_list_item(stmt_ctx);

// If unambiguous await was encountered at top level, upgrade to ESM
// This is like Babel's `sawUnambiguousESM` flag
if track_await_reparse && self.state.encountered_unambiguous_await {
self.module_record_builder.found_unambiguous_await();
track_await_reparse = false;
self.ctx = self.ctx.and_await(true);
}

// Store checkpoint only if await identifier was encountered
if let Some((stmt_index, checkpoint)) = checkpoint
&& self.state.encountered_await_identifier
Expand Down
6 changes: 6 additions & 0 deletions crates/oxc_parser/src/module_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,12 @@ impl<'a> ModuleRecordBuilder<'a> {
pub fn found_ts_export(&mut self) {
self.module_record.has_module_syntax = true;
}

/// Mark the file as ESM when unambiguous top-level await is detected.
/// This is similar to Babel's `sawUnambiguousESM` flag.
pub fn found_unambiguous_await(&mut self) {
self.module_record.has_module_syntax = true;
}
}

fn iter_binding_identifiers_of_declaration<'a, F>(decl: &Declaration<'a>, f: &mut F)
Expand Down
10 changes: 10 additions & 0 deletions crates/oxc_parser/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ pub struct ParserState<'a> {
/// Used to determine if a statement needs to be stored for potential reparsing
/// in unambiguous mode.
pub encountered_await_identifier: bool,

/// Flag to track if an unambiguous await expression was encountered during statement parsing.
/// Used to upgrade to ESM when top-level unambiguous await is detected.
pub encountered_unambiguous_await: bool,

/// Depth counter for function body parsing.
/// Used to detect if we're inside a function body (depth > 0) vs at top level (depth == 0).
pub function_depth: u32,
}

impl ParserState<'_> {
Expand All @@ -41,6 +49,8 @@ impl ParserState<'_> {
trailing_commas: FxHashMap::default(),
potential_await_reparse: Vec::new(),
encountered_await_identifier: false,
encountered_unambiguous_await: false,
function_depth: 0,
}
}
}
4 changes: 1 addition & 3 deletions tasks/coverage/snapshots/codegen_misc.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
codegen_misc Summary:
AST Parsed : 64/64 (100.00%)
Positive Passed: 63/64 (98.44%)
Normal: tasks/coverage/misc/pass/babel-16776-s.js

Positive Passed: 64/64 (100.00%)
8 changes: 4 additions & 4 deletions tasks/coverage/snapshots/parser_misc.snap
Original file line number Diff line number Diff line change
Expand Up @@ -438,14 +438,14 @@ Negative Passed: 133/133 (100.00%)
· ╰── `b` has already been declared here
╰────

× `await` is only allowed within async functions and at the top levels of modules
╭─[misc/fail/oxc-10503.ts:4:1]
× Expected a semicolon or an implicit semicolon after a statement, but found none
╭─[misc/fail/oxc-10503.ts:4:6]
3 │
4 │ await using
· ─────
·
5 │ foo = bar();
╰────
help: Either remove this `await` or add the `async` keyword to the enclosing function
help: Try inserting a semicolon here

× Unexpected token
╭─[misc/fail/oxc-10638.js:2:1]
Expand Down
Loading
Loading