Skip to content

Commit bdee212

Browse files
committed
fix #2158: private+super+static+bundle edge case
1 parent 9ce27c8 commit bdee212

File tree

5 files changed

+127
-0
lines changed

5 files changed

+127
-0
lines changed

CHANGELOG.md

+59
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,65 @@
22

33
## Unreleased
44

5+
* Fix a regression regarding `super` ([#2158](https://github.com/evanw/esbuild/issues/2158))
6+
7+
This fixes a regression from the previous release regarding classes with a super class, a private member, and a static field in the scenario where the static field needs to be lowered but where private members are supported by the configured target environment. In this scenario, esbuild could incorrectly inject the instance field initializers that use `this` into the constructor before the call to `super()`, which is invalid. This problem has now been fixed (notice that `this` is now used after `super()` instead of before):
8+
9+
```js
10+
// Original code
11+
class Foo extends Object {
12+
static FOO;
13+
constructor() {
14+
super();
15+
}
16+
#foo;
17+
}
18+
19+
// Old output (with --bundle)
20+
var _foo;
21+
var Foo = class extends Object {
22+
constructor() {
23+
__privateAdd(this, _foo, void 0);
24+
super();
25+
}
26+
};
27+
_foo = new WeakMap();
28+
__publicField(Foo, "FOO");
29+
30+
// New output (with --bundle)
31+
var _foo;
32+
var Foo = class extends Object {
33+
constructor() {
34+
super();
35+
__privateAdd(this, _foo, void 0);
36+
}
37+
};
38+
_foo = new WeakMap();
39+
__publicField(Foo, "FOO");
40+
```
41+
42+
During parsing, esbuild scans the class and makes certain decisions about the class such as whether to lower all static fields, whether to lower each private member, or whether calls to `super()` need to be tracked and adjusted. Previously esbuild made two passes through the class members to compute this information. However, with the new `super()` call lowering logic added in the previous release, we now need three passes to capture the whole dependency chain for this case: 1) lowering static fields requires 2) lowering private members which requires 3) adjusting `super()` calls.
43+
44+
The reason lowering static fields requires lowering private members is because lowering static fields moves their initializers outside of the class body, where they can't access private members anymore. Consider this code:
45+
46+
```js
47+
class Foo {
48+
get #foo() {}
49+
static bar = new Foo().#foo
50+
}
51+
```
52+
53+
We can't just lower static fields without also lowering private members, since that causes a syntax error:
54+
55+
```js
56+
class Foo {
57+
get #foo() {}
58+
}
59+
Foo.bar = new Foo().#foo;
60+
```
61+
62+
And the reason lowering private members requires adjusting `super()` calls is because the injected private member initializers use `this`, which is only accessible after `super()` calls in the constructor.
63+
564
* Add Linux ARM64 support for Deno ([#2156](https://github.com/evanw/esbuild/issues/2156))
665

766
This release adds Linux ARM64 support to esbuild's [Deno](https://deno.land/) API implementation, which allows esbuild to be used with Deno on a Raspberry Pi.

internal/bundler/bundler_lower_test.go

+22
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,28 @@ func TestLowerPrivateSuperES2021(t *testing.T) {
15321532
})
15331533
}
15341534

