From be94d3787f54a74ab6094b9e9fdebad482fef546 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Thu, 26 Jan 2023 23:51:23 -0500 Subject: [PATCH] update `enum` constant folding for TypeScript 5.0 --- CHANGELOG.md | 58 ++++++ internal/bundler_tests/bundler_ts_test.go | 174 ++++++++++++++++++ .../bundler_tests/snapshots/snapshots_ts.txt | 91 +++++++++ internal/js_ast/js_ast_helpers.go | 68 ++++++- internal/js_parser/js_parser.go | 58 ++++-- internal/js_parser/js_parser_test.go | 9 +- 6 files changed, 437 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 482f9768d12..bcea670dad9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,64 @@ Foo = class { }; ``` +* Update `enum` constant folding for TypeScript 5.0 + + TypeScript 5.0 contains an [updated definition of what it considers a constant expression](https://github.com/microsoft/TypeScript/pull/50528): + + > An expression is considered a *constant expression* if it is + > + > * a number or string literal, + > * a unary `+`, `-`, or `~` applied to a numeric constant expression, + > * a binary `+`, `-`, `*`, `/`, `%`, `**`, `<<`, `>>`, `>>>`, `|`, `&`, `^` applied to two numeric constant expressions, + > * a binary `+` applied to two constant expressions whereof at least one is a string, + > * a template expression where each substitution expression is a constant expression, + > * a parenthesized constant expression, + > * a dotted name (e.g. `x.y.z`) that references a `const` variable with a constant expression initializer and no type annotation, + > * a dotted name that references an enum member with an enum literal type, or + > * a dotted name indexed by a string literal (e.g. `x.y["z"]`) that references an enum member with an enum literal type. + + This impacts esbuild's implementation of TypeScript's `const enum` feature. With this release, esbuild will now attempt to follow these new rules. For example, you can now initialize an `enum` member with a template literal expression that contains a numeric constant: + + ```ts + // Original input + const enum Example { + COUNT = 100, + ERROR = `Expected ${COUNT} items`, + } + console.log( + Example.COUNT, + Example.ERROR, + ) + + // Old output (with --tree-shaking=true) + var Example = /* @__PURE__ */ ((Example2) => { + Example2[Example2["COUNT"] = 100] = "COUNT"; + Example2[Example2["ERROR"] = `Expected ${100 /* COUNT */} items`] = "ERROR"; + return Example2; + })(Example || {}); + console.log( + 100 /* COUNT */, + Example.ERROR + ); + + // New output (with --tree-shaking=true) + console.log( + 100 /* COUNT */, + "Expected 100 items" /* ERROR */ + ); + ``` + + These rules are not followed exactly due to esbuild's limitations. The rule about dotted references to `const` variables is not followed both because esbuild's enum processing is done in an isolated module setting and because doing so would potentially require esbuild to use a type system, which it doesn't have. For example: + + ```ts + // The TypeScript compiler inlines this but esbuild doesn't: + declare const x = 'foo' + const enum Foo { X = x } + console.log(Foo.X) + ``` + + Also, the rule that requires converting numbers to a string currently only followed for 32-bit signed integers and non-finite numbers. This is done to avoid accidentally introducing a bug if esbuild's number-to-string operation doesn't exactly match the behavior of a real JavaScript VM. Currently esbuild's number-to-string constant folding is conservative for safety. + * Forbid definite assignment assertion operators on class methods In TypeScript, class methods can use the `?` optional property operator but not the `!` definite assignment assertion operator (while class fields can use both): diff --git a/internal/bundler_tests/bundler_ts_test.go b/internal/bundler_tests/bundler_ts_test.go index ef20abf2f17..f5a9db9a6d4 100644 --- a/internal/bundler_tests/bundler_ts_test.go +++ b/internal/bundler_tests/bundler_ts_test.go @@ -2085,3 +2085,177 @@ NOTE: Node's package format requires that CommonJS files in a "type": "module" p `, }) } + +func TestEnumRulesFrom_TypeScript_5_0(t *testing.T) { + ts_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/supported.ts": ` + // From https://github.com/microsoft/TypeScript/pull/50528: + // "An expression is considered a constant expression if it is + const enum Foo { + // a number or string literal, + X0 = 123, + X1 = 'x', + + // a unary +, -, or ~ applied to a numeric constant expression, + X2 = +1, + X3 = -2, + X4 = ~3, + + // a binary +, -, *, /, %, **, <<, >>, >>>, |, &, ^ applied to two numeric constant expressions, + X5 = 1 + 2, + X6 = 1 - 2, + X7 = 2 * 3, + X8 = 1 / 2, + X9 = 3 % 2, + X10 = 2 ** 3, + X11 = 1 << 2, + X12 = -9 >> 1, + X13 = -9 >>> 1, + X14 = 5 | 12, + X15 = 5 & 12, + X16 = 5 ^ 12, + + // a binary + applied to two constant expressions whereof at least one is a string, + X17 = 'x' + 0, + X18 = 0 + 'x', + X19 = 'x' + 'y', + X20 = '' + NaN, + X21 = '' + Infinity, + X22 = '' + -Infinity, + X23 = '' + -0, + + // a template expression where each substitution expression is a constant expression, + X24 = ` + "`A${0}B${'x'}C${1 + 3 - 4 / 2 * 5 ** 6}D`" + `, + + // a parenthesized constant expression, + X25 = (321), + + // a dotted name (e.g. x.y.z) that references a const variable with a constant expression initializer and no type annotation, + /* (we don't implement this one) */ + + // a dotted name that references an enum member with an enum literal type, or + X26 = X0, + X27 = X0 + 'x', + X28 = 'x' + X0, + X29 = ` + "`a${X0}b`" + `, + X30 = Foo.X0, + X31 = Foo.X0 + 'x', + X32 = 'x' + Foo.X0, + X33 = ` + "`a${Foo.X0}b`" + `, + + // a dotted name indexed by a string literal (e.g. x.y["z"]) that references an enum member with an enum literal type." + X34 = X1, + X35 = X1 + 'y', + X36 = 'y' + X1, + X37 = ` + "`a${X1}b`" + `, + X38 = Foo['X1'], + X39 = Foo['X1'] + 'y', + X40 = 'y' + Foo['X1'], + X41 = ` + "`a${Foo['X1']}b`" + `, + } + + console.log( + // a number or string literal, + Foo.X0, + Foo.X1, + + // a unary +, -, or ~ applied to a numeric constant expression, + Foo.X2, + Foo.X3, + Foo.X4, + + // a binary +, -, *, /, %, **, <<, >>, >>>, |, &, ^ applied to two numeric constant expressions, + Foo.X5, + Foo.X6, + Foo.X7, + Foo.X8, + Foo.X9, + Foo.X10, + Foo.X11, + Foo.X12, + Foo.X13, + Foo.X14, + Foo.X15, + Foo.X16, + + // a template expression where each substitution expression is a constant expression, + Foo.X17, + Foo.X18, + Foo.X19, + Foo.X20, + Foo.X21, + Foo.X22, + Foo.X23, + + // a template expression where each substitution expression is a constant expression, + Foo.X24, + + // a parenthesized constant expression, + Foo.X25, + + // a dotted name that references an enum member with an enum literal type, or + Foo.X26, + Foo.X27, + Foo.X28, + Foo.X29, + Foo.X30, + Foo.X31, + Foo.X32, + Foo.X33, + + // a dotted name indexed by a string literal (e.g. x.y["z"]) that references an enum member with an enum literal type." + Foo.X34, + Foo.X35, + Foo.X36, + Foo.X37, + Foo.X38, + Foo.X39, + Foo.X40, + Foo.X41, + ) + `, + "/not-supported.ts": ` + const enum NonIntegerNumberToString { + SUPPORTED = '' + 1, + UNSUPPORTED = '' + 1.5, + } + console.log( + NonIntegerNumberToString.SUPPORTED, + NonIntegerNumberToString.UNSUPPORTED, + ) + + const enum OutOfBoundsNumberToString { + SUPPORTED = '' + 1_000_000_000, + UNSUPPORTED = '' + 1_000_000_000_000, + } + console.log( + OutOfBoundsNumberToString.SUPPORTED, + OutOfBoundsNumberToString.UNSUPPORTED, + ) + + const enum TemplateExpressions { + // TypeScript enums don't handle any of these + NULL = '' + null, + TRUE = '' + true, + FALSE = '' + false, + BIGINT = '' + 123n, + } + console.log( + TemplateExpressions.NULL, + TemplateExpressions.TRUE, + TemplateExpressions.FALSE, + TemplateExpressions.BIGINT, + ) + `, + }, + entryPaths: []string{ + "/supported.ts", + "/not-supported.ts", + }, + options: config.Options{ + Mode: config.ModeBundle, + AbsOutputDir: "/out", + }, + }) +} diff --git a/internal/bundler_tests/snapshots/snapshots_ts.txt b/internal/bundler_tests/snapshots/snapshots_ts.txt index 83cf33dab9b..27b51514a9e 100644 --- a/internal/bundler_tests/snapshots/snapshots_ts.txt +++ b/internal/bundler_tests/snapshots/snapshots_ts.txt @@ -1,3 +1,94 @@ +TestEnumRulesFrom_TypeScript_5_0 +---------- /out/supported.js ---------- +// supported.ts +console.log( + // a number or string literal, + 123 /* X0 */, + "x" /* X1 */, + // a unary +, -, or ~ applied to a numeric constant expression, + 1 /* X2 */, + -2 /* X3 */, + -4 /* X4 */, + // a binary +, -, *, /, %, **, <<, >>, >>>, |, &, ^ applied to two numeric constant expressions, + 3 /* X5 */, + -1 /* X6 */, + 6 /* X7 */, + 0.5 /* X8 */, + 1 /* X9 */, + 8 /* X10 */, + 4 /* X11 */, + -5 /* X12 */, + 2147483643 /* X13 */, + 13 /* X14 */, + 4 /* X15 */, + 9 /* X16 */, + // a template expression where each substitution expression is a constant expression, + "x0" /* X17 */, + "0x" /* X18 */, + "xy" /* X19 */, + "NaN" /* X20 */, + "Infinity" /* X21 */, + "-Infinity" /* X22 */, + "0" /* X23 */, + // a template expression where each substitution expression is a constant expression, + "A0BxC-31246D" /* X24 */, + // a parenthesized constant expression, + 321 /* X25 */, + // a dotted name that references an enum member with an enum literal type, or + 123 /* X26 */, + "123x" /* X27 */, + "x123" /* X28 */, + "a123b" /* X29 */, + 123 /* X30 */, + "123x" /* X31 */, + "x123" /* X32 */, + "a123b" /* X33 */, + // a dotted name indexed by a string literal (e.g. x.y["z"]) that references an enum member with an enum literal type." + "x" /* X34 */, + "xy" /* X35 */, + "yx" /* X36 */, + "axb" /* X37 */, + "x" /* X38 */, + "xy" /* X39 */, + "yx" /* X40 */, + "axb" /* X41 */ +); + +---------- /out/not-supported.js ---------- +// not-supported.ts +var NonIntegerNumberToString = ((NonIntegerNumberToString2) => { + NonIntegerNumberToString2["SUPPORTED"] = "1"; + NonIntegerNumberToString2[NonIntegerNumberToString2["UNSUPPORTED"] = "" + 1.5] = "UNSUPPORTED"; + return NonIntegerNumberToString2; +})(NonIntegerNumberToString || {}); +console.log( + "1" /* SUPPORTED */, + NonIntegerNumberToString.UNSUPPORTED +); +var OutOfBoundsNumberToString = ((OutOfBoundsNumberToString2) => { + OutOfBoundsNumberToString2["SUPPORTED"] = "1000000000"; + OutOfBoundsNumberToString2[OutOfBoundsNumberToString2["UNSUPPORTED"] = "" + 1e12] = "UNSUPPORTED"; + return OutOfBoundsNumberToString2; +})(OutOfBoundsNumberToString || {}); +console.log( + "1000000000" /* SUPPORTED */, + OutOfBoundsNumberToString.UNSUPPORTED +); +var TemplateExpressions = ((TemplateExpressions2) => { + TemplateExpressions2[TemplateExpressions2["NULL"] = "" + null] = "NULL"; + TemplateExpressions2[TemplateExpressions2["TRUE"] = "" + true] = "TRUE"; + TemplateExpressions2[TemplateExpressions2["FALSE"] = "" + false] = "FALSE"; + TemplateExpressions2[TemplateExpressions2["BIGINT"] = "" + 123n] = "BIGINT"; + return TemplateExpressions2; +})(TemplateExpressions || {}); +console.log( + TemplateExpressions.NULL, + TemplateExpressions.TRUE, + TemplateExpressions.FALSE, + TemplateExpressions.BIGINT +); + +================================================================================ TestExportTypeIssue379 ---------- /out.js ---------- // a.ts diff --git a/internal/js_ast/js_ast_helpers.go b/internal/js_ast/js_ast_helpers.go index b8fe9a39435..d291d029b25 100644 --- a/internal/js_ast/js_ast_helpers.go +++ b/internal/js_ast/js_ast_helpers.go @@ -1291,13 +1291,61 @@ func joinStrings(a []uint16, b []uint16) []uint16 { return data } +// String concatenation with numbers is required by the TypeScript compiler for +// "constant expression" handling in enums. However, we don't want to introduce +// correctness bugs by accidentally stringifying a number differently than how +// a real JavaScript VM would do it. So we are conservative and we only do this +// when we know it'll be the same result. +func tryToStringOnNumberSafely(n float64) (string, bool) { + if i := int32(n); float64(i) == n { + return strconv.Itoa(int(i)), true + } + if math.IsNaN(n) { + return "NaN", true + } + if math.IsInf(n, 1) { + return "Infinity", true + } + if math.IsInf(n, -1) { + return "-Infinity", true + } + return "", false +} + // This function intentionally avoids mutating the input AST so it can be // called after the AST has been frozen (i.e. after parsing ends). func FoldStringAddition(left Expr, right Expr) Expr { + // "See through" inline enum constants + if l, ok := left.Data.(*EInlinedEnum); ok { + left = l.Value + } + if r, ok := right.Data.(*EInlinedEnum); ok { + right = r.Value + } + + if l, ok := left.Data.(*ENumber); ok { + switch right.Data.(type) { + case *EString, *ETemplate: + // "0 + 'x'" => "0 + 'x'" + // "0 + `${x}`" => "0 + `${x}`" + if str, ok := tryToStringOnNumberSafely(l.Value); ok { + left.Data = &EString{Value: helpers.StringToUTF16(str)} + } + } + } + switch l := left.Data.(type) { case *EString: + // "'x' + 0" => "'x' + '0'" + if r, ok := right.Data.(*ENumber); ok { + if str, ok := tryToStringOnNumberSafely(r.Value); ok { + right.Data = &EString{Value: helpers.StringToUTF16(str)} + } + } + switch r := right.Data.(type) { case *EString: + // "'x' + 'y'" => "'xy'" return Expr{Loc: left.Loc, Data: &EString{ Value: joinStrings(l.Value, r.Value), PreferTemplate: l.PreferTemplate || r.PreferTemplate, @@ -1305,6 +1353,7 @@ func FoldStringAddition(left Expr, right Expr) Expr { case *ETemplate: if r.TagOrNil.Data == nil { + // "'x' + `y${z}`" => "`xy${z}`" return Expr{Loc: left.Loc, Data: &ETemplate{ HeadLoc: left.Loc, HeadCooked: joinStrings(l.Value, r.HeadCooked), @@ -1320,8 +1369,16 @@ func FoldStringAddition(left Expr, right Expr) Expr { case *ETemplate: if l.TagOrNil.Data == nil { + // "`${x}` + 0" => "`${x}` + '0'" + if r, ok := right.Data.(*ENumber); ok { + if str, ok := tryToStringOnNumberSafely(r.Value); ok { + right.Data = &EString{Value: helpers.StringToUTF16(str)} + } + } + switch r := right.Data.(type) { case *EString: + // "`${x}y` + 'z'" => "`${x}yz`" n := len(l.Parts) head := l.HeadCooked parts := make([]TemplatePart, n) @@ -1339,6 +1396,7 @@ func FoldStringAddition(left Expr, right Expr) Expr { case *ETemplate: if r.TagOrNil.Data == nil { + // "`${a}b` + `x${y}`" => "`${a}bx${y}`" n := len(l.Parts) head := l.HeadCooked parts := make([]TemplatePart, n+len(r.Parts)) @@ -1371,7 +1429,7 @@ func FoldStringAddition(left Expr, right Expr) Expr { // // This function intentionally avoids mutating the input AST so it can be // called after the AST has been frozen (i.e. after parsing ends). -func InlineStringsIntoTemplate(loc logger.Loc, e *ETemplate) Expr { +func InlineStringsAndNumbersIntoTemplate(loc logger.Loc, e *ETemplate) Expr { // Can't inline strings if there's a custom template tag if e.TagOrNil.Data != nil { return Expr{Loc: loc, Data: e} @@ -1381,6 +1439,14 @@ func InlineStringsIntoTemplate(loc logger.Loc, e *ETemplate) Expr { parts := make([]TemplatePart, 0, len(e.Parts)) for _, part := range e.Parts { + if value, ok := part.Value.Data.(*EInlinedEnum); ok { + part.Value = value.Value + } + if value, ok := part.Value.Data.(*ENumber); ok { + if str, ok := tryToStringOnNumberSafely(value.Value); ok { + part.Value.Data = &EString{Value: helpers.StringToUTF16(str)} + } + } if str, ok := part.Value.Data.(*EString); ok { if len(parts) == 0 { headCooked = append(append(headCooked, str.Value...), part.TailCooked...) diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 3c417b25392..846b66c0ee3 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -333,9 +333,34 @@ type parser struct { // Relevant issue: https://github.com/evanw/esbuild/issues/1158 hasNonLocalExportDeclareInsideNamespace bool - // The "shouldFoldNumericConstants" flag is enabled inside each enum body block - // since TypeScript requires numeric constant folding in enum definitions. - shouldFoldNumericConstants bool + // When this flag is enabled, we attempt to fold all expressions that + // TypeScript would consider to be "constant expressions". This flag is + // enabled inside each enum body block since TypeScript requires numeric + // constant folding in enum definitions. + // + // We also enable this flag in certain cases in JavaScript files such as when + // parsing "const" declarations at the top of a non-ESM file, but we still + // reuse TypeScript's notion of "constant expressions" for our own convenience. + // + // As of TypeScript 5.0, a "constant expression" is defined as follows: + // + // An expression is considered a constant expression if it is + // + // * a number or string literal, + // * a unary +, -, or ~ applied to a numeric constant expression, + // * a binary +, -, *, /, %, **, <<, >>, >>>, |, &, ^ applied to two numeric constant expressions, + // * a binary + applied to two constant expressions whereof at least one is a string, + // * a template expression where each substitution expression is a constant expression, + // * a parenthesized constant expression, + // * a dotted name (e.g. x.y.z) that references a const variable with a constant expression initializer and no type annotation, + // * a dotted name that references an enum member with an enum literal type, or + // * a dotted name indexed by a string literal (e.g. x.y["z"]) that references an enum member with an enum literal type. + // + // More detail: https://github.com/microsoft/TypeScript/pull/50528. Note that + // we don't implement certain items in this list. For example, we don't do all + // number-to-string conversions since ours might differ from how JavaScript + // would do it, which would be a correctness issue. + shouldFoldTypeScriptConstantExpressions bool allowIn bool allowPrivateIdentifiers bool @@ -8825,9 +8850,10 @@ func (p *parser) substituteSingleUseSymbolInExpr( if value, status := p.substituteSingleUseSymbolInExpr(part.Value, ref, replacement, replacementCanBeRemoved); status != substituteContinue { e.Parts[i].Value = value - // If we substituted a string, merge the string into the template - if _, ok := value.Data.(*js_ast.EString); ok { - expr = js_ast.InlineStringsIntoTemplate(expr.Loc, e) + // If we substituted a string or number, merge it into the template + switch value.Data.(type) { + case *js_ast.EString, *js_ast.ENumber: + expr = js_ast.InlineStringsAndNumbersIntoTemplate(expr.Loc, e) } return expr, status } @@ -9552,12 +9578,12 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_ wasAnonymousNamedExpr := p.isAnonymousNamedExpr(d.ValueOrNil) // Fold numeric constants in the initializer - oldShouldFoldNumericConstants := p.shouldFoldNumericConstants - p.shouldFoldNumericConstants = p.options.minifySyntax && !p.currentScope.IsAfterConstLocalPrefix + oldShouldFoldTypeScriptConstantExpressions := p.shouldFoldTypeScriptConstantExpressions + p.shouldFoldTypeScriptConstantExpressions = p.options.minifySyntax && !p.currentScope.IsAfterConstLocalPrefix d.ValueOrNil = p.visitExpr(d.ValueOrNil) - p.shouldFoldNumericConstants = oldShouldFoldNumericConstants + p.shouldFoldTypeScriptConstantExpressions = oldShouldFoldTypeScriptConstantExpressions // Optionally preserve the name if id, ok := d.Binding.Data.(*js_ast.BIdentifier); ok { @@ -10098,8 +10124,8 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_ // We normally don't fold numeric constants because they might increase code // size, but it's important to fold numeric constants inside enums since // that's what the TypeScript compiler does. - oldShouldFoldNumericConstants := p.shouldFoldNumericConstants - p.shouldFoldNumericConstants = true + oldShouldFoldTypeScriptConstantExpressions := p.shouldFoldTypeScriptConstantExpressions + p.shouldFoldTypeScriptConstantExpressions = true // Create an assignment for each enum value for _, value := range s.Values { @@ -10197,7 +10223,7 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_ } p.popScope() - p.shouldFoldNumericConstants = oldShouldFoldNumericConstants + p.shouldFoldTypeScriptConstantExpressions = oldShouldFoldTypeScriptConstantExpressions // Track all exported top-level enums for cross-module inlining if tsTopLevelEnumValues != nil { @@ -12420,8 +12446,8 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO // When mangling, inline string values into the template literal. Note that // it may no longer be a template literal after this point (it may turn into // a plain string literal instead). - if p.options.minifySyntax { - expr = js_ast.InlineStringsIntoTemplate(expr.Loc, e) + if p.shouldFoldTypeScriptConstantExpressions || p.options.minifySyntax { + expr = js_ast.InlineStringsAndNumbersIntoTemplate(expr.Loc, e) } shouldLowerTemplateLiteral := p.options.unsupportedJSFeatures.Has(compat.TemplateLiteral) @@ -12553,7 +12579,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO } } - if p.shouldFoldNumericConstants || (p.options.minifySyntax && js_ast.ShouldFoldBinaryArithmeticWhenMinifying(e.Op)) { + if p.shouldFoldTypeScriptConstantExpressions || (p.options.minifySyntax && js_ast.ShouldFoldBinaryArithmeticWhenMinifying(e.Op)) { if result := js_ast.FoldBinaryArithmetic(expr.Loc, e); result.Data != nil { return result, exprOut{} } @@ -13323,7 +13349,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO } case js_ast.UnOpCpl: - if p.shouldFoldNumericConstants || p.options.minifySyntax { + if p.shouldFoldTypeScriptConstantExpressions || p.options.minifySyntax { // Minification folds complement operations since they are unlikely to result in larger output if number, ok := js_ast.ToNumberWithoutSideEffects(e.Value.Data); ok { return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ENumber{Value: float64(^js_ast.ToInt32(number))}}, exprOut{} diff --git a/internal/js_parser/js_parser_test.go b/internal/js_parser/js_parser_test.go index 1c44ae8faae..10ee1222ea3 100644 --- a/internal/js_parser/js_parser_test.go +++ b/internal/js_parser/js_parser_test.go @@ -2419,7 +2419,8 @@ func TestTemplate(t *testing.T) { expectParseError(t, "a?.(b).c\n`${d}`", ": ERROR: Template literals cannot have an optional chain as a tag\n") expectParseError(t, "a?.[b].c\n`${d}`", ": ERROR: Template literals cannot have an optional chain as a tag\n") - expectPrinted(t, "`a${1 + `b${2}c` + 3}d`", "`a${1 + `b${2}c` + 3}d`;\n") + expectPrinted(t, "`a${1 + `b${2}c` + 3}d`", "`a${`1b${2}c3`}d`;\n") + expectPrintedMangle(t, "x = `a${1 + `b${2}c` + 3}d`", "x = `a1b2c3d`;\n") expectPrinted(t, "`a\nb`", "`a\nb`;\n") expectPrinted(t, "`a\rb`", "`a\nb`;\n") @@ -2584,7 +2585,7 @@ func TestConstantFolding(t *testing.T) { expectPrinted(t, "x = x + 'a' + 'b'", "x = x + \"ab\";\n") expectPrinted(t, "x = x + 'a' + 'bc'", "x = x + \"abc\";\n") expectPrinted(t, "x = x + 'ab' + 'c'", "x = x + \"abc\";\n") - expectPrinted(t, "x = 'a' + 1", "x = \"a\" + 1;\n") + expectPrinted(t, "x = 'a' + 1", "x = \"a1\";\n") expectPrinted(t, "x = x * 'a' + 'b'", "x = x * \"a\" + \"b\";\n") expectPrinted(t, "x = 'string' + `template`", "x = `stringtemplate`;\n") @@ -3142,8 +3143,8 @@ func TestMangleSwitch(t *testing.T) { } func TestMangleAddEmptyString(t *testing.T) { - expectPrintedNormalAndMangle(t, "a = '' + 0", "a = \"\" + 0;\n", "a = \"\" + 0;\n") - expectPrintedNormalAndMangle(t, "a = 0 + ''", "a = 0 + \"\";\n", "a = 0 + \"\";\n") + expectPrintedNormalAndMangle(t, "a = '' + 0", "a = \"0\";\n", "a = \"0\";\n") + expectPrintedNormalAndMangle(t, "a = 0 + ''", "a = \"0\";\n", "a = \"0\";\n") expectPrintedNormalAndMangle(t, "a = '' + b", "a = \"\" + b;\n", "a = \"\" + b;\n") expectPrintedNormalAndMangle(t, "a = b + ''", "a = b + \"\";\n", "a = b + \"\";\n")