Skip to content

Commit 2aa166b

Browse files
committed
fix #3559: fix recent class transform regression
1 parent 55e1127 commit 2aa166b

4 files changed

Lines changed: 171 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,48 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
* Fix TypeScript-specific class transform edge case ([#3559](https://github.com/evanw/esbuild/issues/3559))
6+
7+
The previous release introduced an optimization that avoided transforming `super()` in the class constructor for TypeScript code compiled with `useDefineForClassFields` set to `false` if all class instance fields have no initializers. The rationale was that in this case, all class instance fields are omitted in the output so no changes to the constructor are needed. However, if all of this is the case _and_ there are `#private` instance fields with initializers, those private instance field initializers were still being moved into the constructor. This was problematic because they were being inserted before the call to `super()` (since `super()` is now no longer transformed in that case). This release introduces an additional optimization that avoids moving the private instance field initializers into the constructor in this edge case, which generates smaller code, matches the TypeScript compiler's output more closely, and avoids this bug:
8+
9+
```ts
10+
// Original code
11+
class Foo extends Bar {
12+
#private = 1;
13+
public: any;
14+
constructor() {
15+
super();
16+
}
17+
}
18+
19+
// Old output (with esbuild v0.19.9)
20+
class Foo extends Bar {
21+
constructor() {
22+
super();
23+
this.#private = 1;
24+
}
25+
#private;
26+
}
27+
28+
// Old output (with esbuild v0.19.10)
29+
class Foo extends Bar {
30+
constructor() {
31+
this.#private = 1;
32+
super();
33+
}
34+
#private;
35+
}
36+
37+
// New output
38+
class Foo extends Bar {
39+
#private = 1;
40+
constructor() {
41+
super();
42+
}
43+
}
44+
```
45+
346
## 0.19.10
447

548
* Fix glob imports in TypeScript files ([#3319](https://github.com/evanw/esbuild/issues/3319))

internal/js_parser/js_parser_lower_class.go

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -376,16 +376,8 @@ func (p *parser) maybeLowerSuperPropertyGetInsideCall(call *js_ast.ECall) {
376376
call.Args = append([]js_ast.Expr{thisExpr}, call.Args...)
377377
}
378378

379-
type lowerAllInstanceFields uint8
380-
381-
const (
382-
lowerAllInstanceFields_false lowerAllInstanceFields = iota
383-
lowerAllInstanceFields_true_skipSuperCallShim
384-
lowerAllInstanceFields_true_needSuperCallShim
385-
)
386-
387379
type classLoweringInfo struct {
388-
lowerAllInstanceFields lowerAllInstanceFields
380+
lowerAllInstanceFields bool
389381
lowerAllStaticFields bool
390382
shimSuperCtorCalls bool
391383
}
@@ -464,7 +456,7 @@ func (p *parser) computeClassLoweringInfo(class *js_ast.Class) (result classLowe
464456
}
465457
} else {
466458
if p.privateSymbolNeedsToBeLowered(private) {
467-
result.lowerAllInstanceFields = lowerAllInstanceFields_true_needSuperCallShim
459+
result.lowerAllInstanceFields = true
468460

469461
// We can't transform this:
470462
//
@@ -503,7 +495,7 @@ func (p *parser) computeClassLoweringInfo(class *js_ast.Class) (result classLowe
503495
}
504496
} else {
505497
if p.options.unsupportedJSFeatures.Has(compat.ClassPrivateField) {
506-
result.lowerAllInstanceFields = lowerAllInstanceFields_true_needSuperCallShim
498+
result.lowerAllInstanceFields = true
507499
result.lowerAllStaticFields = true
508500
}
509501
}
@@ -576,24 +568,22 @@ func (p *parser) computeClassLoweringInfo(class *js_ast.Class) (result classLowe
576568
// fields because there's no way for this to call a setter in the base
577569
// class, so this isn't done for private fields.
578570
if prop.InitializerOrNil.Data != nil {
579-
result.lowerAllInstanceFields = lowerAllInstanceFields_true_needSuperCallShim
580-
} else if result.lowerAllInstanceFields != lowerAllInstanceFields_true_needSuperCallShim {
581-
// We can skip the "super()" call shim if all instance fields
571+
// We can skip lowering all instance fields if all instance fields
582572
// disappear completely when lowered. This happens when
583573
// "useDefineForClassFields" is false and there is no initializer.
584-
result.lowerAllInstanceFields = lowerAllInstanceFields_true_skipSuperCallShim
574+
result.lowerAllInstanceFields = true
585575
}
586576
} else if p.options.unsupportedJSFeatures.Has(compat.ClassField) {
587577
// Instance fields must be lowered if the target doesn't support them
588-
result.lowerAllInstanceFields = lowerAllInstanceFields_true_needSuperCallShim
578+
result.lowerAllInstanceFields = true
589579
}
590580
}
591581
}
592582

593583
// We need to shim "super()" inside the constructor if this is a derived
594584
// class and there are any instance fields that need to be lowered, since
595585
// those use "this" and we can only access "this" after "super()" is called
596-
if result.lowerAllInstanceFields == lowerAllInstanceFields_true_needSuperCallShim && class.ExtendsOrNil.Data != nil {
586+
if result.lowerAllInstanceFields && class.ExtendsOrNil.Data != nil {
597587
result.shimSuperCtorCalls = true
598588
}
599589

@@ -1058,8 +1048,17 @@ func (p *parser) lowerClass(stmt js_ast.Stmt, expr js_ast.Expr, result visitClas
10581048
if !prop.Flags.Has(js_ast.PropertyIsMethod) {
10591049
if prop.Flags.Has(js_ast.PropertyIsStatic) {
10601050
mustLowerField = classLoweringInfo.lowerAllStaticFields
1051+
} else if prop.Kind == js_ast.PropertyNormal && p.options.ts.Parse && !class.UseDefineForClassFields && private == nil {
1052+
// Lower non-private instance fields (not accessors) if TypeScript's
1053+
// "useDefineForClassFields" setting is disabled. When all such fields
1054+
// have no initializers, we avoid setting the "lowerAllInstanceFields"
1055+
// flag as an optimization because we can just remove all class field
1056+
// declarations in that case without messing with the constructor. But
1057+
// we must set the "mustLowerField" flag here to cause this class field
1058+
// declaration to still be removed.
1059+
mustLowerField = true
10611060
} else {
1062-
mustLowerField = classLoweringInfo.lowerAllInstanceFields != lowerAllInstanceFields_false
1061+
mustLowerField = classLoweringInfo.lowerAllInstanceFields
10631062
}
10641063
}
10651064

internal/js_parser/ts_parser_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2418,6 +2418,45 @@ class A extends B {
24182418
__super(2);
24192419
}
24202420
}
2421+
`)
2422+
2423+
expectPrintedAssignSemanticsTS(t, "class A extends B { #x; y; constructor() { super() } }",
2424+
`class A extends B {
2425+
#x;
2426+
constructor() {
2427+
super();
2428+
}
2429+
}
2430+
`)
2431+
2432+
expectPrintedAssignSemanticsTS(t, "class A extends B { #x = 1; y; constructor() { super() } }",
2433+
`class A extends B {
2434+
#x = 1;
2435+
constructor() {
2436+
super();
2437+
}
2438+
}
2439+
`)
2440+
2441+
expectPrintedAssignSemanticsTS(t, "class A extends B { #x; y = 1; constructor() { super() } }",
2442+
`class A extends B {
2443+
constructor() {
2444+
super();
2445+
this.y = 1;
2446+
}
2447+
#x;
2448+
}
2449+
`)
2450+
2451+
expectPrintedAssignSemanticsTS(t, "class A extends B { #x = 1; y = 2; constructor() { super() } }",
2452+
`class A extends B {
2453+
constructor() {
2454+
super();
2455+
this.#x = 1;
2456+
this.y = 2;
2457+
}
2458+
#x;
2459+
}
24212460
`)
24222461
}
24232462

scripts/end-to-end-tests.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5622,6 +5622,78 @@ for (let flags of [['--target=es2022'], ['--target=es6'], ['--bundle', '--target
56225622
},
56235623
}`,
56245624
}),
5625+
5626+
// https://github.com/evanw/esbuild/issues/3559
5627+
test(['in.ts', '--outfile=node.js'].concat(flags), {
5628+
'in.ts': `
5629+
class Foo extends Array {
5630+
#private: any
5631+
pass: any
5632+
constructor() {
5633+
super()
5634+
this.pass = true
5635+
}
5636+
}
5637+
if (!new Foo().pass) throw 'fail'
5638+
`,
5639+
'tsconfig.json': `{
5640+
"compilerOptions": {
5641+
"useDefineForClassFields": false,
5642+
},
5643+
}`,
5644+
}),
5645+
test(['in.ts', '--outfile=node.js'].concat(flags), {
5646+
'in.ts': `
5647+
class Foo extends Array {
5648+
#private: any
5649+
pass = true
5650+
constructor() {
5651+
super()
5652+
}
5653+
}
5654+
if (!new Foo().pass) throw 'fail'
5655+
`,
5656+
'tsconfig.json': `{
5657+
"compilerOptions": {
5658+
"useDefineForClassFields": false,
5659+
},
5660+
}`,
5661+
}),
5662+
test(['in.ts', '--outfile=node.js'].concat(flags), {
5663+
'in.ts': `
5664+
class Foo extends Array {
5665+
#private = 123
5666+
pass: any
5667+
constructor() {
5668+
super()
5669+
this.pass = true
5670+
}
5671+
}
5672+
if (!new Foo().pass) throw 'fail'
5673+
`,
5674+
'tsconfig.json': `{
5675+
"compilerOptions": {
5676+
"useDefineForClassFields": false,
5677+
},
5678+
}`,
5679+
}),
5680+
test(['in.ts', '--outfile=node.js'].concat(flags), {
5681+
'in.ts': `
5682+
class Foo extends Array {
5683+
#private = 123
5684+
pass: any = true
5685+
constructor() {
5686+
super()
5687+
}
5688+
}
5689+
if (!new Foo().pass) throw 'fail'
5690+
`,
5691+
'tsconfig.json': `{
5692+
"compilerOptions": {
5693+
"useDefineForClassFields": false,
5694+
},
5695+
}`,
5696+
}),
56255697
)
56265698

56275699
// https://github.com/evanw/esbuild/issues/3177

0 commit comments

Comments
 (0)