Skip to content

Commit

Permalink
implement class static blocks (#1729)
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Oct 30, 2021
1 parent 859382e commit 8161752
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 14 deletions.
39 changes: 39 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,45 @@

## Unreleased

* Implement class static blocks ([#1558](https://github.com/evanw/esbuild/issues/1558))

This release adds support for a new upcoming JavaScript feature called [class static blocks](https://github.com/tc39/proposal-class-static-block) that lets you evaluate code inside of a class body. It looks like this:

```js
class Foo {
static {
this.foo = 123
}
}
```

This can be useful when you want to use `try`/`catch` or access private `#name` fields during class initialization. Doing that without this feature is quite hacky and basically involves creating temporary static fields containing immediately-invoked functions and then deleting the fields after class initialization. Static blocks are much more ergonomic and avoid performance loss due to `delete` changing the object shape.

Static blocks are transformed for older browsers by moving the static block outside of the class body and into an immediately invoked arrow function after the class definition:

```js
// The transformed version of the example code above
const _Foo = class {
};
let Foo = _Foo;
(() => {
_Foo.foo = 123;
})();
```

In case you're wondering, the additional `let` variable is to guard against the potential reassignment of `Foo` during evaluation such as what happens below. The value of `this` must be bound to the original class, not to the current value of `Foo`:

```js
let bar
class Foo {
static {
bar = () => this
}
}
Foo = null
console.log(bar()) // This should not be "null"
```

* Fix issues with `super` property accesses

Code containing `super` property accesses may need to be transformed even when they are supported. For example, in ES6 `async` methods are unsupported while `super` properties are supported. An `async` method containing `super` property accesses requires those uses of `super` to be transformed (the `async` function is transformed into a nested generator function and the `super` keyword cannot be used inside nested functions).
Expand Down
63 changes: 63 additions & 0 deletions internal/bundler/bundler_lower_test.go
Expand Up @@ -1877,3 +1877,66 @@ func TestLowerNullishCoalescingAssignmentIssue1493(t *testing.T) {
},
})
}

func TestStaticClassBlockESNext(t *testing.T) {
lower_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
class A {
static {}
static {
this.thisField++
A.classField++
super.superField = super.superField + 1
super.superField++
}
}
let B = class {
static {}
static {
this.thisField++
super.superField = super.superField + 1
super.superField++
}
}
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.js",
},
})
}

func TestStaticClassBlockES2021(t *testing.T) {
lower_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
class A {
static {}
static {
this.thisField++
A.classField++
super.superField = super.superField + 1
super.superField++
}
}
let B = class {
static {}
static {
this.thisField++
super.superField = super.superField + 1
super.superField++
}
}
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.js",
UnsupportedJSFeatures: es(2021),
},
})
}
45 changes: 45 additions & 0 deletions internal/bundler/snapshots/snapshots_lower.txt
Expand Up @@ -1255,6 +1255,51 @@ y = () => [
tag(_h || (_h = __template(["x", void 0], ["x", "\\u"])), y)
];

================================================================================
TestStaticClassBlockES2021
---------- /out.js ----------
// entry.js
var _A = class {
};
var A = _A;
(() => {
_A.thisField++;
_A.classField++;
__superStaticSet(_A, "superField", __superStaticGet(_A, "superField") + 1);
__superStaticWrapper(_A, "superField")._++;
})();
var _a;
var B = (_a = class {
}, (() => {
_a.thisField++;
__superStaticSet(_a, "superField", __superStaticGet(_a, "superField") + 1);
__superStaticWrapper(_a, "superField")._++;
})(), _a);

================================================================================
TestStaticClassBlockESNext
---------- /out.js ----------
// entry.js
var A = class {
static {
}
static {
this.thisField++;
A.classField++;
super.superField = super.superField + 1;
super.superField++;
}
};
var B = class {
static {
}
static {
this.thisField++;
super.superField = super.superField + 1;
super.superField++;
}
};

