Skip to content

Commit

Permalink
fix #3561: treeshaking of known Symbol instances
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Dec 29, 2023
1 parent 0811058 commit f8ae3af
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 5 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@
function f(){return $.template('<p contenteditable="false">hello world</p>')}
```

* Minifier: consider properties named using known `Symbol` instances to be side-effect free ([#3561](https://github.com/evanw/esbuild/issues/3561))

Many things in JavaScript can have side effects including property accesses and ToString operations, so using a symbol such as `Symbol.iterator` as a computed property name is not obviously side-effect free. This release adds a special case for known `Symbol` instances so that they are considered side-effect free when used as property names. For example, this class declaration will now be considered side-effect free:

```js
class Foo {
*[Symbol.iterator]() {
}
}
```
* Provide the `stop()` API in node to exit esbuild's child process ([#3558](https://github.com/evanw/esbuild/issues/3558))
You can now call `stop()` in esbuild's node API to exit esbuild's child process to reclaim the resources used. It only makes sense to do this for a long-lived node process when you know you will no longer be making any more esbuild API calls. It is not necessary to call this to allow node to exit, and it's advantageous to not call this in between calls to esbuild's API as sharing a single long-lived esbuild child process is more efficient than re-creating a new esbuild child process for every API call. This API call used to exist but was removed in [version 0.9.0](https://github.com/evanw/esbuild/releases/v0.9.0). This release adds it back due to a user request.
Expand Down
33 changes: 33 additions & 0 deletions internal/bundler_tests/bundler_dce_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4595,3 +4595,36 @@ func TestDropLabelTreeShakingBugIssue3311(t *testing.T) {
},
})
}

func TestDCEOfSymbolInstances(t *testing.T) {
dce_suite.expectBundled(t, bundled{
files: map[string]string{
"/class.js": `
class Remove1 {}
class Remove2 { *[Symbol.iterator]() {} }
class Remove3 { *[Symbol['iterator']]() {} }
class Keep1 { *[Symbol.iterator]() {} [keep] }
class Keep2 { [keep]; *[Symbol.iterator]() {} }
class Keep3 { *[Symbol.wtf]() {} }
`,
"/object.js": `
let remove1 = {}
let remove2 = { *[Symbol.iterator]() {} }
let remove3 = { *[Symbol['iterator']]() {} }
let keep1 = { *[Symbol.iterator]() {}, [keep]: null }
let keep2 = { [keep]: null, *[Symbol.iterator]() {} }
let keep3 = { *[Symbol.wtf]() {} }
`,
},
entryPaths: []string{
"/class.js",
"/object.js",
},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
},
})
}
28 changes: 28 additions & 0 deletions internal/bundler_tests/snapshots/snapshots_dce.txt
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,34 @@ use(isNotPure);
} };
})();

================================================================================
TestDCEOfSymbolInstances
---------- /out/class.js ----------
// class.js
var Keep1 = class {
*[Symbol.iterator]() {
}
[keep];
};
var Keep2 = class {
[keep];
*[Symbol.iterator]() {
}
};
var Keep3 = class {
*[Symbol.wtf]() {
}
};

---------- /out/object.js ----------
// object.js
var keep1 = { *[Symbol.iterator]() {
}, [keep]: null };
var keep2 = { [keep]: null, *[Symbol.iterator]() {
} };
var keep3 = { *[Symbol.wtf]() {
} };

================================================================================
TestDCEOfUsingDeclarations
---------- /out/entry.js ----------
Expand Down
15 changes: 14 additions & 1 deletion internal/config/globals.go
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,10 @@ const (
// output, even when the arguments have side effects. This is used to
// implement the "--drop:console" flag.
MethodCallsMustBeReplacedWithUndefined

// Symbol values are known to not have side effects when used as property
// names in class declarations and object literals.
IsSymbolInstance
)

func (flags DefineFlags) Has(flag DefineFlags) bool {
Expand Down Expand Up @@ -943,7 +947,16 @@ func ProcessDefines(userDefines map[string]DefineData) ProcessedDefines {
if len(parts) == 1 {
result.IdentifierDefines[tail] = DefineData{Flags: CanBeRemovedIfUnused}
} else {
result.DotDefines[tail] = append(result.DotDefines[tail], DotDefine{Parts: parts, Data: DefineData{Flags: CanBeRemovedIfUnused}})
flags := CanBeRemovedIfUnused

// All properties on the "Symbol" global are currently symbol instances
// (i.e. "typeof Symbol.iterator === 'symbol'"). This is used to avoid
// treating properties with these names as having side effects.
if parts[0] == "Symbol" {
flags |= IsSymbolInstance
}

result.DotDefines[tail] = append(result.DotDefines[tail], DotDefine{Parts: parts, Data: DefineData{Flags: flags}})
}
}

Expand Down
14 changes: 12 additions & 2 deletions internal/js_ast/js_ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -624,12 +624,17 @@ type EDot struct {
// unwrapped if the resulting value is unused. Unwrapping means discarding
// the call target but keeping any arguments with side effects.
CallCanBeUnwrappedIfUnused bool

// Symbol values are known to not have side effects when used as property
// names in class declarations and object literals.
IsSymbolInstance bool
}

func (a *EDot) HasSameFlagsAs(b *EDot) bool {
return a.OptionalChain == b.OptionalChain &&
a.CanBeRemovedIfUnused == b.CanBeRemovedIfUnused &&
a.CallCanBeUnwrappedIfUnused == b.CallCanBeUnwrappedIfUnused
a.CallCanBeUnwrappedIfUnused == b.CallCanBeUnwrappedIfUnused &&
a.IsSymbolInstance == b.IsSymbolInstance
}

type EIndex struct {
Expand All @@ -646,12 +651,17 @@ type EIndex struct {
// unwrapped if the resulting value is unused. Unwrapping means discarding
// the call target but keeping any arguments with side effects.
CallCanBeUnwrappedIfUnused bool

// Symbol values are known to not have side effects when used as property
// names in class declarations and object literals.
IsSymbolInstance bool
}

func (a *EIndex) HasSameFlagsAs(b *EIndex) bool {
return a.OptionalChain == b.OptionalChain &&
a.CanBeRemovedIfUnused == b.CanBeRemovedIfUnused &&
a.CallCanBeUnwrappedIfUnused == b.CallCanBeUnwrappedIfUnused
a.CallCanBeUnwrappedIfUnused == b.CallCanBeUnwrappedIfUnused &&
a.IsSymbolInstance == b.IsSymbolInstance
}

type EArrow struct {
Expand Down
18 changes: 16 additions & 2 deletions internal/js_ast/js_ast_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,17 @@ func MaybeSimplifyEqualityComparison(loc logger.Loc, e *EBinary, unsupportedFeat
return Expr{}, false
}

func IsSymbolInstance(data E) bool {
switch e := data.(type) {
case *EDot:
return e.IsSymbolInstance

case *EIndex:
return e.IsSymbolInstance
}
return false
}

func IsPrimitiveLiteral(data E) bool {
switch e := data.(type) {
case *EAnnotation:
Expand Down Expand Up @@ -2173,7 +2184,7 @@ func (ctx HelperContext) ClassCanBeRemovedIfUnused(class Class) bool {
return false
}

if property.Flags.Has(PropertyIsComputed) && !IsPrimitiveLiteral(property.Key.Data) {
if property.Flags.Has(PropertyIsComputed) && !IsPrimitiveLiteral(property.Key.Data) && !IsSymbolInstance(property.Key.Data) {
return false
}

Expand Down Expand Up @@ -2327,7 +2338,10 @@ func (ctx HelperContext) ExprCanBeRemovedIfUnused(expr Expr) bool {
case *EObject:
for _, property := range e.Properties {
// The key must still be evaluated if it's computed or a spread
if property.Kind == PropertySpread || (property.Flags.Has(PropertyIsComputed) && !IsPrimitiveLiteral(property.Key.Data)) {
if property.Kind == PropertySpread {
return false
}
if property.Flags.Has(PropertyIsComputed) && !IsPrimitiveLiteral(property.Key.Data) && !IsSymbolInstance(property.Key.Data) {
return false
}
if property.ValueOrNil.Data != nil && !ctx.ExprCanBeRemovedIfUnused(property.ValueOrNil) {
Expand Down
6 changes: 6 additions & 0 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -13412,6 +13412,9 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
if define.Data.Flags.Has(config.CallCanBeUnwrappedIfUnused) && !p.options.ignoreDCEAnnotations {
e.CallCanBeUnwrappedIfUnused = true
}
if define.Data.Flags.Has(config.IsSymbolInstance) {
e.IsSymbolInstance = true
}
break
}
}
Expand Down Expand Up @@ -13535,6 +13538,9 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
if define.Data.Flags.Has(config.CallCanBeUnwrappedIfUnused) && !p.options.ignoreDCEAnnotations {
e.CallCanBeUnwrappedIfUnused = true
}
if define.Data.Flags.Has(config.IsSymbolInstance) {
e.IsSymbolInstance = true
}
break
}
}
Expand Down

0 comments on commit f8ae3af

Please sign in to comment.