Skip to content

Commit

Permalink
fix #2962: minify global primitive constructors
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Mar 3, 2023
1 parent a5f781e commit 2176b35
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 0 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

## Unreleased

* Minify calls to some global primitive constructors ([#2962](https://github.com/evanw/esbuild/issues/2962))

With this release, esbuild's minifier now replaces calls to `Boolean`/`Number`/`String`/`BigInt` with equivalent shorter code when relevant:

```js
// Original code
console.log(
Boolean(a ? (b | c) !== 0 : (c & d) !== 0),
Number(e ? '1' : '2'),
String(e ? '1' : '2'),
BigInt(e ? 1n : 2n),
)

// Old output (with --minify)
console.log(Boolean(a?(b|c)!==0:(c&d)!==0),Number(e?"1":"2"),String(e?"1":"2"),BigInt(e?1n:2n));

// New output (with --minify)
console.log(!!(a?b|c:c&d),+(e?"1":"2"),e?"1":"2",e?1n:2n);
```

* Adjust some feature compatibility tables for node ([#2940](https://github.com/evanw/esbuild/issues/2940))

This release makes the following adjustments to esbuild's internal feature compatibility tables for node, which tell esbuild which versions of node are known to support all aspects of that feature:
Expand Down
63 changes: 63 additions & 0 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -13715,6 +13715,69 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
}
}

// Optimize references to global constructors
if p.options.minifySyntax && t.CanBeRemovedIfUnused && len(e.Args) <= 1 && !hasSpread {
if symbol := &p.symbols[t.Ref.InnerIndex]; symbol.Kind == js_ast.SymbolUnbound {
// Note: We construct expressions by assigning to "expr.Data" so
// that the source map position for the constructor is preserved
switch symbol.OriginalName {
case "Boolean":
if len(e.Args) == 0 {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EBoolean{Value: false}}, exprOut{}
} else {
expr.Data = &js_ast.EUnary{Value: js_ast.SimplifyBooleanExpr(e.Args[0]), Op: js_ast.UnOpNot}
return js_ast.Not(expr), exprOut{}
}

case "Number":
if len(e.Args) == 0 {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ENumber{Value: 0}}, exprOut{}
} else {
arg := e.Args[0]

switch js_ast.KnownPrimitiveType(arg.Data) {
case js_ast.PrimitiveNumber:
return arg, exprOut{}

case
js_ast.PrimitiveUndefined, // NaN
js_ast.PrimitiveNull, // 0
js_ast.PrimitiveBoolean, // 0 or 1
js_ast.PrimitiveString: // StringToNumber
if number, ok := js_ast.ToNumberWithoutSideEffects(arg.Data); ok {
expr.Data = &js_ast.ENumber{Value: number}
} else {
expr.Data = &js_ast.EUnary{Value: arg, Op: js_ast.UnOpPos}
}
return expr, exprOut{}
}
}

case "String":
if len(e.Args) == 0 {
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EString{Value: nil}}, exprOut{}
} else {
arg := e.Args[0]

switch js_ast.KnownPrimitiveType(arg.Data) {
case js_ast.PrimitiveString:
return arg, exprOut{}
}
}

case "BigInt":
if len(e.Args) == 1 {
arg := e.Args[0]

switch js_ast.KnownPrimitiveType(arg.Data) {
case js_ast.PrimitiveBigInt:
return arg, exprOut{}
}
}
}
}
}