================================================================================
TestTSLowerClassField2020NoBundle
---------- /out.js ----------
Expand Down
5 changes: 5 additions & 0 deletions internal/compat/js_table.go
Expand Up @@ -52,6 +52,7 @@ const (
ClassPrivateStaticAccessor
ClassPrivateStaticField
ClassPrivateStaticMethod
ClassStaticBlocks
ClassStaticField
Const
DefaultArgument
Expand Down Expand Up @@ -209,6 +210,10 @@ var jsTable = map[JSFeature]map[Engine][]int{
Node: {14, 6},
Safari: {15},
},
ClassStaticBlocks: {
Chrome: {91},
Node: {16, 11},
},
ClassStaticField: {
Chrome: {73},
Edge: {79},
Expand Down
12 changes: 10 additions & 2 deletions internal/js_ast/js_ast.go
Expand Up @@ -261,11 +261,19 @@ const (
PropertySet
PropertySpread
PropertyDeclare
PropertyClassStaticBlock
)

type ClassStaticBlock struct {
Loc logger.Loc
Stmts []Stmt
}

type Property struct {
TSDecorators []Expr
Key Expr
TSDecorators []Expr
ClassStaticBlock *ClassStaticBlock

Key Expr

// This is omitted for class fields
ValueOrNil Expr
Expand Down
76 changes: 64 additions & 12 deletions internal/js_parser/js_parser.go
Expand Up @@ -1979,25 +1979,31 @@ func (p *parser) parseProperty(kind js_ast.PropertyKind, opts propertyOpts, erro
}
}
} else if p.lexer.Token == js_lexer.TOpenBrace && name == "static" {
p.log.AddRangeError(&p.tracker, p.lexer.Range(), "Class static blocks are not supported yet")

loc := p.lexer.Loc()
p.lexer.Next()

oldIsClassStaticInit := p.fnOrArrowDataParse.isClassStaticInit
oldAwait := p.fnOrArrowDataParse.await
p.fnOrArrowDataParse.isClassStaticInit = true
p.fnOrArrowDataParse.await = forbidAll
oldFnOrArrowDataParse := p.fnOrArrowDataParse
p.fnOrArrowDataParse = fnOrArrowDataParse{
isClassStaticInit: true,
allowSuperProperty: true,
await: forbidAll,
yield: allowIdent,
}

scopeIndex := p.pushScopeForParsePass(js_ast.ScopeClassStaticInit, loc)
p.parseStmtsUpTo(js_lexer.TCloseBrace, parseStmtOpts{})
p.popAndDiscardScope(scopeIndex)
p.pushScopeForParsePass(js_ast.ScopeClassStaticInit, loc)
stmts := p.parseStmtsUpTo(js_lexer.TCloseBrace, parseStmtOpts{})
p.popScope()

p.fnOrArrowDataParse.isClassStaticInit = oldIsClassStaticInit
p.fnOrArrowDataParse.await = oldAwait
p.fnOrArrowDataParse = oldFnOrArrowDataParse

p.lexer.Expect(js_lexer.TCloseBrace)

return js_ast.Property{
Kind: js_ast.PropertyClassStaticBlock,
ClassStaticBlock: &js_ast.ClassStaticBlock{
Loc: loc,
Stmts: stmts,
},
}, true
}
}

Expand Down Expand Up @@ -9777,8 +9783,47 @@ func (p *parser) visitClass(nameScopeLoc logger.Loc, class *js_ast.Class) js_ast
p.pushScopeForVisitPass(js_ast.ScopeClassBody, class.BodyLoc)
defer p.popScope()

end := 0

for i := range class.Properties {
property := &class.Properties[i]

if property.Kind == js_ast.PropertyClassStaticBlock {
oldFnOrArrowData := p.fnOrArrowDataVisit
oldFnOnlyDataVisit := p.fnOnlyDataVisit

p.fnOrArrowDataVisit = fnOrArrowDataVisit{}
p.fnOnlyDataVisit = fnOnlyDataVisit{
isThisNested: true,
isNewTargetAllowed: true,
}

if classLoweringInfo.lowerAllStaticFields {
// Replace "this" with the class name inside static class blocks
p.fnOnlyDataVisit.thisClassStaticRef = &shadowRef

// Need to lower "super" since it won't be valid outside the class body
p.fnOnlyDataVisit.shouldLowerSuper = true
}

p.pushScopeForVisitPass(js_ast.ScopeClassStaticInit, property.ClassStaticBlock.Loc)
property.ClassStaticBlock.Stmts = p.visitStmts(property.ClassStaticBlock.Stmts, stmtsFnBody)
p.popScope()

p.fnOrArrowDataVisit = oldFnOrArrowData
p.fnOnlyDataVisit = oldFnOnlyDataVisit

// "class { static {} }" => "class {}"
if p.options.mangleSyntax && len(property.ClassStaticBlock.Stmts) == 0 {
continue
}

// Keep this property
class.Properties[end] = *property
end++
continue
}

property.TSDecorators = p.visitTSDecorators(property.TSDecorators)
private, isPrivate := property.Key.Data.(*js_ast.EPrivateIdentifier)

Expand Down Expand Up @@ -9869,8 +9914,15 @@ func (p *parser) visitClass(nameScopeLoc logger.Loc, class *js_ast.Class) js_ast

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

// Keep this property
class.Properties[end] = *property
end++
}

