Skip to content

Commit

Permalink
minify undefined checks to typeof x < u
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Feb 2, 2022
1 parent 3108405 commit 81215bc
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 6 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@

You can now target [Opera](https://www.opera.com/) and/or [Internet Explorer](https://www.microsoft.com/en-us/download/internet-explorer.aspx) using the `--target=` setting. For example, `--target=opera45,ie9` targets Opera 45 and Internet Explorer 9. This change does not add any additional features to esbuild's code transformation pipeline to transform newer syntax so that it works in Internet Explorer. It just adds information about what features are supported in these browsers to esbuild's internal feature compatibility table.

* Minify `typeof x !== 'undefined'` to `typeof x < 'u'`

This release introduces a small improvement for code that does a lot of `typeof` checks against `undefined`:

```js
// Original code
y = typeof x !== 'undefined';

// Old output (with --minify)
y=typeof x!="undefined";

// New output (with --minify)
y=typeof x<"u";
```

This transformation is only active when minification is enabled, and is disabled if the language target is set lower than ES2020 or if Internet Explorer is set as a target environment. Before ES2020, implementations were allowed to return non-standard values from the `typeof` operator for a few objects. Internet Explorer took advantage of this to sometimes return the string `'unknown'` instead of `'undefined'`. But this has been removed from the specification and Internet Explorer was the only engine to do this, so this minification is valid for code that does not need to target Internet Explorer.

## 0.14.17

* Attempt to fix an install script issue on Ubuntu Linux ([#1711](https://github.com/evanw/esbuild/issues/1711))
Expand Down
68 changes: 68 additions & 0 deletions internal/bundler/bundler_dce_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1829,6 +1829,74 @@ func TestDCETypeOfEqualsStringGuardCondition(t *testing.T) {
})
}

func TestDCETypeOfCompareStringGuardCondition(t *testing.T) {
dce_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
// Everything here should be removed as dead code due to tree shaking
var REMOVE_1 = typeof x <= 'u' ? x : null
var REMOVE_1 = typeof x < 'u' ? x : null
var REMOVE_1 = typeof x >= 'u' ? null : x
var REMOVE_1 = typeof x > 'u' ? null : x
var REMOVE_1 = typeof x <= 'u' && x
var REMOVE_1 = typeof x < 'u' && x
var REMOVE_1 = typeof x >= 'u' || x
var REMOVE_1 = typeof x > 'u' || x
var REMOVE_1 = 'u' >= typeof x ? x : null
var REMOVE_1 = 'u' > typeof x ? x : null
var REMOVE_1 = 'u' <= typeof x ? null : x
var REMOVE_1 = 'u' < typeof x ? null : x
var REMOVE_1 = 'u' >= typeof x && x
var REMOVE_1 = 'u' > typeof x && x
var REMOVE_1 = 'u' <= typeof x || x
var REMOVE_1 = 'u' < typeof x || x
// Everything here should be kept as live code because it has side effects
var keep_1 = typeof x <= 'u' ? y : null
var keep_1 = typeof x < 'u' ? y : null
var keep_1 = typeof x >= 'u' ? null : y
var keep_1 = typeof x > 'u' ? null : y
var keep_1 = typeof x <= 'u' && y
var keep_1 = typeof x < 'u' && y
var keep_1 = typeof x >= 'u' || y
var keep_1 = typeof x > 'u' || y
var keep_1 = 'u' >= typeof x ? y : null
var keep_1 = 'u' > typeof x ? y : null
var keep_1 = 'u' <= typeof x ? null : y
var keep_1 = 'u' < typeof x ? null : y
var keep_1 = 'u' >= typeof x && y
var keep_1 = 'u' > typeof x && y
var keep_1 = 'u' <= typeof x || y
var keep_1 = 'u' < typeof x || y
// Everything here should be kept as live code because it has side effects
var keep_2 = typeof x <= 'u' ? null : x
var keep_2 = typeof x < 'u' ? null : x
var keep_2 = typeof x >= 'u' ? x : null
var keep_2 = typeof x > 'u' ? x : null
var keep_2 = typeof x <= 'u' || x
var keep_2 = typeof x < 'u' || x
var keep_2 = typeof x >= 'u' && x
var keep_2 = typeof x > 'u' && x
var keep_2 = 'u' >= typeof x ? null : x
var keep_2 = 'u' > typeof x ? null : x
var keep_2 = 'u' <= typeof x ? x : null
var keep_2 = 'u' < typeof x ? x : null
var keep_2 = 'u' >= typeof x || x
var keep_2 = 'u' > typeof x || x
var keep_2 = 'u' <= typeof x && x
var keep_2 = 'u' < typeof x && x
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
OutputFormat: config.FormatIIFE,
AbsOutputFile: "/out.js",
},
})
}

