diff --git a/CHANGELOG.md b/CHANGELOG.md index 49e61baee5..38147a32cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ Due to a copy+paste typo, the binary published to `@esbuild/netbsd-arm64` was not actually for `arm64`, and didn't run in that environment. This release should fix running esbuild in that environment (NetBSD on 64-bit ARM). Sorry about the mistake. +* Fix esbuild incorrectly rejecting valid TypeScript edge case ([#4027](https://github.com/evanw/esbuild/issues/4027)) + + The following TypeScript code is valid: + + ```ts + export function open(async?: boolean): void { + console.log(async as boolean) + } + ``` + + Before this version, esbuild would fail to parse this with a syntax error as it expected the token sequence `async as ...` to be the start of an async arrow function expression `async as => ...`. This edge case should be parsed correctly by esbuild starting with this release. + ## 2024 All esbuild versions published in the year 2024 (versions 0.19.12 through 0.24.2) can be found in [CHANGELOG-2024.md](./CHANGELOG-2024.md). diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 01c3fde9fa..e8f95d66f6 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -2879,9 +2879,10 @@ func (p *parser) parseAsyncPrefixExpr(asyncRange logger.Range, level js_ast.L, f // "async x => {}" case js_lexer.TIdentifier: if level <= js_ast.LAssign { - // See https://github.com/tc39/ecma262/issues/2034 for details isArrowFn := true if (flags&exprFlagForLoopInit) != 0 && p.lexer.Identifier.String == "of" { + // See https://github.com/tc39/ecma262/issues/2034 for details + // "for (async of" is only an arrow function if the next token is "=>" isArrowFn = p.checkForArrowAfterTheCurrentToken() @@ -2891,6 +2892,18 @@ func (p *parser) parseAsyncPrefixExpr(asyncRange logger.Range, level js_ast.L, f p.log.AddError(&p.tracker, r, "For loop initializers cannot start with \"async of\"") panic(js_lexer.LexerPanic{}) } + } else if p.options.ts.Parse && p.lexer.Token == js_lexer.TIdentifier { + // Make sure we can parse the following TypeScript code: + // + // export function open(async?: boolean): void { + // console.log(async as boolean) + // } + // + // TypeScript solves this by using a two-token lookahead to check for + // "=>" after an identifier after the "async". This is done in + // "isUnParenthesizedAsyncArrowFunctionWorker" which was introduced + // here: https://github.com/microsoft/TypeScript/pull/8444 + isArrowFn = p.checkForArrowAfterTheCurrentToken() } if isArrowFn { diff --git a/internal/js_parser/ts_parser_test.go b/internal/js_parser/ts_parser_test.go index b775f84b55..5ba653b796 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -2307,7 +2307,7 @@ func TestTSArrow(t *testing.T) { expectPrintedTS(t, "async (): void => {}", "async () => {\n};\n") expectPrintedTS(t, "async (a): void => {}", "async (a) => {\n};\n") - expectParseErrorTS(t, "async x: void => {}", ": ERROR: Expected \"=>\" but found \":\"\n") + expectParseErrorTS(t, "async x: void => {}", ": ERROR: Expected \";\" but found \"x\"\n") expectPrintedTS(t, "function foo(x: boolean): asserts x", "") expectPrintedTS(t, "function foo(x: boolean): asserts", "") @@ -2331,6 +2331,11 @@ func TestTSArrow(t *testing.T) { expectParseErrorTargetTS(t, 5, "return check ? (hover = 2, bar) : baz()", "") expectParseErrorTargetTS(t, 5, "return check ? (hover = 2, bar) => 0 : baz()", ": ERROR: Transforming default arguments to the configured target environment is not supported yet\n") + + // https://github.com/evanw/esbuild/issues/4027 + expectPrintedTS(t, "function f(async?) { g(async in x) }", "function f(async) {\n g(async in x);\n}\n") + expectPrintedTS(t, "function f(async?) { g(async as boolean) }", "function f(async) {\n g(async);\n}\n") + expectPrintedTS(t, "function f() { g(async as => boolean) }", "function f() {\n g(async (as) => boolean);\n}\n") } func TestTSSuperCall(t *testing.T) {