Skip to content
83 changes: 54 additions & 29 deletions src/js_parser/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1541,6 +1541,22 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
Ok(stmts)
}

/// One-token lookahead: advance past the current token, evaluate `pred`,
/// then unconditionally restore the lexer (including `is_log_disabled`).
#[inline]
pub(crate) fn next_token_matches(&mut self, pred: impl FnOnce(&Self) -> bool) -> bool {
let old_lexer = self.lexer.snapshot();
self.lexer.is_log_disabled = true;
let result = matches!(self.lexer.next(), Ok(())) && pred(self);
self.lexer.restore(&old_lexer);
result
}

#[inline]
fn check_for_arrow_after_the_current_token(&mut self) -> bool {
self.next_token_matches(|p| p.lexer.token == T::TEqualsGreaterThan)
}

/// This parses an expression. This assumes we've already parsed the "async"
/// keyword and are currently looking at the following token.
pub fn parse_async_prefix_expr(
Expand Down Expand Up @@ -1588,36 +1604,45 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if level.lte(Level::Assign) {
// p.markLoweredSyntaxFeature();

let ref_ = p.store_name_in_ref(p.lexer.identifier)?;
let arg_loc = p.lexer.loc();
let arg_binding = p.b(B::Identifier { r#ref: ref_ }, arg_loc);
let args: &'a mut [G::Arg] = p.arena.alloc_slice_fill_with(1, |_| G::Arg {
binding: arg_binding,
..Default::default()
});
p.lexer.next()?;

let _ = p.push_scope_for_parse_pass(
js_ast::scope::Kind::FunctionArgs,
async_range.loc,
)?;
// In TypeScript, "async <ident>" not followed by "=>" treats "async" as
// a plain identifier (e.g. "async as T"), matching tsc's two-token
// lookahead in isUnParenthesizedAsyncArrowFunctionWorker (TypeScript#8444).
let is_arrow_fn = !Self::IS_TYPESCRIPT_ENABLED
|| p.check_for_arrow_after_the_current_token();

if is_arrow_fn {
let ref_ = p.store_name_in_ref(p.lexer.identifier)?;
let arg_loc = p.lexer.loc();
let arg_binding = p.b(B::Identifier { r#ref: ref_ }, arg_loc);
let args: &'a mut [G::Arg] =
p.arena.alloc_slice_fill_with(1, |_| G::Arg {
binding: arg_binding,
..Default::default()
});
p.lexer.next()?;

let mut data = FnOrArrowDataParse {
allow_await: AwaitOrYield::AllowExpr,
needs_async_loc: args[0].binding.loc,
..Default::default()
};
// Pop the scope on the error path too.
let mut arrow_body = match p.parse_arrow_body(args, &mut data) {
Ok(body) => body,
Err(e) => {
p.pop_scope();
return Err(e);
}
};
arrow_body.is_async = true;
p.pop_scope();
return Ok(p.new_expr(arrow_body, async_range.loc));
let _ = p.push_scope_for_parse_pass(
js_ast::scope::Kind::FunctionArgs,
async_range.loc,
)?;

let mut data = FnOrArrowDataParse {
allow_await: AwaitOrYield::AllowExpr,
needs_async_loc: args[0].binding.loc,
..Default::default()
};
// Pop the scope on the error path too.
let mut arrow_body = match p.parse_arrow_body(args, &mut data) {
Ok(body) => body,
Err(e) => {
p.pop_scope();
return Err(e);
}
};
arrow_body.is_async = true;
p.pop_scope();
return Ok(p.new_expr(arrow_body, async_range.loc));
}
}
}

Expand Down
28 changes: 28 additions & 0 deletions src/js_parser/parse/parse_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,17 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
bad_let_range = Some(p.lexer.range());
}

// "for (async of" is disallowed by the [lookahead != async of] restriction
// on for-of; "for await (async of" is allowed. Cleared below when the init
// parses to anything other than a bare identifier (e.g. "async of => {}").
let mut bad_async_range: Option<bun_ast::Range> = None;
if !is_for_await
&& p.lexer.is_contextual_keyword(b"async")
&& p.next_token_matches(|p| p.lexer.is_contextual_keyword(b"of"))
{
bad_async_range = Some(p.lexer.range());
}

// Track the decl slice separately so we can reference it after `decls` is moved into
// an arena-backed S::Local. The Vec's heap buffer stays put across the move; the
// arena outlives this fn, so the lifetime-erased view remains valid.
Expand Down Expand Up @@ -582,12 +593,16 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
match res.stmt_or_expr {
js_ast::StmtOrExpr::Stmt(stmt) => {
bad_let_range = None;
bad_async_range = None;
// Keep the "let"/"using" declarations visible to the for-in/for-of
// checks below ("forbid_initializers"), like the "var"/"const" arms.
decls_ptr = bun_ast::StoreSlice::new(res.decls.slice());
init_ = Some(stmt);
}
js_ast::StmtOrExpr::Expr(expr) => {
if !matches!(expr.data, js_ast::ExprData::EIdentifier(_)) {
bad_async_range = None;
}
Comment thread
robobun marked this conversation as resolved.
init_ = Some(p.s(
S::SExpr {
value: expr,
Expand All @@ -614,6 +629,19 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
return Err(crate::Error::SyntaxError);
}

if let Some(r) = bad_async_range {
let full = bun_ast::Range {
loc: r.loc,
len: p.lexer.range().end().start - r.loc.start,
};
p.log().add_range_error(
Some(p.source),
full,
b"For loop initializers cannot start with \"async of\"",
);
return Err(crate::Error::SyntaxError);
}

if is_for_await && !p.lexer.is_contextual_keyword(b"of") {
if init_.is_some() {
p.lexer.expected_string(b"\"of\"")?;
Expand Down
41 changes: 41 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,42 @@ export default class {
);
});

it("identifier named async followed by as/satisfies is not an arrow function", () => {
// https://github.com/evanw/esbuild/issues/4027
// https://github.com/microsoft/TypeScript/pull/8444
ts.expectPrinted_("function f(async?) { g(async as boolean) }", "function f(async) {\n g(async);\n}");
ts.expectPrinted_("function f(async?) { g(async satisfies boolean) }", "function f(async) {\n g(async);\n}");
ts.expectPrinted_("function f(async?) { g(async in x) }", "function f(async) {\n g(async in x);\n}");
ts.expectPrinted_("function f() { g(async as => boolean) }", "function f() {\n g(async (as) => boolean);\n}");
ts.expectPrinted_(
"function f() { g(async satisfies => boolean) }",
"function f() {\n g(async (satisfies) => boolean);\n}",
);
ts.expectPrinted_("let async = true; let x = async as boolean;", "let async = true;\nlet x = async;\n");
ts.expectPrinted_(
"let async = true; console.log(async satisfies boolean);",
"let async = true;\nconsole.log(async);\n",
);
ts.expectPrinted_("const async = 1; export default async as any;", "const async = 1;\nexport default async;\n");
ts.expectPrinted_("let f = async x => {}", "let f = async (x) => {}");
ts.expectParseError("function f(async) { g(async as) }", "Unexpected )");

// "for (async of" must stay rejected in TypeScript mode once the
// lookahead above stops the arrow commit; see the [lookahead != async of]
// restriction on the for-of grammar.
ts.expectParseError("for (async of [7]);", 'For loop initializers cannot start with "async of"');
ts.expectParseError("for (async\nof [7]);", 'For loop initializers cannot start with "async of"');
ts.expectPrinted_("for (async.x of [7]);", "for (async.x of [7])\n ;\n");
ts.expectPrinted_("for (async as any of [7]);", "for ((async) of [7])\n ;\n");
ts.expectPrinted_("for (async satisfies T of [7]);", "for ((async) of [7])\n ;\n");
ts.expectPrinted_("for (async! of [7]);", "for ((async) of [7])\n ;\n");
ts.expectPrinted_("for (async of => {};;);", "for (async (of) => {};; )\n ;\n");
ts.expectPrinted_(
"async function f() { for await (async of [7]); }",
"async function f() {\n for await ((async) of [7])\n ;\n}",
);
});

it("satisfies", () => {
ts.expectPrinted_("const t1 = { a: 1 } satisfies I1;", "const t1 = { a: 1 };\n");
ts.expectPrinted_("const t2 = { a: 1, b: 1 } satisfies I1;", "const t2 = { a: 1, b: 1 };\n");
Expand Down Expand Up @@ -2236,6 +2272,11 @@ console.log(<div {...obj} key="after" />);`),

// The keyword spelling is a syntax error, which is why the parentheses matter
expect(() => parsed("for (async of [7]);", false, false)).toThrow();
expectParseError("for (async\nof [7]);", 'For loop initializers cannot start with "async of"');
expectPrinted_(
"async function f() { for await (async\nof [7]); }",
"async function f() {\n for await ((async) of [7])\n ;\n}",
);
});

it("await", () => {
Expand Down
Loading