// These unused imports should be removed since they aren't used, and removing
// them makes the code shorter.
func TestRemoveUnusedImports(t *testing.T) {
Expand Down
39 changes: 39 additions & 0 deletions internal/bundler/snapshots/snapshots_dce.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,45 @@ var e = `${alsoKeep}`;
TestDCETypeOf
---------- /out.js ----------

================================================================================
TestDCETypeOfCompareStringGuardCondition
---------- /out.js ----------
(() => {
// entry.js
var keep_1 = typeof x <= "u" ? y : null;
var keep_1 = typeof x < "u" ? y : null;
var keep_1 = typeof x >= "u" ? null : y;
var keep_1 = typeof x > "u" ? null : y;
var keep_1 = typeof x <= "u" && y;
var keep_1 = typeof x < "u" && y;
var keep_1 = typeof x >= "u" || y;
var keep_1 = typeof x > "u" || y;
var keep_1 = "u" >= typeof x ? y : null;
var keep_1 = "u" > typeof x ? y : null;
var keep_1 = "u" <= typeof x ? null : y;
var keep_1 = "u" < typeof x ? null : y;
var keep_1 = "u" >= typeof x && y;
var keep_1 = "u" > typeof x && y;
var keep_1 = "u" <= typeof x || y;
var keep_1 = "u" < typeof x || y;
var keep_2 = typeof x <= "u" ? null : x;
var keep_2 = typeof x < "u" ? null : x;
var keep_2 = typeof x >= "u" ? x : null;
var keep_2 = typeof x > "u" ? x : null;
var keep_2 = typeof x <= "u" || x;
var keep_2 = typeof x < "u" || x;
var keep_2 = typeof x >= "u" && x;
var keep_2 = typeof x > "u" && x;
var keep_2 = "u" >= typeof x ? null : x;
var keep_2 = "u" > typeof x ? null : x;
var keep_2 = "u" <= typeof x ? x : null;
var keep_2 = "u" < typeof x ? x : null;
var keep_2 = "u" >= typeof x || x;
var keep_2 = "u" > typeof x || x;
var keep_2 = "u" <= typeof x && x;
var keep_2 = "u" < typeof x && x;
})();

================================================================================
TestDCETypeOfEqualsString
---------- /out.js ----------
Expand Down
11 changes: 11 additions & 0 deletions internal/compat/js_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const (
RestArgument
TemplateLiteral
TopLevelAwait
TypeofExoticObjectIsObject
UnicodeEscapes
)

Expand Down Expand Up @@ -485,6 +486,16 @@ var jsTable = map[JSFeature]map[Engine][]versionRange{
Node: {{start: v{14, 8, 0}}},
Safari: {{start: v{15, 0, 0}}},
},
TypeofExoticObjectIsObject: {
Chrome: {{start: v{0, 0, 0}}},
Edge: {{start: v{0, 0, 0}}},
ES: {{start: v{2020, 0, 0}}},
Firefox: {{start: v{0, 0, 0}}},
IOS: {{start: v{0, 0, 0}}},
Node: {{start: v{0, 0, 0}}},
Opera: {{start: v{0, 0, 0}}},
Safari: {{start: v{0, 0, 0}}},
},
UnicodeEscapes: {
Chrome: {{start: v{44, 0, 0}}},
Edge: {{start: v{12, 0, 0}}},
Expand Down
61 changes: 55 additions & 6 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -10587,19 +10587,41 @@ func canChangeStrictToLoose(a js_ast.Expr, b js_ast.Expr) bool {
return x == y && x != js_ast.PrimitiveUnknown && x != js_ast.PrimitiveMixed
}

func maybeSimplifyEqualityComparison(e *js_ast.EBinary, isNotEqual bool) (js_ast.Expr, bool) {
func (p *parser) maybeSimplifyEqualityComparison(loc logger.Loc, e *js_ast.EBinary) (js_ast.Expr, bool) {
// "!x === true" => "!x"
// "!x === false" => "!!x"
// "!x !== true" => "!!x"
// "!x !== false" => "!x"
if boolean, ok := e.Right.Data.(*js_ast.EBoolean); ok && js_ast.KnownPrimitiveType(e.Left) == js_ast.PrimitiveBoolean {
if boolean.Value == isNotEqual {
if boolean.Value == (e.Op == js_ast.BinOpLooseNe || e.Op == js_ast.BinOpStrictNe) {
return js_ast.Not(e.Left), true
} else {
return e.Left, true
}
}

// "typeof x != 'undefined'" => "typeof x < 'u'"
// "typeof x == 'undefined'" => "typeof x > 'u'"
if !p.options.unsupportedJSFeatures.Has(compat.TypeofExoticObjectIsObject) {
// Only do this optimization if we know that the "typeof" operator won't
// return something random. The only case of this happening was Internet
// Explorer returning "unknown" for some objects, which messes with this
// optimization. So we don't do this when targeting Internet Explorer.
if typeof, ok := e.Left.Data.(*js_ast.EUnary); ok && typeof.Op == js_ast.UnOpTypeof {
if str, ok := e.Right.Data.(*js_ast.EString); ok && js_lexer.UTF16EqualsString(str.Value, "undefined") {
op := js_ast.BinOpLt
if e.Op == js_ast.BinOpLooseEq || e.Op == js_ast.BinOpStrictEq {
op = js_ast.BinOpGt
}
return js_ast.Expr{Loc: loc, Data: &js_ast.EBinary{
Op: op,
Left: e.Left,
Right: js_ast.Expr{Loc: e.Right.Loc, Data: &js_ast.EString{Value: []uint16{'u'}}},
}}, true
}
}
}

return js_ast.Expr{}, false
}

Expand Down Expand Up @@ -11733,7 +11755,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
e.Right.Data = js_ast.ENullShared
}

if result, ok := maybeSimplifyEqualityComparison(e, false /* isNotEqual */); ok {
if result, ok := p.maybeSimplifyEqualityComparison(expr.Loc, e); ok {
return result, exprOut{}
}
}
Expand All @@ -11754,7 +11776,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
e.Op = js_ast.BinOpLooseEq
}

if result, ok := maybeSimplifyEqualityComparison(e, false /* isNotEqual */); ok {
if result, ok := p.maybeSimplifyEqualityComparison(expr.Loc, e); ok {
return result, exprOut{}
}
}
Expand All @@ -11775,7 +11797,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
e.Right.Data = js_ast.ENullShared
}

if result, ok := maybeSimplifyEqualityComparison(e, true /* isNotEqual */); ok {
if result, ok := p.maybeSimplifyEqualityComparison(expr.Loc, e); ok {
return result, exprOut{}
}
}
Expand All @@ -11796,7 +11818,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
e.Op = js_ast.BinOpLooseNe
}