1535+
// https://github.com/evanw/esbuild/issues/2158
1536+
func TestLowerPrivateSuperStaticBundleIssue2158(t *testing.T) {
1537+
lower_suite.expectBundled(t, bundled{
1538+
files: map[string]string{
1539+
"/entry.js": `
1540+
export class Foo extends Object {
1541+
static FOO;
1542+
constructor() {
1543+
super();
1544+
}
1545+
#foo;
1546+
}
1547+
`,
1548+
},
1549+
entryPaths: []string{"/entry.js"},
1550+
options: config.Options{
1551+
Mode: config.ModeBundle,
1552+
AbsOutputFile: "/out.js",
1553+
},
1554+
})
1555+
}
1556+
15351557
func TestLowerClassField2020NoBundle(t *testing.T) {
15361558
lower_suite.expectBundled(t, bundled{
15371559
files: map[string]string{

internal/bundler/snapshots/snapshots_lower.txt

+17
Original file line numberDiff line numberDiff line change
@@ -1633,6 +1633,23 @@ export {
16331633
foo8_default as foo8
16341634
};
16351635

1636+
================================================================================
1637+
TestLowerPrivateSuperStaticBundleIssue2158
1638+
---------- /out.js ----------
1639+
// entry.js
1640+
var _foo;
1641+
var Foo = class extends Object {
1642+
constructor() {
1643+
super();
1644+
__privateAdd(this, _foo, void 0);
1645+
}
1646+
};
1647+
_foo = new WeakMap();
1648+
__publicField(Foo, "FOO");
1649+
export {
1650+
Foo
1651+
};
1652+
16361653
================================================================================
16371654
TestLowerStaticAsyncArrowSuperES2016
16381655
---------- /out.js ----------

internal/js_parser/js_parser.go

+9
Original file line numberDiff line numberDiff line change
@@ -10158,6 +10158,7 @@ func (p *parser) visitClass(nameScopeLoc logger.Loc, class *js_ast.Class, defaul
1015810158
// field initializers outside of the class body and "this" will no longer
1015910159
// reference the same thing.
1016010160
classLoweringInfo := p.computeClassLoweringInfo(class)
10161+
recomputeClassLoweringInfo := false
1016110162

1016210163
// Sometimes we need to lower private members even though they are supported.
1016310164
// This flags them for lowering so that we lower references to them as we
@@ -10186,6 +10187,7 @@ func (p *parser) visitClass(nameScopeLoc logger.Loc, class *js_ast.Class, defaul
1018610187
// The private getter must be lowered too.
1018710188
if private, ok := prop.Key.Data.(*js_ast.EPrivateIdentifier); ok {
1018810189
p.symbols[private.Ref.InnerIndex].Flags |= js_ast.PrivateSymbolMustBeLowered
10190+
recomputeClassLoweringInfo = true
1018910191
}
1019010192
}
1019110193
}
@@ -10197,11 +10199,18 @@ func (p *parser) visitClass(nameScopeLoc logger.Loc, class *js_ast.Class, defaul
1019710199
if private, ok := prop.Key.Data.(*js_ast.EPrivateIdentifier); ok {
1019810200
if symbol := &p.symbols[private.Ref.InnerIndex]; p.classPrivateBrandChecksToLower[symbol.OriginalName] {
1019910201
symbol.Flags |= js_ast.PrivateSymbolMustBeLowered
10202+
recomputeClassLoweringInfo = true
1020010203
}
1020110204
}
1020210205
}
1020310206
}
1020410207

10208+
// If we changed private symbol lowering decisions, then recompute class
10209+
// lowering info because that may have changed other decisions too
10210+
if recomputeClassLoweringInfo {
10211+
classLoweringInfo = p.computeClassLoweringInfo(class)
10212+
}
10213+
1020510214
p.pushScopeForVisitPass(js_ast.ScopeClassName, nameScopeLoc)
1020610215
oldEnclosingClassKeyword := p.enclosingClassKeyword
1020710216
p.enclosingClassKeyword = class.ClassKeyword

scripts/end-to-end-tests.js

+20
Original file line numberDiff line numberDiff line change
@@ -4923,6 +4923,26 @@
49234923
}
49244924
`,
49254925
}, { async: true }),
4926+
test(['in.js', '--outfile=node.js', '--bundle'].concat(flags), {
4927+
// Check edge case in https://github.com/evanw/esbuild/issues/2158
4928+
'in.js': `
4929+
class Foo {
4930+
constructor(x) {
4931+
this.base = x
4932+
}
4933+
}
4934+
class Bar extends Foo {
4935+
static FOO = 1
4936+
constructor() {
4937+
super(2)
4938+
this.derived = this.#foo + Bar.FOO
4939+
}
4940+
#foo = 3
4941+
}
4942+
let bar = new Bar
4943+
if (bar.base !== 2 || bar.derived !== 4) throw 'fail'
4944+
`,
4945+
}),
49264946
test(['in.js', '--outfile=node.js', '--keep-names', '--bundle'].concat(flags), {
49274947
// Check default export name preservation with lowered "super" inside lowered "async"
49284948
'in.js': `

0 commit comments

Comments
 (0)