Skip to content

Commit d968af2

Browse files
committed
fix #3511: @__NO_SIDE_EFFECTS__ with templates
1 parent 00c4ebe commit d968af2

File tree

6 files changed

+81
-1
lines changed

6 files changed

+81
-1
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,28 @@
103103
104104
With this release, esbuild will now attempt to terminate the Go GC in this edge case by calling `clearTimeout()` on these pending timeouts.
105105
106+
* Apply `/* @__NO_SIDE_EFFECTS__ */` on tagged template literals ([#3511](https://github.com/evanw/esbuild/issues/3511))
107+
108+
Tagged template literals that reference functions annotated with a `@__NO_SIDE_EFFECTS__` comment are now able to be removed via tree-shaking if the result is unused. This is a convention from [Rollup](https://github.com/rollup/rollup/pull/5024). Here is an example:
109+
110+
```js
111+
// Original code
112+
const html = /* @__NO_SIDE_EFFECTS__ */ (a, ...b) => ({ a, b })
113+
html`<a>remove</a>`
114+
x = html`<b>keep</b>`
115+
116+
// Old output (with --tree-shaking=true)
117+
const html = /* @__NO_SIDE_EFFECTS__ */ (a, ...b) => ({ a, b });
118+
html`<a>remove</a>`;
119+
x = html`<b>keep</b>`;
120+
121+
// New output (with --tree-shaking=true)
122+
const html = /* @__NO_SIDE_EFFECTS__ */ (a, ...b) => ({ a, b });
123+
x = html`<b>keep</b>`;
124+
```
125+
126+
Note that this feature currently only works within a single file, so it's not especially useful. This feature does not yet work across separate files. I still recommend using `@__PURE__` annotations instead of this feature, as they have wider tooling support. The drawback of course is that `@__PURE__` annotations need to be added at each call site, not at the declaration, and for non-call expressions such as template literals you need to wrap the expression in an IIFE (immediately-invoked function expression) to create a call expression to apply the `@__PURE__` annotation to.
127+
106128
* Publish builds for IBM AIX PowerPC 64-bit ([#3549](https://github.com/evanw/esbuild/issues/3549))
107129
108130
This release publishes a binary executable to npm for IBM AIX PowerPC 64-bit, which means that in theory esbuild can now be installed in that environment with `npm install esbuild`. This hasn't actually been tested yet. If you have access to such a system, it would be helpful to confirm whether or not doing this actually works.

internal/bundler_tests/bundler_dce_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,31 @@ func TestRemoveUnusedPureCommentCalls(t *testing.T) {
12221222
})
12231223
}
12241224

1225+
func TestRemoveUnusedNoSideEffectsTaggedTemplates(t *testing.T) {
1226+
dce_suite.expectBundled(t, bundled{
1227+
files: map[string]string{
1228+
"/entry.js": `
1229+
// @__NO_SIDE_EFFECTS__
1230+
function foo() {}
1231+
1232+
foo` + "`remove`" + `;
1233+
foo` + "`remove${null}`" + `;
1234+
foo` + "`remove${123}`" + `;
1235+
1236+
use(foo` + "`keep`" + `);
1237+
foo` + "`remove this part ${keep} and this ${alsoKeep}`" + `;
1238+
` + "`remove this part ${keep} and this ${alsoKeep}`" + `;
1239+
`,
1240+
},
1241+
entryPaths: []string{"/entry.js"},
1242+
options: config.Options{
1243+
Mode: config.ModeBundle,
1244+
AbsOutputFile: "/out.js",
1245+
MinifySyntax: true,
1246+
},
1247+
})
1248+
}
1249+
12251250
func TestTreeShakingReactElements(t *testing.T) {
12261251
dce_suite.expectBundled(t, bundled{
12271252
files: map[string]string{

internal/bundler_tests/snapshots/snapshots_dce.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2456,6 +2456,17 @@ TestRemoveUnusedImportsEvalTS
24562456
---------- /out.js ----------
24572457
eval("foo(a, b, c)");
24582458

2459+
================================================================================
2460+
TestRemoveUnusedNoSideEffectsTaggedTemplates
2461+
---------- /out.js ----------
2462+
// entry.js
2463+
// @__NO_SIDE_EFFECTS__
2464+
function foo() {
2465+
}
2466+
use(foo`keep`);
2467+
keep, alsoKeep;
2468+
`${keep}${alsoKeep}`;
2469+
24592470
================================================================================
24602471
TestRemoveUnusedPureCommentCalls
24612472
---------- /out.js ----------

internal/js_ast/js_ast.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,13 @@ type ETemplate struct {
799799
HeadLoc logger.Loc
800800
LegacyOctalLoc logger.Loc
801801

802+
// True if this is a tagged template literal with a comment that indicates
803+
// this function call can be removed if the result is unused. Note that the
804+
// arguments are not considered to be part of the call. If the call itself
805+
// is removed due to this annotation, the arguments must remain if they have
806+
// side effects (including the string conversions).
807+
CanBeUnwrappedIfUnused bool
808+
802809
// If the tag is present, it is expected to be a function and is called. If
803810
// the tag is a syntactic property access, then the value for "this" in the
804811
// function call is the object whose property was accessed (e.g. in "a.b``"

internal/js_ast/js_ast_helpers.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,16 @@ func (ctx HelperContext) SimplifyUnusedExpr(expr Expr, unsupportedFeatures compa
576576
comma = JoinWithComma(comma, Expr{Loc: templateLoc, Data: template})
577577
}
578578
return comma
579+
} else if e.CanBeUnwrappedIfUnused {
580+
// If the function call was annotated as being able to be removed if the
581+
// result is unused, then we can remove it and just keep the arguments.
582+
// Note that there are no implicit "ToString" operations for tagged
583+
// template literals.
584+
var comma Expr
585+
for _, part := range e.Parts {
586+
comma = JoinWithComma(comma, ctx.SimplifyUnusedExpr(part.Value, unsupportedFeatures))
587+
}
588+
return comma
579589
}
580590

581591
case *EArray:
@@ -2413,7 +2423,7 @@ func (ctx HelperContext) ExprCanBeRemovedIfUnused(expr Expr) bool {
24132423
// A template can be removed if it has no tag and every value has no side
24142424
// effects and results in some kind of primitive, since all primitives
24152425
// have a "ToString" operation with no side effects.
2416-
if e.TagOrNil.Data == nil {
2426+
if e.TagOrNil.Data == nil || e.CanBeUnwrappedIfUnused {
24172427
for _, part := range e.Parts {
24182428
if !ctx.ExprCanBeRemovedIfUnused(part.Value) || KnownPrimitiveType(part.Value.Data) == PrimitiveUnknown {
24192429
return false

internal/js_parser/js_parser.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13237,6 +13237,11 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
1323713237
tagThisFunc = tagOut.thisArgFunc
1323813238
tagWrapFunc = tagOut.thisArgWrapFunc
1323913239

13240+
// Copy the call side effect flag over if this is a known target
13241+
if id, ok := tag.Data.(*js_ast.EIdentifier); ok && p.symbols[id.Ref.InnerIndex].Flags.Has(ast.CallCanBeUnwrappedIfUnused) {
13242+
e.CanBeUnwrappedIfUnused = true
13243+
}
13244+
1324013245
// The value of "this" must be manually preserved for private member
1324113246
// accesses inside template tag expressions such as "this.#foo``".
1324213247
// The private member "this.#foo" must see the value of "this".

0 commit comments

Comments
 (0)