if result, ok := maybeSimplifyEqualityComparison(e, true /* isNotEqual */); ok {
if result, ok := p.maybeSimplifyEqualityComparison(expr.Loc, e); ok {
return result, exprOut{}
}
}
Expand Down Expand Up @@ -14557,6 +14579,14 @@ func (p *parser) exprCanBeRemovedIfUnused(expr js_ast.Expr) bool {
// we must also consider "typeof x == 'object'" to be side-effect free.
case js_ast.BinOpLooseEq, js_ast.BinOpLooseNe:
return canChangeStrictToLoose(e.Left, e.Right) && p.exprCanBeRemovedIfUnused(e.Left) && p.exprCanBeRemovedIfUnused(e.Right)

// Special-case "<" and ">" with string, number, or bigint arguments
case js_ast.BinOpLt, js_ast.BinOpGt, js_ast.BinOpLe, js_ast.BinOpGe:
left := js_ast.KnownPrimitiveType(e.Left)
switch left {
case js_ast.PrimitiveString, js_ast.PrimitiveNumber, js_ast.PrimitiveBigInt:
return js_ast.KnownPrimitiveType(e.Right) == left && p.exprCanBeRemovedIfUnused(e.Left) && p.exprCanBeRemovedIfUnused(e.Right)
}
}

case *js_ast.ETemplate:
Expand Down Expand Up @@ -14599,6 +14629,25 @@ func (p *parser) isSideEffectFreeUnboundIdentifierRef(value js_ast.Expr, guardCo
}
}
}

