Skip to content

Commit

Permalink
fix #2214: minify int strings in computed keys
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed May 26, 2022
1 parent 9bf5a79 commit 99744d6
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 16 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Expand Up @@ -43,6 +43,23 @@

The presence of a `"use strict"` directive in the output file is controlled by the presence of one in the entry point. However, there was a bug that would include one twice if the output format is ESM. This bug has been fixed.

* Minify strings into integers inside computed properties ([#2214](https://github.com/evanw/esbuild/issues/2214))

This release now minifies `a["0"]` into `a[0]` when the result is equivalent:

```js
// Original code
console.log(x['0'], { '0': x }, class { '0' = x })

// Old output (with --minify)
console.log(x["0"],{"0":x},class{"0"=x});

// New output (with --minify)
console.log(x[0],{0:x},class{0=x});
```

This transformation currently only happens when the numeric property represents an integer within the signed 32-bit integer range.

## 0.14.39

* Fix code generation for `export default` and `/* @__PURE__ */` call ([#2203](https://github.com/evanw/esbuild/issues/2203))
Expand Down
87 changes: 71 additions & 16 deletions internal/js_parser/js_parser.go
Expand Up @@ -5,6 +5,7 @@ import (
"math"
"regexp"
"sort"
"strconv"
"strings"
"unicode/utf8"

Expand Down Expand Up @@ -10370,25 +10371,31 @@ func (p *parser) visitClass(nameScopeLoc logger.Loc, class *js_ast.Class, defaul
})
property.Key = key

// "class {['x'] = y}" => "class {x = y}"
if p.options.minifySyntax && property.Flags.Has(js_ast.PropertyIsComputed) {
if str, ok := key.Data.(*js_ast.EString); ok && js_lexer.IsIdentifierUTF16(str.Value) {
isInvalidConstructor := false
if helpers.UTF16EqualsString(str.Value, "constructor") {
if !property.Flags.Has(js_ast.PropertyIsMethod) {
// "constructor" is an invalid name for both instance and static fields
isInvalidConstructor = true
} else if !property.Flags.Has(js_ast.PropertyIsStatic) {
// Calling an instance method "constructor" is problematic so avoid that too
isInvalidConstructor = true
if p.options.minifySyntax {
if str, ok := key.Data.(*js_ast.EString); ok {
if numberValue, ok := stringToEquivalentNumberValue(str.Value); ok {
// "class { '123' }" => "class { 123 }"
property.Key.Data = &js_ast.ENumber{Value: numberValue}
property.Flags &= ^js_ast.PropertyIsComputed
} else if property.Flags.Has(js_ast.PropertyIsComputed) {
// "class {['x'] = y}" => "class {'x' = y}"
isInvalidConstructor := false
if helpers.UTF16EqualsString(str.Value, "constructor") {
if !property.Flags.Has(js_ast.PropertyIsMethod) {
// "constructor" is an invalid name for both instance and static fields
isInvalidConstructor = true
} else if !property.Flags.Has(js_ast.PropertyIsStatic) {
// Calling an instance method "constructor" is problematic so avoid that too
isInvalidConstructor = true
}
}
}

// A static property must not be called "prototype"
isInvalidPrototype := property.Flags.Has(js_ast.PropertyIsStatic) && helpers.UTF16EqualsString(str.Value, "prototype")
// A static property must not be called "prototype"
isInvalidPrototype := property.Flags.Has(js_ast.PropertyIsStatic) && helpers.UTF16EqualsString(str.Value, "prototype")

if !isInvalidConstructor && !isInvalidPrototype {
property.Flags &= ^js_ast.PropertyIsComputed
if !isInvalidConstructor && !isInvalidPrototype {
property.Flags &= ^js_ast.PropertyIsComputed
}
}
}
}
Expand Down Expand Up @@ -11350,6 +11357,36 @@ func (p *parser) valueForImportMeta(loc logger.Loc) (js_ast.Expr, bool) {
return js_ast.Expr{}, false
}

func stringToEquivalentNumberValue(value []uint16) (float64, bool) {
if len(value) > 0 {
var intValue int32
isNegative := false
start := 0

if value[0] == '-' && len(value) > 1 {
isNegative = true
start++
}

for _, c := range value[start:] {
if c < '0' || c > '9' {
return 0, false
}
intValue = intValue*10 + int32(c) - '0'
}

if isNegative {
intValue = -intValue
}

if helpers.UTF16EqualsString(value, strconv.FormatInt(int64(intValue), 10)) {
return float64(intValue), true
}
}

return 0, false
}

func isBinaryNullAndUndefined(left js_ast.Expr, right js_ast.Expr, op js_ast.OpCode) (js_ast.Expr, js_ast.Expr, bool) {
if a, ok := left.Data.(*js_ast.EBinary); ok && a.Op == op {
if b, ok := right.Data.(*js_ast.EBinary); ok && b.Op == op {
Expand Down Expand Up @@ -12606,6 +12643,15 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
}
}

// "a['123']" => "a[123]" (this is done late to allow "'123'" to be mangled)
if p.options.minifySyntax {
if str, ok := e.Index.Data.(*js_ast.EString); ok {
if numberValue, ok := stringToEquivalentNumberValue(str.Value); ok {
e.Index.Data = &js_ast.ENumber{Value: numberValue}
}
}
}

return js_ast.Expr{Loc: expr.Loc, Data: e}, out

case *js_ast.EUnary:
Expand Down Expand Up @@ -12972,6 +13018,15 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
}
}
}

// "{ '123': 4 }" => "{ 123: 4 }" (this is done late to allow "'123'" to be mangled)
if p.options.minifySyntax {
if str, ok := property.Key.Data.(*js_ast.EString); ok {
if numberValue, ok := stringToEquivalentNumberValue(str.Value); ok {
property.Key.Data = &js_ast.ENumber{Value: numberValue}
}
}
}
}

// Check for and warn about duplicate keys in object literals
Expand Down
42 changes: 42 additions & 0 deletions internal/js_parser/js_parser_test.go
Expand Up @@ -1214,6 +1214,20 @@ func TestObject(t *testing.T) {
expectParseError(t, "({get x() {}, set x(y) {}, set x(y) {}})", duplicateWarning)
expectParseError(t, "({get x() {}, set x(y) {}})", "")
expectParseError(t, "({set x(y) {}, get x() {}})", "")

// Check the string-to-int optimization
expectPrintedMangle(t, "x = { '0': y }", "x = { 0: y };\n")
expectPrintedMangle(t, "x = { '123': y }", "x = { 123: y };\n")
expectPrintedMangle(t, "x = { '-123': y }", "x = { -123: y };\n")
expectPrintedMangle(t, "x = { '-0': y }", "x = { \"-0\": y };\n")
expectPrintedMangle(t, "x = { '01': y }", "x = { \"01\": y };\n")
expectPrintedMangle(t, "x = { '-01': y }", "x = { \"-01\": y };\n")
expectPrintedMangle(t, "x = { '0x1': y }", "x = { \"0x1\": y };\n")
expectPrintedMangle(t, "x = { '-0x1': y }", "x = { \"-0x1\": y };\n")
expectPrintedMangle(t, "x = { '2147483647': y }", "x = { 2147483647: y };\n")
expectPrintedMangle(t, "x = { '2147483648': y }", "x = { \"2147483648\": y };\n")
expectPrintedMangle(t, "x = { '-2147483648': y }", "x = { -2147483648: y };\n")
expectPrintedMangle(t, "x = { '-2147483649': y }", "x = { \"-2147483649\": y };\n")
}

func TestComputedProperty(t *testing.T) {
Expand Down Expand Up @@ -1556,6 +1570,20 @@ func TestClass(t *testing.T) {
"class Foo {\n constructor() {\n }\n [\"constructor\"]() {\n }\n}\n")
expectPrintedMangle(t, "class Foo { static constructor() {} static ['constructor']() {} }",
"class Foo {\n static constructor() {\n }\n static constructor() {\n }\n}\n")

// Check the string-to-int optimization
expectPrintedMangle(t, "class x { '0' = y }", "class x {\n 0 = y;\n}\n")
expectPrintedMangle(t, "class x { '123' = y }", "class x {\n 123 = y;\n}\n")
expectPrintedMangle(t, "class x { ['-123'] = y }", "class x {\n -123 = y;\n}\n")
expectPrintedMangle(t, "class x { '-0' = y }", "class x {\n \"-0\" = y;\n}\n")
expectPrintedMangle(t, "class x { '01' = y }", "class x {\n \"01\" = y;\n}\n")
expectPrintedMangle(t, "class x { '-01' = y }", "class x {\n \"-01\" = y;\n}\n")
expectPrintedMangle(t, "class x { '0x1' = y }", "class x {\n \"0x1\" = y;\n}\n")
expectPrintedMangle(t, "class x { '-0x1' = y }", "class x {\n \"-0x1\" = y;\n}\n")
expectPrintedMangle(t, "class x { '2147483647' = y }", "class x {\n 2147483647 = y;\n}\n")
expectPrintedMangle(t, "class x { '2147483648' = y }", "class x {\n \"2147483648\" = y;\n}\n")
expectPrintedMangle(t, "class x { ['-2147483648'] = y }", "class x {\n -2147483648 = y;\n}\n")
expectPrintedMangle(t, "class x { ['-2147483649'] = y }", "class x {\n \"-2147483649\" = y;\n}\n")
}

func TestSuperCall(t *testing.T) {
Expand Down Expand Up @@ -2913,6 +2941,20 @@ func TestMangleIndex(t *testing.T) {
expectPrintedMangle(t, "x?.['y z']", "x?.[\"y z\"];\n")
expectPrintedMangle(t, "x?.['y']()", "x?.y();\n")
expectPrintedMangle(t, "x?.['y z']()", "x?.[\"y z\"]();\n")

// Check the string-to-int optimization
expectPrintedMangle(t, "x['0']", "x[0];\n")
expectPrintedMangle(t, "x['123']", "x[123];\n")
expectPrintedMangle(t, "x['-123']", "x[-123];\n")
expectPrintedMangle(t, "x['-0']", "x[\"-0\"];\n")
expectPrintedMangle(t, "x['01']", "x[\"01\"];\n")
expectPrintedMangle(t, "x['-01']", "x[\"-01\"];\n")
expectPrintedMangle(t, "x['0x1']", "x[\"0x1\"];\n")
expectPrintedMangle(t, "x['-0x1']", "x[\"-0x1\"];\n")
expectPrintedMangle(t, "x['2147483647']", "x[2147483647];\n")
expectPrintedMangle(t, "x['2147483648']", "x[\"2147483648\"];\n")
expectPrintedMangle(t, "x['-2147483648']", "x[-2147483648];\n")
expectPrintedMangle(t, "x['-2147483649']", "x[\"-2147483649\"];\n")
}

func TestMangleBlock(t *testing.T) {
Expand Down

0 comments on commit 99744d6

Please sign in to comment.