// Copy the call side effect flag over if this is a known target
if t.CallCanBeUnwrappedIfUnused {
e.CanBeUnwrappedIfUnused = true
Expand Down
54 changes: 54 additions & 0 deletions internal/js_parser/js_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3277,6 +3277,60 @@ func TestMangleDoubleNot(t *testing.T) {
expectPrintedNormalAndMangle(t, "a = !!(b, c)", "a = !!(b, c);\n", "a = (b, !!c);\n")
}

func TestMangleBooleanConstructor(t *testing.T) {
expectPrintedNormalAndMangle(t, "a = Boolean(b); var Boolean", "a = Boolean(b);\nvar Boolean;\n", "a = Boolean(b);\nvar Boolean;\n")

expectPrintedNormalAndMangle(t, "a = Boolean()", "a = Boolean();\n", "a = false;\n")
expectPrintedNormalAndMangle(t, "a = Boolean(b)", "a = Boolean(b);\n", "a = !!b;\n")
expectPrintedNormalAndMangle(t, "a = Boolean(!b)", "a = Boolean(!b);\n", "a = !b;\n")
expectPrintedNormalAndMangle(t, "a = Boolean(!!b)", "a = Boolean(!!b);\n", "a = !!b;\n")
expectPrintedNormalAndMangle(t, "a = Boolean(b ? true : false)", "a = Boolean(b ? true : false);\n", "a = !!b;\n")
expectPrintedNormalAndMangle(t, "a = Boolean(b ? false : true)", "a = Boolean(b ? false : true);\n", "a = !b;\n")
expectPrintedNormalAndMangle(t, "a = Boolean(b ? c > 0 : c < 0)", "a = Boolean(b ? c > 0 : c < 0);\n", "a = b ? c > 0 : c < 0;\n")

// Check for calling "SimplifyBooleanExpr" on the argument
expectPrintedNormalAndMangle(t, "a = Boolean((b | c) !== 0)", "a = Boolean((b | c) !== 0);\n", "a = !!(b | c);\n")
expectPrintedNormalAndMangle(t, "a = Boolean(b ? (c | d) !== 0 : (d | e) !== 0)", "a = Boolean(b ? (c | d) !== 0 : (d | e) !== 0);\n", "a = !!(b ? c | d : d | e);\n")
}

func TestMangleNumberConstructor(t *testing.T) {
expectPrintedNormalAndMangle(t, "a = Number(x)", "a = Number(x);\n", "a = Number(x);\n")
expectPrintedNormalAndMangle(t, "a = Number(0n)", "a = Number(0n);\n", "a = Number(0n);\n")
expectPrintedNormalAndMangle(t, "a = Number(false); var Number", "a = Number(false);\nvar Number;\n", "a = Number(false);\nvar Number;\n")
expectPrintedNormalAndMangle(t, "a = Number(0xFFFF_FFFF_FFFF_FFFFn)", "a = Number(0xFFFFFFFFFFFFFFFFn);\n", "a = Number(0xFFFFFFFFFFFFFFFFn);\n")

expectPrintedNormalAndMangle(t, "a = Number()", "a = Number();\n", "a = 0;\n")
expectPrintedNormalAndMangle(t, "a = Number(-123)", "a = Number(-123);\n", "a = -123;\n")
expectPrintedNormalAndMangle(t, "a = Number(false)", "a = Number(false);\n", "a = 0;\n")
expectPrintedNormalAndMangle(t, "a = Number(true)", "a = Number(true);\n", "a = 1;\n")
expectPrintedNormalAndMangle(t, "a = Number(undefined)", "a = Number(void 0);\n", "a = NaN;\n")
expectPrintedNormalAndMangle(t, "a = Number(null)", "a = Number(null);\n", "a = 0;\n")
expectPrintedNormalAndMangle(t, "a = Number(b ? !c : !d)", "a = Number(b ? !c : !d);\n", "a = +(b ? !c : !d);\n")
}

func TestMangleStringConstructor(t *testing.T) {
expectPrintedNormalAndMangle(t, "a = String(x)", "a = String(x);\n", "a = String(x);\n")
expectPrintedNormalAndMangle(t, "a = String('x'); var String", "a = String(\"x\");\nvar String;\n", "a = String(\"x\");\nvar String;\n")

expectPrintedNormalAndMangle(t, "a = String()", "a = String();\n", "a = \"\";\n")
expectPrintedNormalAndMangle(t, "a = String('x')", "a = String(\"x\");\n", "a = \"x\";\n")
expectPrintedNormalAndMangle(t, "a = String(b ? 'x' : 'y')", "a = String(b ? \"x\" : \"y\");\n", "a = b ? \"x\" : \"y\";\n")
}

func TestMangleBigIntConstructor(t *testing.T) {
expectPrintedNormalAndMangle(t, "a = BigInt(x)", "a = BigInt(x);\n", "a = BigInt(x);\n")
expectPrintedNormalAndMangle(t, "a = BigInt(0n); var BigInt", "a = BigInt(0n);\nvar BigInt;\n", "a = BigInt(0n);\nvar BigInt;\n")

// Note: This throws instead of returning "0n"
expectPrintedNormalAndMangle(t, "a = BigInt()", "a = BigInt();\n", "a = BigInt();\n")

// Note: Transforming this into "0n" is unsafe because that syntax may not be supported
expectPrintedNormalAndMangle(t, "a = BigInt('0')", "a = BigInt(\"0\");\n", "a = BigInt(\"0\");\n")

expectPrintedNormalAndMangle(t, "a = BigInt(0n)", "a = BigInt(0n);\n", "a = 0n;\n")
expectPrintedNormalAndMangle(t, "a = BigInt(b ? 0n : 1n)", "a = BigInt(b ? 0n : 1n);\n", "a = b ? 0n : 1n;\n")
}

func TestMangleIf(t *testing.T) {
expectPrintedNormalAndMangle(t, "1 ? a() : b()", "1 ? a() : b();\n", "a();\n")
expectPrintedNormalAndMangle(t, "0 ? a() : b()", "0 ? a() : b();\n", "b();\n")
Expand Down
92 changes: 92 additions & 0 deletions scripts/end-to-end-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2800,6 +2800,98 @@ for (const minify of [[], ['--minify-syntax']]) {
`,
}),
);

// Check global constructor behavior
tests.push(
test(['in.js', '--outfile=node.js'].concat(minify), {
'in.js': `
const check = (before, after) => {
if (Boolean(before) !== after) throw 'fail: Boolean(' + before + ') should not be ' + Boolean(before)
if (new Boolean(before) === after) throw 'fail: new Boolean(' + before + ') should not be ' + new Boolean(before)
if (new Boolean(before).valueOf() !== after) throw 'fail: new Boolean(' + before + ').valueOf() should not be ' + new Boolean(before).valueOf()
}
check(false, false); check(0, false); check(0n, false)
check(true, true); check(1, true); check(1n, true)
check(null, false); check(undefined, false)
check('', false); check('x', true)

const checkSpread = (before, after) => {
if (Boolean(...before) !== after) throw 'fail: Boolean(...' + before + ') should not be ' + Boolean(...before)
if (new Boolean(...before) === after) throw 'fail: new Boolean(...' + before + ') should not be ' + new Boolean(...before)
if (new Boolean(...before).valueOf() !== after) throw 'fail: new Boolean(...' + before + ').valueOf() should not be ' + new Boolean(...before).valueOf()
}
checkSpread([0], false); check([1], true)
checkSpread([], false)
`,
}),
test(['in.js', '--outfile=node.js'].concat(minify), {
'in.js': `
class ToPrimitive { [Symbol.toPrimitive]() { return '100.001' } }
const someObject = { toString: () => 123, valueOf: () => 321 }

const check = (before, after) => {
if (Number(before) !== after) throw 'fail: Number(' + before + ') should not be ' + Number(before)
if (new Number(before) === after) throw 'fail: new Number(' + before + ') should not be ' + new Number(before)
if (new Number(before).valueOf() !== after) throw 'fail: new Number(' + before + ').valueOf() should not be ' + new Number(before).valueOf()
}
check(-1.23, -1.23)
check('-1.23', -1.23)
check(123n, 123)
check(null, 0)
check(false, 0)
check(true, 1)
check(someObject, 321)
check(new ToPrimitive(), 100.001)

const checkSpread = (before, after) => {
if (Number(...before) !== after) throw 'fail: Number(...' + before + ') should not be ' + Number(...before)
if (new Number(...before) === after) throw 'fail: new Number(...' + before + ') should not be ' + new Number(...before)
if (new Number(...before).valueOf() !== after) throw 'fail: new Number(...' + before + ').valueOf() should not be ' + new Number(...before).valueOf()
}
checkSpread(['123'], 123)
checkSpread([], 0)
`,
}),
test(['in.js', '--outfile=node.js'].concat(minify), {
'in.js': `
class ToPrimitive { [Symbol.toPrimitive]() { return 100.001 } }
const someObject = { toString: () => 123, valueOf: () => 321 }

const check = (before, after) => {
if (String(before) !== after) throw 'fail: String(' + before + ') should not be ' + String(before)
if (new String(before) === after) throw 'fail: new String(' + before + ') should not be ' + new String(before)
if (new String(before).valueOf() !== after) throw 'fail: new String(' + before + ').valueOf() should not be ' + new String(before).valueOf()
}
check('', '')
check('x', 'x')
check(null, 'null')
check(false, 'false')
check(1.23, '1.23')
check(-123n, '-123')
check(someObject, '123')
check(new ToPrimitive(), '100.001')

const checkSpread = (before, after) => {
if (String(...before) !== after) throw 'fail: String(...' + before + ') should not be ' + String(...before)
if (new String(...before) === after) throw 'fail: new String(...' + before + ') should not be ' + new String(...before)
if (new String(...before).valueOf() !== after) throw 'fail: new String(...' + before + ').valueOf() should not be ' + new String(...before).valueOf()
}
checkSpread([123], '123')
checkSpread([], '')

const checkAndExpectNewToThrow = (before, after) => {
if (String(before) !== after) throw 'fail: String(...) should not be ' + String(before)
try {
new String(before)
} catch (e) {
return
}
throw 'fail: new String(...) should not succeed'
}
checkAndExpectNewToThrow(Symbol('abc'), 'Symbol(abc)')
`,
}),
);
}

// Test minification of top-level symbols
Expand Down

0 comments on commit 2176b35

Please sign in to comment.