Skip to content

Commit

Permalink
fix #885: "this" in class static fields
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Feb 25, 2021
1 parent ffc269b commit 5afc5bf
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 13 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## Unreleased

* Handle `this` in class static field initializers ([#885](https://github.com/evanw/esbuild/issues/885))

When you use `this` in a static field initializer inside a `class` statement or expression, it references the class object itself:

```js
class Foo {
static Bar = class extends this {
}
}
assert(new Foo.Bar() instanceof Foo)
```

This case previously wasn't handled because doing this is a compile error in TypeScript code. However, JavaScript does allow this so esbuild needs to be able to handle this. This edge case should now work correctly with this release.
## 0.8.52
* Fix a concurrent map write with the `--inject:` feature ([#878](https://github.com/evanw/esbuild/issues/878))
Expand Down
17 changes: 15 additions & 2 deletions internal/bundler/bundler_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1791,7 +1791,7 @@ func TestThisInsideFunction(t *testing.T) {
files: map[string]string{
"/entry.js": `
function foo(x = this) { console.log(this) }
const obj = {
const objFoo = {
foo(x = this) { console.log(this) }
}
class Foo {
Expand All @@ -1800,7 +1800,20 @@ func TestThisInsideFunction(t *testing.T) {
foo(x = this) { console.log(this) }
static bar(x = this) { console.log(this) }
}
new Foo(foo(obj))
new Foo(foo(objFoo))
if (nested) {
function bar(x = this) { console.log(this) }
const objBar = {
foo(x = this) { console.log(this) }
}
class Bar {
x = this
static y = this.z
foo(x = this) { console.log(this) }
static bar(x = this) { console.log(this) }
}
new Bar(bar(objBar))
}
`,
},
entryPaths: []string{"/entry.js"},
Expand Down
32 changes: 28 additions & 4 deletions internal/bundler/snapshots/snapshots_default.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2256,22 +2256,46 @@ TestThisInsideFunction
function foo(x = this) {
console.log(this);
}
var obj = {
var objFoo = {
foo(x = this) {
console.log(this);
}
};
var Foo = class {
var _Foo = class {
x = this;
static y = this.z;
foo(x = this) {
console.log(this);
}
static bar(x = this) {
console.log(this);
}
};
new Foo(foo(obj));
var Foo = _Foo;
__publicField(Foo, "y", _Foo.z);
new Foo(foo(objFoo));
if (nested) {
let bar = function(x = this) {
console.log(this);
};
bar2 = bar;
const objBar = {
foo(x = this) {
console.log(this);
}
};
class Bar {
x = this;
static y = this.z;
foo(x = this) {
console.log(this);
}
static bar(x = this) {
console.log(this);
}
}
new Bar(bar(objBar));
}
var bar2;

================================================================================
TestThisOutsideFunction
Expand Down
65 changes: 58 additions & 7 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,10 @@ type fnOnlyDataVisit struct {
thisCaptureRef *js_ast.Ref
argumentsCaptureRef *js_ast.Ref

// Inside a static class property initializer, "this" expressions should be
// replaced with the class name.
thisClassStaticRef *js_ast.Ref

// If we're inside an async arrow function and async functions are not
// supported, then we will have to convert that arrow function to a generator
// function. That means references to "arguments" inside the arrow function
Expand Down Expand Up @@ -8893,30 +8897,48 @@ func (p *parser) visitClass(nameScopeLoc logger.Loc, class *js_ast.Class) js_ast
p.recordDeclaredSymbol(class.Name.Ref)
}

// Replace "this" with a reference to the class inside static field
// initializers either if static fields are not supported or if we are
// converting this class to a "var" to avoid the temporal dead zone.
replaceThisInStaticFieldInit := p.options.unsupportedJSFeatures.Has(compat.ClassStaticField) ||
(p.options.mode == config.ModeBundle && p.currentScope.Parent == nil)

p.pushScopeForVisitPass(js_ast.ScopeClassName, nameScopeLoc)
oldEnclosingClassKeyword := p.enclosingClassKeyword
p.enclosingClassKeyword = class.ClassKeyword
p.currentScope.RecursiveSetStrictMode(js_ast.ImplicitStrictModeClass)

classNameRef := js_ast.InvalidRef
if class.Name != nil {
classNameRef = class.Name.Ref
} else if replaceThisInStaticFieldInit {
// Generate a name if one doesn't already exist. This is necessary for
// handling "this" in static class property initializers.
classNameRef = p.newSymbol(js_ast.SymbolOther, "this")
}

// Insert a shadowing name that spans the whole class, which matches
// JavaScript's semantics. The class body (and extends clause) "captures" the
// original value of the name. This matters for class statements because the
// symbol can be re-assigned to something else later. The captured values
// must be the original value of the name, not the re-assigned value.
shadowRef := js_ast.InvalidRef
if class.Name != nil {
if classNameRef != js_ast.InvalidRef {
// Use "const" for this symbol to match JavaScript run-time semantics. You
// are not allowed to assign to this symbol (it throws a TypeError).
name := p.symbols[class.Name.Ref.InnerIndex].OriginalName
name := p.symbols[classNameRef.InnerIndex].OriginalName
shadowRef = p.newSymbol(js_ast.SymbolConst, "_"+name)
p.recordDeclaredSymbol(shadowRef)
p.currentScope.Members[name] = js_ast.ScopeMember{Loc: class.Name.Loc, Ref: shadowRef}
if class.Name != nil {
p.currentScope.Members[name] = js_ast.ScopeMember{Loc: class.Name.Loc, Ref: shadowRef}
}
}

if class.Extends != nil {
*class.Extends = p.visitExpr(*class.Extends)
}

// Replace "this" inside the class body
oldIsThisCaptured := p.fnOnlyDataVisit.isThisNested
p.fnOnlyDataVisit.isThisNested = true

Expand Down Expand Up @@ -8946,24 +8968,47 @@ func (p *parser) visitClass(nameScopeLoc logger.Loc, class *js_ast.Class) js_ast
p.currentScope.ForbidArguments = true

if property.Value != nil {
// Do not capture "this" inside property values (e.g. methods)
oldThis := p.fnOnlyDataVisit.thisClassStaticRef
p.fnOnlyDataVisit.thisClassStaticRef = nil
*property.Value = p.visitExpr(*property.Value)
p.fnOnlyDataVisit.thisClassStaticRef = oldThis
}

if property.Initializer != nil {
oldThis := p.fnOnlyDataVisit.thisClassStaticRef
if property.IsStatic && replaceThisInStaticFieldInit {
// Replace "this" with the class name inside static property initializers
p.fnOnlyDataVisit.thisClassStaticRef = &shadowRef
} else {
// Otherwise, rely on the native "this" implementation in this initializer
p.fnOnlyDataVisit.thisClassStaticRef = nil
}
*property.Initializer = p.visitExpr(*property.Initializer)
p.fnOnlyDataVisit.thisClassStaticRef = oldThis
}

// Restore the ability to use "arguments" in decorators and computed properties
p.currentScope.ForbidArguments = false
}

// Restore "this" now that we're leaving the class body
p.fnOnlyDataVisit.isThisNested = oldIsThisCaptured

p.enclosingClassKeyword = oldEnclosingClassKeyword
p.popScope()

// Don't generate a shadowing name if one isn't needed
if shadowRef != js_ast.InvalidRef && p.symbols[shadowRef.InnerIndex].UseCountEstimate == 0 {
shadowRef = js_ast.InvalidRef
if shadowRef != js_ast.InvalidRef {
if p.symbols[shadowRef.InnerIndex].UseCountEstimate == 0 {
// Don't generate a shadowing name if one isn't needed
shadowRef = js_ast.InvalidRef
} else if class.Name == nil {
// If there was originally no class name but something inside needed one
// (e.g. there was a static property initializer that referenced "this"),
// store our generated name so the class expression ends up with a name.
class.Name = &js_ast.LocRef{Loc: nameScopeLoc, Ref: classNameRef}
p.currentScope.Generated = append(p.currentScope.Generated, classNameRef)
p.recordDeclaredSymbol(classNameRef)
}
}

return shadowRef
Expand Down Expand Up @@ -9543,6 +9588,12 @@ func (p *parser) visitExpr(expr js_ast.Expr) js_ast.Expr {
}

func (p *parser) valueForThis(loc logger.Loc) (js_ast.Expr, bool) {
// Substitute "this" if we're inside a static class property initializer
if p.fnOnlyDataVisit.thisClassStaticRef != nil {
p.recordUsage(*p.fnOnlyDataVisit.thisClassStaticRef)
return js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: *p.fnOnlyDataVisit.thisClassStaticRef}}, true
}

if p.options.mode != config.ModePassThrough && !p.fnOnlyDataVisit.isThisNested {
if p.es6ImportKeyword.Len > 0 || p.es6ExportKeyword.Len > 0 {
// In an ES6 module, "this" is supposed to be undefined. Instead of
Expand Down
86 changes: 86 additions & 0 deletions internal/js_parser/js_parser_lower_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,92 @@ let Bar = (_a = class {
`)
}

func TestLowerClassStaticThis(t *testing.T) {
expectPrinted(t, "class Foo { x = this }", "class Foo {\n x = this;\n}\n")
expectPrinted(t, "class Foo { static x = this }", "class Foo {\n static x = this;\n}\n")
expectPrinted(t, "class Foo { static x = () => this }", "class Foo {\n static x = () => this;\n}\n")
expectPrinted(t, "class Foo { static x = function() { return this } }", "class Foo {\n static x = function() {\n return this;\n };\n}\n")
expectPrinted(t, "class Foo { static [this.x] }", "class Foo {\n static [this.x];\n}\n")
expectPrinted(t, "class Foo { static x = class { y = this } }", "class Foo {\n static x = class {\n y = this;\n };\n}\n")
expectPrinted(t, "class Foo { static x = class { [this.y] } }", "class Foo {\n static x = class {\n [this.y];\n };\n}\n")
expectPrinted(t, "class Foo { static x = class extends this {} }", "class Foo {\n static x = class extends this {\n };\n}\n")

expectPrinted(t, "x = class Foo { x = this }", "x = class Foo {\n x = this;\n};\n")
expectPrinted(t, "x = class Foo { static x = this }", "x = class Foo {\n static x = this;\n};\n")
expectPrinted(t, "x = class Foo { static x = () => this }", "x = class Foo {\n static x = () => this;\n};\n")
expectPrinted(t, "x = class Foo { static x = function() { return this } }", "x = class Foo {\n static x = function() {\n return this;\n };\n};\n")
expectPrinted(t, "x = class Foo { static [this.x] }", "x = class Foo {\n static [this.x];\n};\n")
expectPrinted(t, "x = class Foo { static x = class { y = this } }", "x = class Foo {\n static x = class {\n y = this;\n };\n};\n")
expectPrinted(t, "x = class Foo { static x = class { [this.y] } }", "x = class Foo {\n static x = class {\n [this.y];\n };\n};\n")
expectPrinted(t, "x = class Foo { static x = class extends this {} }", "x = class Foo {\n static x = class extends this {\n };\n};\n")

expectPrinted(t, "x = class { x = this }", "x = class {\n x = this;\n};\n")
expectPrinted(t, "x = class { static x = this }", "x = class {\n static x = this;\n};\n")
expectPrinted(t, "x = class { static x = () => this }", "x = class {\n static x = () => this;\n};\n")
expectPrinted(t, "x = class { static x = function() { return this } }", "x = class {\n static x = function() {\n return this;\n };\n};\n")
expectPrinted(t, "x = class { static [this.x] }", "x = class {\n static [this.x];\n};\n")
expectPrinted(t, "x = class { static x = class { y = this } }", "x = class {\n static x = class {\n y = this;\n };\n};\n")
expectPrinted(t, "x = class { static x = class { [this.y] } }", "x = class {\n static x = class {\n [this.y];\n };\n};\n")
expectPrinted(t, "x = class { static x = class extends this {} }", "x = class {\n static x = class extends this {\n };\n};\n")

expectPrintedTarget(t, 2015, "class Foo { x = this }",
"class Foo {\n constructor() {\n __publicField(this, \"x\", this);\n }\n}\n")
expectPrintedTarget(t, 2015, "class Foo { [this.x] }",
"var _a;\nclass Foo {\n constructor() {\n __publicField(this, _a);\n }\n}\n_a = this.x;\n")
expectPrintedTarget(t, 2015, "class Foo { static x = this }",
"const _Foo = class {\n};\nlet Foo = _Foo;\n__publicField(Foo, \"x\", _Foo);\n")
expectPrintedTarget(t, 2015, "class Foo { static x = () => this }",
"const _Foo = class {\n};\nlet Foo = _Foo;\n__publicField(Foo, \"x\", () => _Foo);\n")
expectPrintedTarget(t, 2015, "class Foo { static x = function() { return this } }",
"class Foo {\n}\n__publicField(Foo, \"x\", function() {\n return this;\n});\n")
expectPrintedTarget(t, 2015, "class Foo { static [this.x] }",
"var _a;\nclass Foo {\n}\n_a = this.x;\n__publicField(Foo, _a);\n")
expectPrintedTarget(t, 2015, "class Foo { static x = class { y = this } }",
"class Foo {\n}\n__publicField(Foo, \"x\", class {\n constructor() {\n __publicField(this, \"y\", this);\n }\n});\n")
expectPrintedTarget(t, 2015, "class Foo { static x = class { [this.y] } }",
"var _a, _b;\nconst _Foo = class {\n};\nlet Foo = _Foo;\n__publicField(Foo, \"x\", (_b = class {\n constructor() {\n __publicField(this, _a);\n }\n}, _a = _Foo.y, _b));\n")
expectPrintedTarget(t, 2015, "class Foo { static x = class extends this {} }",
"const _Foo = class {\n};\nlet Foo = _Foo;\n__publicField(Foo, \"x\", class extends _Foo {\n});\n")

expectPrintedTarget(t, 2015, "x = class Foo { x = this }",
"x = class Foo {\n constructor() {\n __publicField(this, \"x\", this);\n }\n};\n")
expectPrintedTarget(t, 2015, "x = class Foo { [this.x] }",
"var _a, _b;\nx = (_b = class {\n constructor() {\n __publicField(this, _a);\n }\n}, _a = this.x, _b);\n")
expectPrintedTarget(t, 2015, "x = class Foo { static x = this }",
"var _a;\nx = (_a = class {\n}, __publicField(_a, \"x\", _a), _a);\n")
expectPrintedTarget(t, 2015, "x = class Foo { static x = () => this }",
"var _a;\nx = (_a = class {\n}, __publicField(_a, \"x\", () => _a), _a);\n")
expectPrintedTarget(t, 2015, "x = class Foo { static x = function() { return this } }",
"var _a;\nx = (_a = class {\n}, __publicField(_a, \"x\", function() {\n return this;\n}), _a);\n")
expectPrintedTarget(t, 2015, "x = class Foo { static [this.x] }",
"var _a, _b;\nx = (_b = class {\n}, _a = this.x, __publicField(_b, _a), _b);\n")
expectPrintedTarget(t, 2015, "x = class Foo { static x = class { y = this } }",
"var _a;\nx = (_a = class {\n}, __publicField(_a, \"x\", class {\n constructor() {\n __publicField(this, \"y\", this);\n }\n}), _a);\n")
expectPrintedTarget(t, 2015, "x = class Foo { static x = class { [this.y] } }",
"var _a, _b, _c;\nx = (_c = class {\n}, __publicField(_c, \"x\", (_b = class {\n constructor() {\n __publicField(this, _a);\n }\n}, _a = _c.y, _b)), _c);\n")
expectPrintedTarget(t, 2015, "x = class Foo { static x = class extends this {} }",
"var _a;\nx = (_a = class {\n}, __publicField(_a, \"x\", class extends _a {\n}), _a);\n")

expectPrintedTarget(t, 2015, "x = class { x = this }",
"x = class {\n constructor() {\n __publicField(this, \"x\", this);\n }\n};\n")
expectPrintedTarget(t, 2015, "x = class { [this.x] }",
"var _a, _b;\nx = (_b = class {\n constructor() {\n __publicField(this, _a);\n }\n}, _a = this.x, _b);\n")
expectPrintedTarget(t, 2015, "x = class { static x = this }",
"var _a;\nx = (_a = class {\n}, __publicField(_a, \"x\", _a), _a);\n")
expectPrintedTarget(t, 2015, "x = class { static x = () => this }",
"var _a;\nx = (_a = class {\n}, __publicField(_a, \"x\", () => _a), _a);\n")
expectPrintedTarget(t, 2015, "x = class { static x = function() { return this } }",
"var _a;\nx = (_a = class {\n}, __publicField(_a, \"x\", function() {\n return this;\n}), _a);\n")
expectPrintedTarget(t, 2015, "x = class { static [this.x] }",
"var _a, _b;\nx = (_b = class {\n}, _a = this.x, __publicField(_b, _a), _b);\n")
expectPrintedTarget(t, 2015, "x = class { static x = class { y = this } }",
"var _a;\nx = (_a = class {\n}, __publicField(_a, \"x\", class {\n constructor() {\n __publicField(this, \"y\", this);\n }\n}), _a);\n")
expectPrintedTarget(t, 2015, "x = class { static x = class { [this.y] } }",
"var _a, _b, _c;\nx = (_c = class {\n}, __publicField(_c, \"x\", (_b = class {\n constructor() {\n __publicField(this, _a);\n }\n}, _a = _c.y, _b)), _c);\n")
expectPrintedTarget(t, 2015, "x = class Foo { static x = class extends this {} }",
"var _a;\nx = (_a = class {\n}, __publicField(_a, \"x\", class extends _a {\n}), _a);\n")
}

func TestLowerOptionalChain(t *testing.T) {
expectPrintedTarget(t, 2019, "a?.b.c", "a == null ? void 0 : a.b.c;\n")
expectPrintedTarget(t, 2019, "(a?.b).c", "(a == null ? void 0 : a.b).c;\n")
Expand Down
45 changes: 45 additions & 0 deletions scripts/end-to-end-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -1673,6 +1673,51 @@
}),

// Test class re-assignment
test(['in.js', '--outfile=node.js', '--target=es6'], {
'in.js': `
class Foo {
foo = () => this
}
let foo = new Foo()
if (foo.foo() !== foo) throw 'fail'
`,
}),
test(['in.js', '--outfile=node.js', '--target=es6'], {
'in.js': `
class Foo {
static foo = () => this
}
let old = Foo
let foo = Foo.foo
Foo = class Bar {}
if (foo() !== old) throw 'fail'
`,
}),
test(['in.js', '--outfile=node.js', '--target=es6'], {
'in.js': `
class Foo {
bar = 'works'
foo = () => class {
[this.bar]
}
}
let foo = new Foo().foo
if (!('works' in new (foo()))) throw 'fail'
`,
}),
test(['in.js', '--outfile=node.js', '--target=es6'], {
'in.js': `
class Foo {
static bar = 'works'
static foo = () => class {
[this.bar]
}
}
let foo = Foo.foo
Foo = class Bar {}
if (!('works' in new (foo()))) throw 'fail'
`,
}),
test(['in.js', '--outfile=node.js', '--target=es6'], {
'in.js': `
class Foo {
Expand Down

0 comments on commit 5afc5bf

Please sign in to comment.