diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e2c6c8a726..4b87e748390 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,14 @@ } ``` +* Fix bug with TypeScript parsing of instantiation expressions followed by `=` ([#3111](https://github.com/evanw/esbuild/issues/3111)) + + This release fixes esbuild's TypeScript-to-JavaScript conversion code in the case where a potential instantiation expression is followed immediately by a `=` token (such that the trailing `>` becomes a `>=` token). Previously esbuild considered that to still be an instantiation expression, but the official TypeScript compiler considered it to be a `>=` operator instead. This release changes esbuild's interpretation to match TypeScript. This edge case currently [appears to be problematic](https://sucrase.io/#transforms=typescript&compareWithTypeScript=true&code=x%3Cy%3E%3Da%3Cb%3Cc%3E%3E()) for other TypeScript-to-JavaScript converters as well: + + | Original code | TypeScript | esbuild 0.17.18 | esbuild 0.17.19 | Sucrase | Babel | + |---|---|---|---|---|---| + | `x=a>()` | `x=a();` | `x=a();` | `x=a();` | `x=a()` | Invalid left-hand side in assignment expression | + * Avoid removing unrecognized directives from the directive prologue when minifying ([#3115](https://github.com/evanw/esbuild/issues/3115)) The [directive prologue](https://262.ecma-international.org/6.0/#sec-directive-prologues-and-the-use-strict-directive) in JavaScript is a sequence of top-level string expressions that come before your code. The only directives that JavaScript engines currently recognize are `use strict` and sometimes `use asm`. However, the people behind React have made up their own directive for their own custom dialect of JavaScript. Previously esbuild only preserved the `use strict` directive when minifying, although you could still write React JavaScript with esbuild using something like `--banner:js="'your directive here';"`. With this release, you can now put arbitrary directives in the entry point and esbuild will preserve them in its minified output: diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 15ca9a74ad2..f7f5dc19996 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -4014,7 +4014,7 @@ func (p *parser) parseSuffix(left js_ast.Expr, level js_ast.L, errors *deferredE if !p.options.ts.Parse { p.lexer.Expected(js_lexer.TIdentifier) } - p.skipTypeScriptTypeArguments(false /* isInsideJSXElement */) + p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{}) if p.lexer.Token != js_lexer.TOpenParen { p.lexer.Expected(js_lexer.TOpenParen) } @@ -4326,7 +4326,7 @@ func (p *parser) parseSuffix(left js_ast.Expr, level js_ast.L, errors *deferredE // TypeScript allows type arguments to be specified with angle brackets // inside an expression. Unlike in other languages, this unfortunately // appears to require backtracking to parse. - if p.options.ts.Parse && p.trySkipTypeScriptTypeArgumentsWithBacktracking() { + if p.options.ts.Parse && p.trySkipTypeArgumentsInExpressionWithBacktracking() { optionalChain = oldOptionalChain continue } @@ -4362,7 +4362,7 @@ func (p *parser) parseSuffix(left js_ast.Expr, level js_ast.L, errors *deferredE // TypeScript allows type arguments to be specified with angle brackets // inside an expression. Unlike in other languages, this unfortunately // appears to require backtracking to parse. - if p.options.ts.Parse && p.trySkipTypeScriptTypeArgumentsWithBacktracking() { + if p.options.ts.Parse && p.trySkipTypeArgumentsInExpressionWithBacktracking() { optionalChain = oldOptionalChain continue } @@ -4777,7 +4777,7 @@ func (p *parser) parseJSXElement(loc logger.Loc) js_ast.Expr { // js_lexer.NextInsideJSXElement() after we hit the closing ">". The next // token after the ">" might be an attribute name with a dash in it // like this: " data-disabled/>" - p.skipTypeScriptTypeArguments(true /* isInsideJSXElement */) + p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{isInsideJSXElement: true}) } // Parse attributes @@ -5949,7 +5949,7 @@ func (p *parser) parseClass(classKeyword logger.Range, name *js_ast.LocRef, clas // does and it probably doesn't have that high of a performance overhead // because "extends" clauses aren't that frequent, so it should be ok. if p.options.ts.Parse { - p.skipTypeScriptTypeArguments(false /* isInsideJSXElement */) + p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{}) } } diff --git a/internal/js_parser/ts_parser.go b/internal/js_parser/ts_parser.go index 1704e0d2b44..6850dc37e5e 100644 --- a/internal/js_parser/ts_parser.go +++ b/internal/js_parser/ts_parser.go @@ -382,7 +382,7 @@ loop: // "let foo: any \n foo" must not become a single type if checkTypeParameters && !p.lexer.HasNewlineBefore { - p.skipTypeScriptTypeArguments(false /* isInsideJSXElement */) + p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{}) } case js_lexer.TTypeof: @@ -414,7 +414,7 @@ loop: } if !p.lexer.HasNewlineBefore { - p.skipTypeScriptTypeArguments(false /* isInsideJSXElement */) + p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{}) } } @@ -510,7 +510,7 @@ loop: // "{ (): c.d \n (): g.h }" must not become a single type if !p.lexer.HasNewlineBefore { - p.skipTypeScriptTypeArguments(false /* isInsideJSXElement */) + p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{}) } case js_lexer.TOpenBracket: @@ -774,7 +774,12 @@ func (p *parser) skipTypeScriptTypeParameters(flags typeParameterFlags) skipType return result } -func (p *parser) skipTypeScriptTypeArguments(isInsideJSXElement bool) bool { +type skipTypeScriptTypeArgumentsOpts struct { + isInsideJSXElement bool + isParseTypeArgumentsInExpression bool +} + +func (p *parser) skipTypeScriptTypeArguments(opts skipTypeScriptTypeArgumentsOpts) bool { switch p.lexer.Token { case js_lexer.TLessThan, js_lexer.TLessThanEquals, js_lexer.TLessThanLessThan, js_lexer.TLessThanLessThanEquals: @@ -793,11 +798,31 @@ func (p *parser) skipTypeScriptTypeArguments(isInsideJSXElement bool) bool { } // This type argument list must end with a ">" - p.lexer.ExpectGreaterThan(isInsideJSXElement) + if !opts.isParseTypeArgumentsInExpression { + // Normally TypeScript allows any token starting with ">". For example, + // "Array>()" is a type argument list even though there's a + // ">>" token, because ">>" starts with ">". + p.lexer.ExpectGreaterThan(opts.isInsideJSXElement) + } else { + // However, if we're emulating the TypeScript compiler's function called + // "parseTypeArgumentsInExpression" function, then we must only allow the + // ">" token itself. For example, "x < y >= z" is not a type argument list. + // + // This doesn't detect ">>" in "Array>()" because the inner + // type argument list isn't a call to "parseTypeArgumentsInExpression" + // because it's within a type context, not an expression context. So the + // token that we see here is ">" in that case because the first ">" has + // already been stripped off of the ">>" by the inner call. + if opts.isInsideJSXElement { + p.lexer.ExpectInsideJSXElement(js_lexer.TGreaterThan) + } else { + p.lexer.Expect(js_lexer.TGreaterThan) + } + } return true } -func (p *parser) trySkipTypeScriptTypeArgumentsWithBacktracking() bool { +func (p *parser) trySkipTypeArgumentsInExpressionWithBacktracking() bool { oldLexer := p.lexer p.lexer.IsLogDisabled = true @@ -811,7 +836,7 @@ func (p *parser) trySkipTypeScriptTypeArgumentsWithBacktracking() bool { } }() - if p.skipTypeScriptTypeArguments(false /* isInsideJSXElement */) { + if p.skipTypeScriptTypeArguments(skipTypeScriptTypeArgumentsOpts{isParseTypeArgumentsInExpression: true}) { // Check the token after the type argument list and backtrack if it's invalid if !p.tsCanFollowTypeArgumentsInExpression() { p.lexer.Unexpected() diff --git a/internal/js_parser/ts_parser_test.go b/internal/js_parser/ts_parser_test.go index b0752d6fd45..de70873b0cd 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -2061,11 +2061,17 @@ func TestTSInstantiationExpression(t *testing.T) { expectPrintedTS(t, "f.x<() => T>;", "f.x;\n") expectPrintedTS(t, "f['x']<() => T>;", "f[\"x\"];\n") expectPrintedTS(t, "fg;", "f < x > g;\n") - expectPrintedTS(t, "f=g;", "f = g;\n") + expectPrintedTS(t, "f=g;", "f < x >= g;\n") expectPrintedTS(t, "f>g;", "f < x >> g;\n") expectPrintedTS(t, "f>>g;", "f < x >>> g;\n") expectParseErrorTS(t, "f>=g;", ": ERROR: Invalid assignment target\n") expectParseErrorTS(t, "f>>=g;", ": ERROR: Invalid assignment target\n") + expectPrintedTS(t, "fg;", "f < x, y > g;\n") + expectPrintedTS(t, "f=g;", "f < x, y >= g;\n") + expectPrintedTS(t, "f>g;", "f < x, y >> g;\n") + expectPrintedTS(t, "f>>g;", "f < x, y >>> g;\n") + expectPrintedTS(t, "f>=g;", "f < x, y >>= g;\n") + expectPrintedTS(t, "f>>=g;", "f < x, y >>>= g;\n") expectPrintedTS(t, "f = g;", "f = g;\n") expectParseErrorTS(t, "f > g;", ": ERROR: Unexpected \">\"\n") expectParseErrorTS(t, "f >> g;", ": ERROR: Unexpected \">>\"\n") @@ -2073,6 +2079,13 @@ func TestTSInstantiationExpression(t *testing.T) { expectParseErrorTS(t, "f >= g;", ": ERROR: Unexpected \">=\"\n") expectParseErrorTS(t, "f >>= g;", ": ERROR: Unexpected \">>=\"\n") expectParseErrorTS(t, "f >>>= g;", ": ERROR: Unexpected \">>>=\"\n") + expectPrintedTS(t, "f = g;", "f = g;\n") + expectParseErrorTS(t, "f > g;", ": ERROR: Unexpected \">\"\n") + expectParseErrorTS(t, "f >> g;", ": ERROR: Unexpected \">>\"\n") + expectParseErrorTS(t, "f >>> g;", ": ERROR: Unexpected \">>>\"\n") + expectParseErrorTS(t, "f >= g;", ": ERROR: Unexpected \">=\"\n") + expectParseErrorTS(t, "f >>= g;", ": ERROR: Unexpected \">>=\"\n") + expectParseErrorTS(t, "f >>>= g;", ": ERROR: Unexpected \">>>=\"\n") expectPrintedTS(t, "[f];", "[f];\n") expectPrintedTS(t, "f ? g : h;", "f ? g : h;\n") expectPrintedTS(t, "{ f }", "{\n f;\n}\n") @@ -2086,6 +2099,8 @@ func TestTSInstantiationExpression(t *testing.T) { expectPrintedTS(t, "f instanceof g;", "f instanceof g;\n") expectPrintedTS(t, "f as g;", "f;\n") expectPrintedTS(t, "f satisfies g;", "f;\n") + expectPrintedTS(t, "class A extends B { f() { super.f=y } }", "class A extends B {\n f() {\n super.f < x >= y;\n }\n}\n") + expectPrintedTS(t, "class A extends B { f() { super.f=z } }", "class A extends B {\n f() {\n super.f < x, y >= z;\n }\n}\n") expectParseErrorTS(t, "const a8 = f;", ": ERROR: Unexpected \";\"\n") expectParseErrorTS(t, "const b1 = f?.;", ": ERROR: Expected \"(\" but found \";\"\n")