Skip to content

Commit f8ae3af

Browse files
committed
fix evanw#3561: treeshaking of known Symbol instances
1 parent 0811058 commit f8ae3af

File tree

7 files changed

+120
-5
lines changed

7 files changed

+120
-5
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@
6464
function f(){return $.template('<p contenteditable="false">hello world</p>')}
6565
```
6666

67+
* Minifier: consider properties named using known `Symbol` instances to be side-effect free ([#3561](https://github.com/evanw/esbuild/issues/3561))
68+
69+
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:
70+
71+
```js
72+
class Foo {
73+
*[Symbol.iterator]() {
74+
}
75+
}
76+
```
77+
6778
* Provide the `stop()` API in node to exit esbuild's child process ([#3558](https://github.com/evanw/esbuild/issues/3558))
6879
6980
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.

internal/bundler_tests/bundler_dce_test.go

+33
Original file line numberDiff line numberDiff line change
@@ -4595,3 +4595,36 @@ func TestDropLabelTreeShakingBugIssue3311(t *testing.T) {
45954595
},
45964596
})
45974597
}
4598+
4599+
func TestDCEOfSymbolInstances(t *testing.T) {
4600+
dce_suite.expectBundled(t, bundled{
4601+
files: map[string]string{
4602+
"/class.js": `
4603+
class Remove1 {}
4604+
class Remove2 { *[Symbol.iterator]() {} }
4605+
class Remove3 { *[Symbol['iterator']]() {} }
4606+
4607+
class Keep1 { *[Symbol.iterator]() {} [keep] }
4608+
class Keep2 { [keep]; *[Symbol.iterator]() {} }
4609+
class Keep3 { *[Symbol.wtf]() {} }
4610+
`,
4611+
"/object.js": `
4612+
let remove1 = {}
4613+
let remove2 = { *[Symbol.iterator]() {} }
4614+
let remove3 = { *[Symbol['iterator']]() {} }
4615+
4616+
let keep1 = { *[Symbol.iterator]() {}, [keep]: null }
4617+
let keep2 = { [keep]: null, *[Symbol.iterator]() {} }
4618+
let keep3 = { *[Symbol.wtf]() {} }
4619+
`,
4620+
},
4621+
entryPaths: []string{
4622+
"/class.js",
4623+
"/object.js",
4624+
},
4625+
options: config.Options{
4626+
Mode: config.ModeBundle,
4627+
AbsOutputDir: "/out",
4628+
},
4629+
})
4630+
}

internal/bundler_tests/snapshots/snapshots_dce.txt

+28
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,34 @@ use(isNotPure);
555555
} };
556556
})();
557557

558+
================================================================================
559+
TestDCEOfSymbolInstances
560+
---------- /out/class.js ----------
561+
// class.js
562+
var Keep1 = class {
563+
*[Symbol.iterator]() {
564+
}
565+
[keep];
566+
};
567+
var Keep2 = class {
568+
[keep];
569+
*[Symbol.iterator]() {
570+
}
571+
};
572+
var Keep3 = class {
573+
*[Symbol.wtf]() {
574+
}
575+
};
576+
577+
---------- /out/object.js ----------
578+
// object.js
579+
var keep1 = { *[Symbol.iterator]() {
580+
}, [keep]: null };
581+
var keep2 = { [keep]: null, *[Symbol.iterator]() {
582+
} };
583+
var keep3 = { *[Symbol.wtf]() {
584+
} };
585+
558586
================================================================================
559587
TestDCEOfUsingDeclarations
560588
---------- /out/entry.js ----------

internal/config/globals.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,10 @@ const (
890890
// output, even when the arguments have side effects. This is used to
891891
// implement the "--drop:console" flag.
892892
MethodCallsMustBeReplacedWithUndefined
893+
894+
// Symbol values are known to not have side effects when used as property
895+
// names in class declarations and object literals.
896+
IsSymbolInstance
893897
)
894898

895899
func (flags DefineFlags) Has(flag DefineFlags) bool {
@@ -943,7 +947,16 @@ func ProcessDefines(userDefines map[string]DefineData) ProcessedDefines {
943947
if len(parts) == 1 {
944948
result.IdentifierDefines[tail] = DefineData{Flags: CanBeRemovedIfUnused}
945949
} else {
946-
result.DotDefines[tail] = append(result.DotDefines[tail], DotDefine{Parts: parts, Data: DefineData{Flags: CanBeRemovedIfUnused}})
950+
flags := CanBeRemovedIfUnused
951+
952+
// All properties on the "Symbol" global are currently symbol instances
953+
// (i.e. "typeof Symbol.iterator === 'symbol'"). This is used to avoid
954+
// treating properties with these names as having side effects.
955+
if parts[0] == "Symbol" {
956+
flags |= IsSymbolInstance
957+
}
958+
959+
result.DotDefines[tail] = append(result.DotDefines[tail], DotDefine{Parts: parts, Data: DefineData{Flags: flags}})
947960
}
948961
}
949962

internal/js_ast/js_ast.go

+12-2
Original file line numberDiff line numberDiff line change
@@ -624,12 +624,17 @@ type EDot struct {
624624
// unwrapped if the resulting value is unused. Unwrapping means discarding
625625
// the call target but keeping any arguments with side effects.
626626
CallCanBeUnwrappedIfUnused bool
627+
628+
// Symbol values are known to not have side effects when used as property
629+
// names in class declarations and object literals.
630+
IsSymbolInstance bool
627631
}
628632

629633
func (a *EDot) HasSameFlagsAs(b *EDot) bool {
630634
return a.OptionalChain == b.OptionalChain &&
631635
a.CanBeRemovedIfUnused == b.CanBeRemovedIfUnused &&
632-
a.CallCanBeUnwrappedIfUnused == b.CallCanBeUnwrappedIfUnused
636+
a.CallCanBeUnwrappedIfUnused == b.CallCanBeUnwrappedIfUnused &&
637+
a.IsSymbolInstance == b.IsSymbolInstance
633638
}
634639

635640
type EIndex struct {
@@ -646,12 +651,17 @@ type EIndex struct {
646651
// unwrapped if the resulting value is unused. Unwrapping means discarding
647652
// the call target but keeping any arguments with side effects.
648653
CallCanBeUnwrappedIfUnused bool
654+
655+
// Symbol values are known to not have side effects when used as property
656+
// names in class declarations and object literals.
657+
IsSymbolInstance bool
649658
}
650659

651660
func (a *EIndex) HasSameFlagsAs(b *EIndex) bool {
652661
return a.OptionalChain == b.OptionalChain &&
653662
a.CanBeRemovedIfUnused == b.CanBeRemovedIfUnused &&
654-
a.CallCanBeUnwrappedIfUnused == b.CallCanBeUnwrappedIfUnused
663+
a.CallCanBeUnwrappedIfUnused == b.CallCanBeUnwrappedIfUnused &&
664+
a.IsSymbolInstance == b.IsSymbolInstance
655665
}
656666

657667
type EArrow struct {

internal/js_ast/js_ast_helpers.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,17 @@ func MaybeSimplifyEqualityComparison(loc logger.Loc, e *EBinary, unsupportedFeat
184184
return Expr{}, false
185185
}
186186

187+
func IsSymbolInstance(data E) bool {
188+
switch e := data.(type) {
189+
case *EDot:
190+
return e.IsSymbolInstance
191+
192+
case *EIndex:
193+
return e.IsSymbolInstance
194+
}
195+
return false
196+
}
197+
187198
func IsPrimitiveLiteral(data E) bool {
188199
switch e := data.(type) {
189200
case *EAnnotation:
@@ -2173,7 +2184,7 @@ func (ctx HelperContext) ClassCanBeRemovedIfUnused(class Class) bool {
21732184
return false
21742185
}
21752186

2176-
if property.Flags.Has(PropertyIsComputed) && !IsPrimitiveLiteral(property.Key.Data) {
2187+
if property.Flags.Has(PropertyIsComputed) && !IsPrimitiveLiteral(property.Key.Data) && !IsSymbolInstance(property.Key.Data) {
21772188
return false
21782189
}
21792190

@@ -2327,7 +2338,10 @@ func (ctx HelperContext) ExprCanBeRemovedIfUnused(expr Expr) bool {
23272338
case *EObject:
23282339
for _, property := range e.Properties {
23292340
// The key must still be evaluated if it's computed or a spread
2330-
if property.Kind == PropertySpread || (property.Flags.Has(PropertyIsComputed) && !IsPrimitiveLiteral(property.Key.Data)) {
2341+
if property.Kind == PropertySpread {
2342+
return false
2343+
}
2344+
if property.Flags.Has(PropertyIsComputed) && !IsPrimitiveLiteral(property.Key.Data) && !IsSymbolInstance(property.Key.Data) {
23312345
return false
23322346
}
23332347
if property.ValueOrNil.Data != nil && !ctx.ExprCanBeRemovedIfUnused(property.ValueOrNil) {

internal/js_parser/js_parser.go

+6
Original file line numberDiff line numberDiff line change
@@ -13412,6 +13412,9 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
1341213412
if define.Data.Flags.Has(config.CallCanBeUnwrappedIfUnused) && !p.options.ignoreDCEAnnotations {
1341313413
e.CallCanBeUnwrappedIfUnused = true
1341413414
}
13415+
if define.Data.Flags.Has(config.IsSymbolInstance) {
13416+
e.IsSymbolInstance = true
13417+
}
1341513418
break
1341613419
}
1341713420
}
@@ -13535,6 +13538,9 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
1353513538
if define.Data.Flags.Has(config.CallCanBeUnwrappedIfUnused) && !p.options.ignoreDCEAnnotations {
1353613539
e.CallCanBeUnwrappedIfUnused = true
1353713540
}
13541+
if define.Data.Flags.Has(config.IsSymbolInstance) {
13542+
e.IsSymbolInstance = true
13543+
}
1353813544
break
1353913545
}
1354013546
}

0 commit comments

Comments
 (0)