Skip to content

Commit 49c229f

Browse files
committed
fix #2407: undo recursive --define substitution
1 parent fd13718 commit 49c229f

File tree

4 files changed

+81
-14
lines changed

4 files changed

+81
-14
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
* Fix an accidental infinite loop with `--define` substitution ([#2407](https://github.com/evanw/esbuild/issues/2407))
6+
7+
This is a fix for a regression that was introduced in esbuild version 0.14.44 where certain `--define` substitutions could result in esbuild crashing with a stack overflow. The problem was an incorrect fix for #2292. The fix merged the code paths for `--define` and `--jsx-factory` rewriting since the value substitution is now the same for both. However, doing this accidentally made `--define` substitution recursive since the JSX factory needs to be able to match against `--define` substitutions to integrate with the `--inject` feature. The fix is to only do one additional level of matching against define substitutions, and to only do this for JSX factories. Now these cases are able to build successfully without a stack overflow.
8+
59
* Fix a cross-platform consistency bug ([#2383](https://github.com/evanw/esbuild/issues/2383))
610

711
Previously esbuild would minify `0xFFFF_FFFF_FFFF_FFFF` as `0xffffffffffffffff` (18 bytes) on arm64 chips and as `18446744073709552e3` (19 bytes) on x86_64 chips. The reason was that the number was converted to a 64-bit unsigned integer internally for printing as hexadecimal, the 64-bit floating-point number `0xFFFF_FFFF_FFFF_FFFF` is actually `0x1_0000_0000_0000_0180` (i.e. it's rounded up, not down), and converting `float64` to `uint64` is implementation-dependent in Go when the input is out of bounds. This was fixed by changing the upper limit for which esbuild uses hexadecimal numbers during minification to `0xFFFF_FFFF_FFFF_F800`, which is the next representable 64-bit floating-point number below `0x1_0000_0000_0000_0180`, and which fits in a `uint64`. As a result, esbuild will now consistently never minify `0xFFFF_FFFF_FFFF_FFFF` as `0xffffffffffffffff` anymore, which means the output should now be consistent across platforms.

internal/bundler/bundler_default_test.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4172,14 +4172,20 @@ func TestInjectJSX(t *testing.T) {
41724172
Parts: []string{"el"},
41734173
},
41744174
},
4175+
"React.Fragment": {
4176+
DefineExpr: &config.DefineExpr{
4177+
Parts: []string{"frag"},
4178+
},
4179+
},
41754180
})
41764181
default_suite.expectBundled(t, bundled{
41774182
files: map[string]string{
41784183
"/entry.jsx": `
4179-
console.log(<div/>)
4184+
console.log(<><div/></>)
41804185
`,
41814186
"/inject.js": `
41824187
export function el() {}
4188+
export function frag() {}
41834189
`,
41844190
},
41854191
entryPaths: []string{"/entry.jsx"},
@@ -4559,6 +4565,46 @@ func TestDefineOptionalChainLowered(t *testing.T) {
45594565
})
45604566
}
45614567

4568+
// See: https://github.com/evanw/esbuild/issues/2407
4569+
func TestDefineInfiniteLoopIssue2407(t *testing.T) {
4570+
defines := config.ProcessDefines(map[string]config.DefineData{
4571+
"a.b": {
4572+
DefineExpr: &config.DefineExpr{
4573+
Parts: []string{"b", "c"},
4574+
},
4575+
},
4576+
"b.c": {
4577+
DefineExpr: &config.DefineExpr{
4578+
Parts: []string{"c", "a"},
4579+
},
4580+
},
4581+
"c.a": {
4582+
DefineExpr: &config.DefineExpr{
4583+
Parts: []string{"a", "b"},
4584+
},
4585+
},
4586+
"x.y": {
4587+
DefineExpr: &config.DefineExpr{
4588+
Parts: []string{"y"},
4589+
},
4590+
},
4591+
})
4592+
default_suite.expectBundled(t, bundled{
4593+
files: map[string]string{
4594+
"/entry.js": `
4595+
a.b()
4596+
x.y()
4597+
`,
4598+
},
4599+
entryPaths: []string{"/entry.js"},
4600+
options: config.Options{
4601+
Mode: config.ModeBundle,
4602+
AbsOutputFile: "/out.js",
4603+
Defines: &defines,
4604+
},
4605+
})
4606+
}
4607+
45624608
func TestKeepNamesTreeShaking(t *testing.T) {
45634609
default_suite.expectBundled(t, bundled{
45644610
files: map[string]string{

internal/bundler/snapshots/snapshots_default.txt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,13 @@ console.log(import_meta.y);
420420

421421
---------- /out/dead-code.js ----------
422422

423+
================================================================================
424+
TestDefineInfiniteLoopIssue2407
425+
---------- /out.js ----------
426+
// entry.js
427+
b.c();
428+
y();
429+
423430
================================================================================
424431
TestDefineOptionalChain
425432
---------- /out.js ----------
@@ -1376,9 +1383,11 @@ TestInjectJSX
13761383
// inject.js
13771384
function el() {
13781385
}
1386+
function frag() {
1387+
}
13791388

13801389
// entry.jsx
1381-
console.log(/* @__PURE__ */ el("div", null));
1390+
console.log(/* @__PURE__ */ el(frag, null, /* @__PURE__ */ el("div", null)));
13821391

13831392
================================================================================
13841393
TestInjectNoBundle

internal/js_parser/js_parser.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10754,20 +10754,25 @@ func (p *parser) instantiateDefineExpr(loc logger.Loc, expr config.DefineExpr, o
1075410754
}
1075510755

1075610756
// Check both user-specified defines and known globals
10757-
if defines, ok := p.options.defines.DotDefines[parts[len(parts)-1]]; ok {
10758-
next:
10759-
for _, define := range defines {
10760-
if len(define.Parts) == len(parts) {
10761-
for i := range parts {
10762-
if parts[i] != define.Parts[i] {
10763-
continue next
10757+
if opts.matchAgainstDefines {
10758+
// Make sure define resolution is not recursive
10759+
opts.matchAgainstDefines = false
10760+
10761+
if defines, ok := p.options.defines.DotDefines[parts[len(parts)-1]]; ok {
10762+
next:
10763+
for _, define := range defines {
10764+
if len(define.Parts) == len(parts) {
10765+
for i := range parts {
10766+
if parts[i] != define.Parts[i] {
10767+
continue next
10768+
}
1076410769
}
10765-
}
10766-
}
1076710770

10768-
// Substitute user-specified defines
10769-
if define.Data.DefineExpr != nil {
10770-
return p.instantiateDefineExpr(loc, *define.Data.DefineExpr, opts)
10771+
// Substitute user-specified defines
10772+
if define.Data.DefineExpr != nil {
10773+
return p.instantiateDefineExpr(loc, *define.Data.DefineExpr, opts)
10774+
}
10775+
}
1077110776
}
1077210777
}
1077310778
}
@@ -12060,6 +12065,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
1206012065
if e.TagOrNil.Data == nil {
1206112066
e.TagOrNil = p.instantiateDefineExpr(expr.Loc, p.options.jsx.Fragment, identifierOpts{
1206212067
wasOriginallyIdentifier: true,
12068+
matchAgainstDefines: true, // Allow defines to rewrite the JSX fragment factory
1206312069
})
1206412070
}
1206512071

@@ -12079,6 +12085,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
1207912085
// Call createElement()
1208012086
target := p.instantiateDefineExpr(expr.Loc, p.options.jsx.Factory, identifierOpts{
1208112087
wasOriginallyIdentifier: true,
12088+
matchAgainstDefines: true, // Allow defines to rewrite the JSX factory
1208212089
})
1208312090
p.warnAboutImportNamespaceCall(target, exprKindCall)
1208412091
return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{
@@ -14280,6 +14287,7 @@ type identifierOpts struct {
1428014287
isDeleteTarget bool
1428114288
preferQuotedKey bool
1428214289
wasOriginallyIdentifier bool
14290+
matchAgainstDefines bool
1428314291
}
1428414292

1428514293
func (p *parser) handleIdentifier(loc logger.Loc, e *js_ast.EIdentifier, opts identifierOpts) js_ast.Expr {

0 commit comments

Comments
 (0)