case js_ast.BinOpLt, js_ast.BinOpGt, js_ast.BinOpLe, js_ast.BinOpGe:
// Pattern match for "typeof x < <string>"
typeof, string := binary.Left, binary.Right
if _, ok := typeof.Data.(*js_ast.EString); ok {
typeof, string = string, typeof
isYesBranch = !isYesBranch
}
if typeof, ok := typeof.Data.(*js_ast.EUnary); ok && typeof.Op == js_ast.UnOpTypeof {
if text, ok := string.Data.(*js_ast.EString); ok && js_lexer.UTF16EqualsString(text.Value, "u") {
// In "typeof x < 'u' ? x : null", the reference to "x" is side-effect free
// In "typeof x > 'u' ? x : null", the reference to "x" is side-effect free
if isYesBranch == (binary.Op == js_ast.BinOpLt || binary.Op == js_ast.BinOpLe) {
if id2, ok := typeof.Value.Data.(*js_ast.EIdentifier); ok && id2.Ref == id.Ref {
return true
}
}
}
}
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions internal/js_parser/js_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3554,6 +3554,18 @@ func TestMangleTypeofIdentifier(t *testing.T) {
expectPrintedMangle(t, "return typeof (false || x); var x", "return typeof x;\nvar x;\n")
}

func TestMangleTypeofEqualsUndefined(t *testing.T) {
expectPrintedMangle(t, "return typeof x !== 'undefined'", "return typeof x < \"u\";\n")
expectPrintedMangle(t, "return typeof x != 'undefined'", "return typeof x < \"u\";\n")
expectPrintedMangle(t, "return 'undefined' !== typeof x", "return typeof x < \"u\";\n")
expectPrintedMangle(t, "return 'undefined' != typeof x", "return typeof x < \"u\";\n")

expectPrintedMangle(t, "return typeof x === 'undefined'", "return typeof x > \"u\";\n")
expectPrintedMangle(t, "return typeof x == 'undefined'", "return typeof x > \"u\";\n")
expectPrintedMangle(t, "return 'undefined' === typeof x", "return typeof x > \"u\";\n")
expectPrintedMangle(t, "return 'undefined' == typeof x", "return typeof x > \"u\";\n")
}

func TestMangleEquals(t *testing.T) {
expectPrintedMangle(t, "return typeof x === y", "return typeof x === y;\n")
expectPrintedMangle(t, "return typeof x !== y", "return typeof x !== y;\n")
Expand Down
13 changes: 13 additions & 0 deletions scripts/compat-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ mergeVersions('BigInt', { es2020: true })
mergeVersions('ImportMeta', { es2020: true })
mergeVersions('NullishCoalescing', { es2020: true })
mergeVersions('OptionalChain', { es2020: true })
mergeVersions('TypeofExoticObjectIsObject', { es2020: true }) // https://github.com/tc39/ecma262/pull/1441
mergeVersions('LogicalAssignment', { es2021: true })
mergeVersions('TopLevelAwait', {})
mergeVersions('ArbitraryModuleNamespaceNames', {})
Expand Down Expand Up @@ -201,6 +202,18 @@ mergeVersions('DynamicImport', {
safari11_1: true,
})

// This is a problem specific to Internet explorer. See https://github.com/tc39/ecma262/issues/1440
mergeVersions('TypeofExoticObjectIsObject', {
chrome0: true,
edge0: true,
es0: true,
firefox0: true,
ios0: true,
node0: true,
opera0: true,
safari0: true,
})

// This is a special case. Node added support for it to both v12.20+ and v13.2+
// so the range is inconveniently discontiguous. Sources:
//
Expand Down
9 changes: 9 additions & 0 deletions scripts/js-api-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -4249,6 +4249,15 @@ let transformTests = {
assert.strictEqual(fromPromiseResolve(code4), `Promise.resolve().then(function(){return __toESM(require(foo))});\n`)
},

async typeofEqualsUndefinedTarget({ esbuild }) {
assert.strictEqual((await esbuild.transform(`a = typeof b !== 'undefined'`, { minify: true })).code, `a=typeof b<"u";\n`)
assert.strictEqual((await esbuild.transform(`a = typeof b !== 'undefined'`, { minify: true, target: 'es2020' })).code, `a=typeof b<"u";\n`)
assert.strictEqual((await esbuild.transform(`a = typeof b !== 'undefined'`, { minify: true, target: 'chrome11' })).code, `a=typeof b<"u";\n`)

assert.strictEqual((await esbuild.transform(`a = typeof b !== 'undefined'`, { minify: true, target: 'es2019' })).code, `a=typeof b!="undefined";\n`)
assert.strictEqual((await esbuild.transform(`a = typeof b !== 'undefined'`, { minify: true, target: 'ie11' })).code, `a=typeof b!="undefined";\n`)
},

async caseInsensitiveTarget({ esbuild }) {
assert.strictEqual((await esbuild.transform(`a ||= b`, { target: 'eS5' })).code, `a || (a = b);\n`)
assert.strictEqual((await esbuild.transform(`a ||= b`, { target: 'eSnExT' })).code, `a ||= b;\n`)
Expand Down

0 comments on commit 81215bc

Please sign in to comment.