Skip to content

Commit f5f8ff8

Browse files
committed
fix #3568: can reorder primitive past side-effect
1 parent 914f608 commit f5f8ff8

File tree

5 files changed

+72
-40
lines changed

5 files changed

+72
-40
lines changed

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,27 @@
4343
}
4444
```
4545

46+
* Minifier: allow reording a primitive past a side-effect ([#3568](https://github.com/evanw/esbuild/issues/3568))
47+
48+
The minifier previously allowed reordering a side-effect past a primitive, but didn't handle the case of reordering a primitive past a side-effect. This additional case is now handled:
49+
50+
```js
51+
// Original code
52+
function f() {
53+
let x = false;
54+
let y = x;
55+
const boolean = y;
56+
let frag = $.template(`<p contenteditable="${boolean}">hello world</p>`);
57+
return frag;
58+
}
59+
60+
// Old output (with --minify)
61+
function f(){const e=!1;return $.template(`<p contenteditable="${e}">hello world</p>`)}
62+
63+
// New output (with --minify)
64+
function f(){return $.template('<p contenteditable="false">hello world</p>')}
65+
```
66+
4667
* Provide the `stop()` API in node to exit esbuild's child process ([#3558](https://github.com/evanw/esbuild/issues/3558))
4768

4869
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/js_ast/js_ast_helpers.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1696,7 +1696,7 @@ func FoldStringAddition(left Expr, right Expr, kind StringAdditionKind) Expr {
16961696
//
16971697
// This function intentionally avoids mutating the input AST so it can be
16981698
// called after the AST has been frozen (i.e. after parsing ends).
1699-
func InlineStringsAndNumbersIntoTemplate(loc logger.Loc, e *ETemplate) Expr {
1699+
func InlinePrimitivesIntoTemplate(loc logger.Loc, e *ETemplate) Expr {
17001700
// Can't inline strings if there's a custom template tag
17011701
if e.TagOrNil.Data != nil {
17021702
return Expr{Loc: loc, Data: e}
@@ -1709,10 +1709,8 @@ func InlineStringsAndNumbersIntoTemplate(loc logger.Loc, e *ETemplate) Expr {
17091709
if value, ok := part.Value.Data.(*EInlinedEnum); ok {
17101710
part.Value = value.Value
17111711
}
1712-
if value, ok := part.Value.Data.(*ENumber); ok {
1713-
if str, ok := TryToStringOnNumberSafely(value.Value, 10); ok {
1714-
part.Value.Data = &EString{Value: helpers.StringToUTF16(str)}
1715-
}
1712+
if str, ok := ToStringWithoutSideEffects(part.Value.Data); ok {
1713+
part.Value.Data = &EString{Value: helpers.StringToUTF16(str)}
17161714
}
17171715
if str, ok := part.Value.Data.(*EString); ok {
17181716
if len(parts) == 0 {

internal/js_parser/js_parser.go

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8732,34 +8732,40 @@ func (p *parser) mangleStmts(stmts []js_ast.Stmt, kind stmtsKind) []js_ast.Stmt
87328732
// by now since they are scoped to this block which we just finished
87338733
// visiting.
87348734
if prevS, ok := result[len(result)-1].Data.(*js_ast.SLocal); ok && prevS.Kind != js_ast.LocalVar {
8735-
// The variable must be initialized, since we will be substituting
8736-
// the value into the usage.
8737-
if last := prevS.Decls[len(prevS.Decls)-1]; last.ValueOrNil.Data != nil {
8738-
// The binding must be an identifier that is only used once.
8739-
// Ignore destructuring bindings since that's not the simple case.
8740-
// Destructuring bindings could potentially execute side-effecting
8741-
// code which would invalidate reordering.
8742-
if id, ok := last.Binding.Data.(*js_ast.BIdentifier); ok {
8743-
// Don't do this if "__name" was called on this symbol. In that
8744-
// case there is actually more than one use even though it says
8745-
// there is only one. The "__name" use isn't counted so that
8746-
// tree shaking still works when names are kept.
8747-
if symbol := p.symbols[id.Ref.InnerIndex]; symbol.UseCountEstimate == 1 && !symbol.Flags.Has(ast.DidKeepName) {
8748-
// Try to substitute the identifier with the initializer. This will
8749-
// fail if something with side effects is in between the declaration
8750-
// and the usage.
8751-
if p.substituteSingleUseSymbolInStmt(stmt, id.Ref, last.ValueOrNil) {
8752-
// Remove the previous declaration, since the substitution was
8753-
// successful.
8754-
if len(prevS.Decls) == 1 {
8755-
result = result[:len(result)-1]
8756-
} else {
8757-
prevS.Decls = prevS.Decls[:len(prevS.Decls)-1]
8758-
}
8735+
last := prevS.Decls[len(prevS.Decls)-1]
8736+
8737+
// The binding must be an identifier that is only used once.
8738+
// Ignore destructuring bindings since that's not the simple case.
8739+
// Destructuring bindings could potentially execute side-effecting
8740+
// code which would invalidate reordering.
8741+
if id, ok := last.Binding.Data.(*js_ast.BIdentifier); ok {
8742+
// Don't do this if "__name" was called on this symbol. In that
8743+
// case there is actually more than one use even though it says
8744+
// there is only one. The "__name" use isn't counted so that
8745+
// tree shaking still works when names are kept.
8746+
if symbol := p.symbols[id.Ref.InnerIndex]; symbol.UseCountEstimate == 1 && !symbol.Flags.Has(ast.DidKeepName) {
8747+
replacement := last.ValueOrNil
8748+
8749+
// The variable must be initialized, since we will be substituting
8750+
// the value into the usage.
8751+
if replacement.Data == nil {
8752+
replacement = js_ast.Expr{Loc: last.Binding.Loc, Data: js_ast.EUndefinedShared}
8753+
}
87598754

8760-
// Loop back to try again
8761-
continue
8755+
// Try to substitute the identifier with the initializer. This will
8756+
// fail if something with side effects is in between the declaration
8757+
// and the usage.
8758+
if p.substituteSingleUseSymbolInStmt(stmt, id.Ref, replacement) {
8759+
// Remove the previous declaration, since the substitution was
8760+
// successful.
8761+
if len(prevS.Decls) == 1 {
8762+
result = result[:len(result)-1]
8763+
} else {
8764+
prevS.Decls = prevS.Decls[:len(prevS.Decls)-1]
87628765
}
8766+
8767+
// Loop back to try again
8768+
continue
87638769
}
87648770
}
87658771
}
@@ -9437,10 +9443,9 @@ func (p *parser) substituteSingleUseSymbolInExpr(
94379443
if value, status := p.substituteSingleUseSymbolInExpr(part.Value, ref, replacement, replacementCanBeRemoved); status != substituteContinue {
94389444
e.Parts[i].Value = value
94399445

9440-
// If we substituted a string or number, merge it into the template
9441-
switch value.Data.(type) {
9442-
case *js_ast.EString, *js_ast.ENumber:
9443-
expr = js_ast.InlineStringsAndNumbersIntoTemplate(expr.Loc, e)
9446+
// If we substituted a primitive, merge it into the template
9447+
if js_ast.IsPrimitiveLiteral(value.Data) {
9448+
expr = js_ast.InlinePrimitivesIntoTemplate(expr.Loc, e)
94449449
}
94459450
return expr, status
94469451
}
@@ -9454,7 +9459,7 @@ func (p *parser) substituteSingleUseSymbolInExpr(
94549459
}
94559460

94569461
// We can always reorder past primitive values
9457-
if js_ast.IsPrimitiveLiteral(expr.Data) {
9462+
if js_ast.IsPrimitiveLiteral(expr.Data) || js_ast.IsPrimitiveLiteral(replacement.Data) {
94589463
return expr, substituteContinue
94599464
}
94609465

@@ -13268,7 +13273,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
1326813273
// it may no longer be a template literal after this point (it may turn into
1326913274
// a plain string literal instead).
1327013275
if p.shouldFoldTypeScriptConstantExpressions || p.options.minifySyntax {
13271-
expr = js_ast.InlineStringsAndNumbersIntoTemplate(expr.Loc, e)
13276+
expr = js_ast.InlinePrimitivesIntoTemplate(expr.Loc, e)
1327213277
}
1327313278

1327413279
shouldLowerTemplateLiteral := p.options.unsupportedJSFeatures.Has(compat.TemplateLiteral)

internal/js_parser/js_parser_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4974,15 +4974,23 @@ func TestMangleInlineLocals(t *testing.T) {
49744974
check("let x = 1; return void x", "let x = 1;")
49754975
check("let x = 1; return typeof x", "return typeof 1;")
49764976

4977+
// Can substitute into template literals
4978+
check("let x = 1; return `<${x}>`", "return `<1>`;")
4979+
check("let x = 1n; return `<${x}>`", "return `<1>`;")
4980+
check("let x = null; return `<${x}>`", "return `<null>`;")
4981+
check("let x = undefined; return `<${x}>`", "return `<undefined>`;")
4982+
check("let x = false; return `<${x}>`", "return `<false>`;")
4983+
check("let x = true; return `<${x}>`", "return `<true>`;")
4984+
49774985
// Check substituting a side-effect free value into normal binary operators
49784986
check("let x = 1; return x + 2", "return 1 + 2;")
49794987
check("let x = 1; return 2 + x", "return 2 + 1;")
49804988
check("let x = 1; return x + arg0", "return 1 + arg0;")
49814989
check("let x = 1; return arg0 + x", "return arg0 + 1;")
49824990
check("let x = 1; return x + fn()", "return 1 + fn();")
4983-
check("let x = 1; return fn() + x", "let x = 1;\nreturn fn() + x;")
4991+
check("let x = 1; return fn() + x", "return fn() + 1;")
49844992
check("let x = 1; return x + undef", "return 1 + undef;")
4985-
check("let x = 1; return undef + x", "let x = 1;\nreturn undef + x;")
4993+
check("let x = 1; return undef + x", "return undef + 1;")
49864994

49874995
// Check substituting a value with side-effects into normal binary operators
49884996
check("let x = fn(); return x + 2", "return fn() + 2;")

internal/js_printer/js_printer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2999,7 +2999,7 @@ func (p *printer) printExpr(expr js_ast.Expr, level js_ast.L, flags printExprFla
29992999
if replaced != nil {
30003000
copy := *e
30013001
copy.Parts = replaced
3002-
switch e2 := js_ast.InlineStringsAndNumbersIntoTemplate(logger.Loc{}, &copy).Data.(type) {
3002+
switch e2 := js_ast.InlinePrimitivesIntoTemplate(logger.Loc{}, &copy).Data.(type) {
30033003
case *js_ast.EString:
30043004
p.printQuotedUTF16(e2.Value, printQuotedAllowBacktick)
30053005
return

0 commit comments

Comments
 (0)