// Finish the filtering operation
class.Properties = class.Properties[:end]

p.enclosingClassKeyword = oldEnclosingClassKeyword
p.popScope()

Expand Down
25 changes: 25 additions & 0 deletions internal/js_parser/js_parser_lower.go
Expand Up @@ -1820,6 +1820,13 @@ func (p *parser) computeClassLoweringInfo(class *js_ast.Class) (result classLowe
// _foo = new WeakMap();
//
for _, prop := range class.Properties {
if prop.Kind == js_ast.PropertyClassStaticBlock {
if p.options.unsupportedJSFeatures.Has(compat.ClassStaticBlocks) && len(prop.ClassStaticBlock.Stmts) > 0 {
result.lowerAllStaticFields = true
}
continue
}

if private, ok := prop.Key.Data.(*js_ast.EPrivateIdentifier); ok {
if prop.IsStatic {
if p.privateSymbolNeedsToBeLowered(private) {
Expand Down Expand Up @@ -2105,6 +2112,24 @@ func (p *parser) lowerClass(stmt js_ast.Stmt, expr js_ast.Expr, shadowRef js_ast
classLoweringInfo := p.computeClassLoweringInfo(class)

for _, prop := range class.Properties {
if prop.Kind == js_ast.PropertyClassStaticBlock {
if p.options.unsupportedJSFeatures.Has(compat.ClassStaticBlocks) {
if block := *prop.ClassStaticBlock; len(block.Stmts) > 0 {
staticMembers = append(staticMembers, js_ast.Expr{Loc: block.Loc, Data: &js_ast.ECall{
Target: js_ast.Expr{Loc: block.Loc, Data: &js_ast.EArrow{Body: js_ast.FnBody{
Stmts: block.Stmts,
}}},
}})
}
continue
}

// Keep this property
class.Properties[end] = prop
end++
continue
}

// Merge parameter decorators with method decorators
if p.options.ts.Parse && prop.IsMethod {
if fn, ok := prop.ValueOrNil.Data.(*js_ast.EFunction); ok {
Expand Down
16 changes: 16 additions & 0 deletions internal/js_parser/js_parser_test.go
Expand Up @@ -1673,6 +1673,22 @@ func TestClassFields(t *testing.T) {
expectPrinted(t, "class Foo { static ['prototype'] = 1 }", "class Foo {\n static [\"prototype\"] = 1;\n}\n")
}

func TestClassStaticBlocks(t *testing.T) {
expectPrinted(t, "class Foo { static {} }", "class Foo {\n static {\n }\n}\n")
expectPrinted(t, "class Foo { static {} x = 1 }", "class Foo {\n static {\n }\n x = 1;\n}\n")
expectPrinted(t, "class Foo { static { this.foo() } }", "class Foo {\n static {\n this.foo();\n }\n}\n")

expectParseError(t, "class Foo { static { yield } }",
"<stdin>: error: \"yield\" is a reserved word and cannot be used in strict mode\n"+
"<stdin>: note: All code inside a class is implicitly in strict mode\n")
expectParseError(t, "class Foo { static { await } }", "<stdin>: error: The keyword \"await\" cannot be used here\n")
expectParseError(t, "class Foo { static { return } }", "<stdin>: error: A return statement cannot be used inside a class static block\n")
expectParseError(t, "class Foo { static { break } }", "<stdin>: error: Cannot use \"break\" here\n")
expectParseError(t, "class Foo { static { continue } }", "<stdin>: error: Cannot use \"continue\" here\n")
expectParseError(t, "x: { class Foo { static { break x } } }", "<stdin>: error: There is no containing label named \"x\"\n")
expectParseError(t, "x: { class Foo { static { continue x } } }", "<stdin>: error: There is no containing label named \"x\"\n")
}

func TestGenerator(t *testing.T) {
expectParseError(t, "(class { * foo })", "<stdin>: error: Expected \"(\" but found \"}\"\n")
expectParseError(t, "(class { * *foo() {} })", "<stdin>: error: Unexpected \"*\"\n")
Expand Down
9 changes: 9 additions & 0 deletions internal/js_printer/js_printer.go
Expand Up @@ -819,6 +819,15 @@ func (p *printer) printClass(class js_ast.Class) {
for _, item := range class.Properties {
p.printSemicolonIfNeeded()
p.printIndent()

if item.Kind == js_ast.PropertyClassStaticBlock {
p.print("static")
p.printSpace()
p.printBlock(item.ClassStaticBlock.Loc, item.ClassStaticBlock.Stmts)
p.printNewline()
continue
}

p.printProperty(item)

// Need semicolons after class fields
Expand Down

0 comments on commit 8161752

Please sign in to comment.