From 2cf35ea612b35d09313f8aa5e4bb69d5f2a11a99 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Mon, 8 Jun 2020 02:50:31 -0700 Subject: [PATCH] fix #104: initial implementation of typescript decorators --- CHANGELOG.md | 34 ++ README.md | 7 +- internal/ast/ast.go | 44 ++- internal/bundler/bundler_test.go | 22 +- internal/bundler/bundler_ts_test.go | 365 ++++++++++++++++++++ internal/bundler/linker.go | 20 +- internal/parser/parser.go | 500 ++++++++++++++++++++-------- internal/parser/parser_ts_test.go | 78 +++-- internal/runtime/runtime.go | 72 ++++ 9 files changed, 921 insertions(+), 221 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 679e0c2fbfe..8ec58cafa13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ ## Unreleased +* Initial implementation of TypeScript decorators ([#104](https://github.com/evanw/esbuild/issues/104)) + + This release contains an initial implementation of the non-standard TypeScript-specific decorator syntax. This syntax transformation is enabled by default in esbuild, so no extra configuration is needed. The TypeScript compiler will need `"experimentalDecorators": true` configured in `tsconfig.json` for type checking to work with TypeScript decorators. + + Here's an example of a method decorator: + + ```ts + function logged(target, key, descriptor) { + let method = descriptor.value + descriptor.value = function(...args) { + let result = method.apply(this, args) + let joined = args.map(x => JSON.stringify(x)).join(', ') + console.log(`${key}(${joined}) => ${JSON.stringify(result)}`) + return result + } + } + + class Example { + @logged + method(text: string) { + return text + '!' + } + } + + const x = new Example + x.method('text') + ``` + + There are four kinds of TypeScript decorators: class, method, parameter, and field decorators. See [the TypeScript decorator documentation](https://www.typescriptlang.org/docs/handbook/decorators.html) for more information. Note that esbuild only implements TypeScript's `experimentalDecorators` setting. It does not implement the `emitDecoratorMetadata` setting because that requires type information. + +* Fix order of side effects for computed fields + + When transforming computed class fields, esbuild had a bug where the side effects of the field property names were not evaluated in source code order. The order of side effects now matches the order in the source code. + * Fix private fields in TypeScript This fixes a bug with private instance fields in TypeScript where the private field declaration was incorrectly removed during the TypeScript class field transform, which inlines the initializers into the constructor. Now the initializers are still moved to the constructor but the private field declaration is preserved without the initializer. diff --git a/README.md b/README.md index 870f20b8099..40daa93215c 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ These TypeScript-only syntax features are supported, and are always converted to | Type casts | `a as B` and `a` | | | Type imports | `import {Type} from 'foo'` | Handled by removing all unused imports | | Type exports | `export {Type} from 'foo'` | Handled by ignoring missing exports in TypeScript files | +| Experimental decorators | `@sealed class Foo {}` | | These TypeScript-only syntax features are parsed and ignored (a non-exhaustive list): @@ -156,12 +157,6 @@ These TypeScript-only syntax features are parsed and ignored (a non-exhaustive l | Type-only imports | `import type {Type} from 'foo'` | | Type-only exports | `export type {Type} from 'foo'` | -These TypeScript-only syntax features are not yet supported, and currently cannot be parsed (an exhaustive list): - -| Syntax feature | Example | Notes | -|-------------------------|------------------------|-------| -| Experimental decorators | `@sealed class Foo {}` | This is tracked [here](https://github.com/evanw/esbuild/issues/104) | - #### Disclaimers: * As far as I know, this hasn't yet been used in production by anyone yet. It's still pretty new code and you may run into some bugs. diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 204763f596c..7de89c89250 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -256,11 +256,8 @@ const ( ) type Property struct { - Kind PropertyKind - IsComputed bool - IsMethod bool - IsStatic bool - Key Expr + TSDecorators []Expr + Key Expr // This is omitted for class fields Value *Expr @@ -275,6 +272,11 @@ type Property struct { // class Foo { a = 1 } // Initializer *Expr + + Kind PropertyKind + IsComputed bool + IsMethod bool + IsStatic bool } type PropertyBinding struct { @@ -286,20 +288,22 @@ type PropertyBinding struct { } type Arg struct { + TSDecorators []Expr + Binding Binding + Default *Expr + // "constructor(public x: boolean) {}" IsTypeScriptCtorField bool - - Binding Binding - Default *Expr } type Fn struct { - Name *LocRef - Args []Arg + Name *LocRef + Args []Arg + Body FnBody + IsAsync bool IsGenerator bool HasRestArg bool - Body FnBody } type FnBody struct { @@ -308,10 +312,11 @@ type FnBody struct { } type Class struct { - Name *LocRef - Extends *Expr - BodyLoc Loc - Properties []Property + TSDecorators []Expr + Name *LocRef + Extends *Expr + BodyLoc Loc + Properties []Property } type ArrayBinding struct { @@ -1257,11 +1262,16 @@ func GenerateNonUniqueNameFromPath(text string) string { // Convert it to an ASCII identifier bytes := []byte{} + needsGap := false for _, c := range base { if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (len(bytes) > 0 && c >= '0' && c <= '9') { + if needsGap { + bytes = append(bytes, '_') + needsGap = false + } bytes = append(bytes, byte(c)) - } else if len(bytes) > 0 && bytes[len(bytes)-1] != '_' { - bytes = append(bytes, '_') + } else if len(bytes) > 0 { + needsGap = true } } diff --git a/internal/bundler/bundler_test.go b/internal/bundler/bundler_test.go index 0c7dc7b2333..bfff5e7f176 100644 --- a/internal/bundler/bundler_test.go +++ b/internal/bundler/bundler_test.go @@ -547,13 +547,13 @@ export * as fromB from "./b"; "/out/b.js": `export default function() { } `, - "/out/c.js": `export default function a() { + "/out/c.js": `export default function b() { } `, "/out/d.js": `export default class { } `, - "/out/e.js": `export default class a { + "/out/e.js": `export default class b { } `, }, @@ -643,7 +643,7 @@ import f, * as e from "foo"; import g, {a2 as h, b as i} from "foo"; const j = [ import("foo"), - function C() { + function F() { return import("foo"); } ]; @@ -729,9 +729,9 @@ var require_c = __commonJS((exports) => { // /d.js var require_d = __commonJS((exports) => { __export(exports, { - default: () => Foo + default: () => d_default }); - class Foo { + class d_default { } }); @@ -747,9 +747,9 @@ var require_e = __commonJS((exports) => { // /f.js var require_f = __commonJS((exports) => { __export(exports, { - default: () => foo + default: () => f_default }); - function foo() { + function f_default() { } }); @@ -765,9 +765,9 @@ var require_g = __commonJS((exports) => { // /h.js var require_h = __commonJS((exports) => { __export(exports, { - default: () => foo + default: () => h_default }); - async function foo() { + async function h_default() { } }); @@ -822,9 +822,9 @@ func TestReExportDefaultCommonJS(t *testing.T) { "/out.js": `// /bar.js var require_bar = __commonJS((exports) => { __export(exports, { - default: () => foo2 + default: () => bar_default }); - function foo2() { + function bar_default() { return exports; } }); diff --git a/internal/bundler/bundler_ts_test.go b/internal/bundler/bundler_ts_test.go index 02185b49c16..7b4cdb6273e 100644 --- a/internal/bundler/bundler_ts_test.go +++ b/internal/bundler/bundler_ts_test.go @@ -788,3 +788,368 @@ func TestTSNamespaceExportArraySpreadNoBundle(t *testing.T) { }, }) } + +func TestTypeScriptDecorators(t *testing.T) { + expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + import all from './all' + import all_computed from './all_computed' + import {a} from './a' + import {b} from './b' + import {c} from './c' + import {d} from './d' + import e from './e' + import f from './f' + import g from './g' + import h from './h' + import {i} from './i' + import {j} from './j' + import k from './k' + console.log(all, all_computed, a, b, c, d, e, f, g, h, i, j, k) + `, + "/all.ts": ` + @x.y() + @new y.x() + export default class Foo { + @x @y mUndef + @x @y mDef = 1 + @x @y method(@x0 @y0 arg0, @x1 @y1 arg1) { return new Foo } + @x @y static sUndef + @x @y static sDef = new Foo + @x @y static sMethod(@x0 @y0 arg0, @x1 @y1 arg1) { return new Foo } + } + `, + "/all_computed.ts": ` + @x?.[_ + 'y']() + @new y?.[_ + 'x']() + export default class Foo { + @x @y [mUndef()] + @x @y [mDef()] = 1 + @x @y [method()](@x0 @y0 arg0, @x1 @y1 arg1) { return new Foo } + + // Side effect order must be preserved even for fields without decorators + [xUndef()] + [xDef()] = 2 + static [yUndef()] + static [yDef()] = 3 + + @x @y static [sUndef()] + @x @y static [sDef()] = new Foo + @x @y static [sMethod()](@x0 @y0 arg0, @x1 @y1 arg1) { return new Foo } + } + `, + "/a.ts": ` + @x(() => 0) @y(() => 1) + class a_class { + fn() { return new a_class } + static z = new a_class + } + export let a = a_class + `, + "/b.ts": ` + @x(() => 0) @y(() => 1) + abstract class b_class { + fn() { return new b_class } + static z = new b_class + } + export let b = b_class + `, + "/c.ts": ` + @x(() => 0) @y(() => 1) + export class c { + fn() { return new c } + static z = new c + } + `, + "/d.ts": ` + @x(() => 0) @y(() => 1) + export abstract class d { + fn() { return new d } + static z = new d + } + `, + "/e.ts": ` + @x(() => 0) @y(() => 1) + export default class {} + `, + "/f.ts": ` + @x(() => 0) @y(() => 1) + export default class f { + fn() { return new f } + static z = new f + } + `, + "/g.ts": ` + @x(() => 0) @y(() => 1) + export default abstract class {} + `, + "/h.ts": ` + @x(() => 0) @y(() => 1) + export default abstract class h { + fn() { return new h } + static z = new h + } + `, + "/i.ts": ` + class i_class { + @x(() => 0) @y(() => 1) + foo + } + export let i = i_class + `, + "/j.ts": ` + export class j { + @x(() => 0) @y(() => 1) + foo() {} + } + `, + "/k.ts": ` + export default class { + foo(@x(() => 0) @y(() => 1) x) {} + } + `, + }, + entryPaths: []string{"/entry.js"}, + parseOptions: parser.ParseOptions{ + IsBundling: true, + }, + bundleOptions: BundleOptions{ + IsBundling: true, + AbsOutputFile: "/out.js", + }, + expected: map[string]string{ + "/out.js": `// /all.ts +let Foo = class { + constructor() { + this.mDef = 1; + } + method(arg0, arg1) { + return new Foo(); + } + static sMethod(arg0, arg1) { + return new Foo(); + } +}; +Foo.sDef = new Foo(); +__decorate([ + x, + y +], Foo.prototype, "mUndef", 2); +__decorate([ + x, + y +], Foo.prototype, "mDef", 2); +__decorate([ + x, + y, + __param(0, x0), + __param(0, y0), + __param(1, x1), + __param(1, y1) +], Foo.prototype, "method", 1); +__decorate([ + x, + y +], Foo.prototype, "sUndef", 2); +__decorate([ + x, + y +], Foo.prototype, "sDef", 2); +__decorate([ + x, + y, + __param(0, x0), + __param(0, y0), + __param(1, x1), + __param(1, y1) +], Foo.prototype, "sMethod", 1); +Foo = __decorate([ + x.y(), + new y.x() +], Foo); +const all_default = Foo; + +// /all_computed.ts +var _a, _b, _c, _d, _e, _f, _g, _h; +let Foo2 = class { + constructor() { + this[_b] = 1; + this[_d] = 2; + } + [(_a = mUndef(), _b = mDef(), _c = method())](arg0, arg1) { + return new Foo2(); + } + static [(xUndef(), _d = xDef(), yUndef(), _e = yDef(), _f = sUndef(), _g = sDef(), _h = sMethod())](arg0, arg1) { + return new Foo2(); + } +}; +Foo2[_e] = 3; +Foo2[_g] = new Foo2(); +__decorate([ + x, + y +], Foo2.prototype, _a, 2); +__decorate([ + x, + y +], Foo2.prototype, _b, 2); +__decorate([ + x, + y, + __param(0, x0), + __param(0, y0), + __param(1, x1), + __param(1, y1) +], Foo2.prototype, _c, 1); +__decorate([ + x, + y +], Foo2.prototype, _f, 2); +__decorate([ + x, + y +], Foo2.prototype, _g, 2); +__decorate([ + x, + y, + __param(0, x0), + __param(0, y0), + __param(1, x1), + __param(1, y1) +], Foo2.prototype, _h, 1); +Foo2 = __decorate([ + x?.[_ + "y"](), + new y?.[_ + "x"]() +], Foo2); +const all_computed_default = Foo2; + +// /a.ts +let a_class = class { + fn() { + return new a_class(); + } +}; +a_class.z = new a_class(); +a_class = __decorate([ + x(() => 0), + y(() => 1) +], a_class); +let a = a_class; + +// /b.ts +let b_class = class { + fn() { + return new b_class(); + } +}; +b_class.z = new b_class(); +b_class = __decorate([ + x(() => 0), + y(() => 1) +], b_class); +let b = b_class; + +// /c.ts +let c = class { + fn() { + return new c(); + } +}; +c.z = new c(); +c = __decorate([ + x(() => 0), + y(() => 1) +], c); + +// /d.ts +let d = class { + fn() { + return new d(); + } +}; +d.z = new d(); +d = __decorate([ + x(() => 0), + y(() => 1) +], d); + +// /e.ts +let _a2 = class { +}; +_a2 = __decorate([ + x(() => 0), + y(() => 1) +], _a2); +const e_default = _a2; + +// /f.ts +let f2 = class { + fn() { + return new f2(); + } +}; +f2.z = new f2(); +f2 = __decorate([ + x(() => 0), + y(() => 1) +], f2); +const f_default = f2; + +// /g.ts +let _a3 = class { +}; +_a3 = __decorate([ + x(() => 0), + y(() => 1) +], _a3); +const g_default = _a3; + +// /h.ts +let h2 = class { + fn() { + return new h2(); + } +}; +h2.z = new h2(); +h2 = __decorate([ + x(() => 0), + y(() => 1) +], h2); +const h2 = h2; + +// /i.ts +class i_class { +} +__decorate([ + x(() => 0), + y(() => 1) +], i_class.prototype, "foo", 2); +let i2 = i_class; + +// /j.ts +class j2 { + foo() { + } +} +__decorate([ + x(() => 0), + y(() => 1) +], j2.prototype, "foo", 1); + +// /k.ts +class k_default { + foo(x2) { + } +} +__decorate([ + __param(0, x2(() => 0)), + __param(0, y(() => 1)) +], _a4.prototype, "foo", 1); + +// /entry.js +console.log(all_default, all_computed_default, a, b, c, d, e_default, f_default, g_default, h2, i2, j2, k_default); +`, + }, + }) +} diff --git a/internal/bundler/linker.go b/internal/bundler/linker.go index 6cb86f9b1e1..b5037aff9d7 100644 --- a/internal/bundler/linker.go +++ b/internal/bundler/linker.go @@ -1483,21 +1483,21 @@ func (c *linkerContext) convertStmtsForChunk(sourceIndex uint32, stmtList *stmtL case *ast.SFunction: // "export default function() {}" => "function default() {}" // "export default function foo() {}" => "function foo() {}" - if s2.Fn.Name == nil { - // Be careful to not modify the original statement - s2 = &ast.SFunction{Fn: s2.Fn} - s2.Fn.Name = &s.DefaultName - } + + // Be careful to not modify the original statement + s2 = &ast.SFunction{Fn: s2.Fn} + s2.Fn.Name = &s.DefaultName + stmt = ast.Stmt{s.Value.Stmt.Loc, s2} case *ast.SClass: // "export default class {}" => "class default {}" // "export default class Foo {}" => "class Foo {}" - if s2.Class.Name == nil { - // Be careful to not modify the original statement - s2 = &ast.SClass{Class: s2.Class} - s2.Class.Name = &s.DefaultName - } + + // Be careful to not modify the original statement + s2 = &ast.SClass{Class: s2.Class} + s2.Class.Name = &s.DefaultName + stmt = ast.Stmt{s.Value.Stmt.Loc, s2} default: diff --git a/internal/parser/parser.go b/internal/parser/parser.go index e12540c7968..df053663ae3 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -123,6 +123,9 @@ type fnOpts struct { // In TypeScript, forward declarations of functions have no bodies allowMissingBodyForTypeScript bool + + // Allow TypeScript decorators in function arguments + allowTSDecorators bool } func isJumpStatement(data ast.S) bool { @@ -367,6 +370,27 @@ func (p *parser) popAndFlattenScope(scopeIndex int) { } } +// Undo all scopes pushed and popped after this scope index. This assumes that +// the scope stack is at the same level now as it was at the given scope index. +func (p *parser) discardScopesUpTo(scopeIndex int) { + // Remove any direct children from their parent + children := p.currentScope.Children + for _, child := range p.scopesInOrder[scopeIndex:] { + if child.scope.Parent == p.currentScope { + for i := len(children) - 1; i >= 0; i-- { + if children[i] == child.scope { + children = append(children[:i], children[i+1:]...) + break + } + } + } + } + p.currentScope.Children = children + + // Truncate the scope order where we started to pretend we never saw this scope + p.scopesInOrder = p.scopesInOrder[:scopeIndex] +} + func (p *parser) newSymbol(kind ast.SymbolKind, name string) ast.Ref { ref := ast.Ref{p.source.Index, uint32(len(p.symbols))} p.symbols = append(p.symbols, ast.Symbol{ @@ -1349,23 +1373,21 @@ func (p *parser) logBindingErrors(errors *deferredErrors) { } } -type propertyContext int - -const ( - propertyContextObject = iota - propertyContextClass -) - type propertyOpts struct { - asyncRange ast.Range - isAsync bool - isGenerator bool - isStatic bool - classHasExtends bool + asyncRange ast.Range + isAsync bool + isGenerator bool + + // Class-related options + isStatic bool + isClass bool + classHasExtends bool + allowTSDecorators bool + tsDecorators []ast.Expr } func (p *parser) parseProperty( - context propertyContext, kind ast.PropertyKind, opts propertyOpts, errors *deferredErrors, + kind ast.PropertyKind, opts propertyOpts, errors *deferredErrors, ) (ast.Property, bool) { var key ast.Expr keyRange := p.lexer.Range() @@ -1381,7 +1403,7 @@ func (p *parser) parseProperty( p.lexer.Next() case lexer.TPrivateIdentifier: - if context != propertyContextClass { + if !opts.isClass || len(opts.tsDecorators) > 0 { p.lexer.Expected(lexer.TIdentifier) } p.markFutureSyntax(futureSyntaxPrivateName, p.lexer.Range()) @@ -1395,8 +1417,7 @@ func (p *parser) parseProperty( expr := p.parseExpr(ast.LComma) // Handle index signatures - if p.ts.Parse && p.lexer.Token == lexer.TColon && wasIdentifier && - context == propertyContextClass { + if p.ts.Parse && p.lexer.Token == lexer.TColon && wasIdentifier && opts.isClass { if _, ok := expr.Data.(*ast.EIdentifier); ok { // "[key: string]: any;" p.lexer.Next() @@ -1420,7 +1441,7 @@ func (p *parser) parseProperty( } p.lexer.Next() opts.isGenerator = true - return p.parseProperty(context, ast.PropertyNormal, opts, errors) + return p.parseProperty(ast.PropertyNormal, opts, errors) default: name := p.lexer.Identifier @@ -1447,31 +1468,31 @@ func (p *parser) parseProperty( switch name { case "get": if !opts.isAsync { - return p.parseProperty(context, ast.PropertyGet, opts, nil) + return p.parseProperty(ast.PropertyGet, opts, nil) } case "set": if !opts.isAsync { - return p.parseProperty(context, ast.PropertySet, opts, nil) + return p.parseProperty(ast.PropertySet, opts, nil) } case "async": if !opts.isAsync { opts.isAsync = true opts.asyncRange = nameRange - return p.parseProperty(context, kind, opts, nil) + return p.parseProperty(kind, opts, nil) } case "static": - if !opts.isStatic && !opts.isAsync && context == propertyContextClass { + if !opts.isStatic && !opts.isAsync && opts.isClass { opts.isStatic = true - return p.parseProperty(context, kind, opts, nil) + return p.parseProperty(kind, opts, nil) } case "private", "protected", "public", "readonly", "abstract": // Skip over TypeScript keywords - if context == propertyContextClass && p.ts.Parse { - return p.parseProperty(context, kind, opts, nil) + if opts.isClass && p.ts.Parse { + return p.parseProperty(kind, opts, nil) } } } @@ -1480,7 +1501,7 @@ func (p *parser) parseProperty( key = ast.Expr{nameRange.Loc, &ast.EString{lexer.StringToUTF16(name)}} // Parse a shorthand property - if context == propertyContextObject && kind == ast.PropertyNormal && p.lexer.Token != lexer.TColon && + if !opts.isClass && kind == ast.PropertyNormal && p.lexer.Token != lexer.TColon && p.lexer.Token != lexer.TOpenParen && p.lexer.Token != lexer.TLessThan && !opts.isGenerator { ref := p.storeNameInRef(name) value := ast.Expr{key.Loc, &ast.EIdentifier{ref}} @@ -1506,7 +1527,7 @@ func (p *parser) parseProperty( if p.ts.Parse { // "class X { foo?: number }" // "class X { foo!: number }" - if context == propertyContextClass && (p.lexer.Token == lexer.TQuestion || p.lexer.Token == lexer.TExclamation) { + if opts.isClass && (p.lexer.Token == lexer.TQuestion || p.lexer.Token == lexer.TExclamation) { p.lexer.Next() } @@ -1516,8 +1537,8 @@ func (p *parser) parseProperty( } // Parse a class field with an optional initial value - if context == propertyContextClass && kind == ast.PropertyNormal && - !opts.isAsync && !opts.isGenerator && p.lexer.Token != lexer.TOpenParen { + if opts.isClass && kind == ast.PropertyNormal && !opts.isAsync && + !opts.isGenerator && p.lexer.Token != lexer.TOpenParen { var initializer *ast.Expr // Forbid the names "constructor" and "prototype" in some cases @@ -1551,23 +1572,24 @@ func (p *parser) parseProperty( p.lexer.ExpectOrInsertSemicolon() return ast.Property{ - Kind: kind, - IsComputed: isComputed, - IsStatic: opts.isStatic, - Key: key, - Initializer: initializer, + TSDecorators: opts.tsDecorators, + Kind: kind, + IsComputed: isComputed, + IsStatic: opts.isStatic, + Key: key, + Initializer: initializer, }, true } // Parse a method expression if p.lexer.Token == lexer.TOpenParen || kind != ast.PropertyNormal || - context == propertyContextClass || opts.isAsync || opts.isGenerator { + opts.isClass || opts.isAsync || opts.isGenerator { loc := p.lexer.Loc() scopeIndex := p.pushScopeForParsePass(ast.ScopeFunctionArgs, loc) isConstructor := false // Forbid the names "constructor" and "prototype" in some cases - if context == propertyContextClass && !isComputed { + if opts.isClass && !isComputed { if str, ok := key.Data.(*ast.EString); ok { if !opts.isStatic && lexer.UTF16EqualsString(str.Value, "constructor") { switch { @@ -1589,13 +1611,14 @@ func (p *parser) parseProperty( } fn, hadBody := p.parseFn(nil, fnOpts{ - asyncRange: opts.asyncRange, - allowAwait: opts.isAsync, - allowYield: opts.isGenerator, - allowSuperCall: opts.classHasExtends && isConstructor, + asyncRange: opts.asyncRange, + allowAwait: opts.isAsync, + allowYield: opts.isGenerator, + allowSuperCall: opts.classHasExtends && isConstructor, + allowTSDecorators: opts.allowTSDecorators, // Only allow omitting the body if we're parsing TypeScript class - allowMissingBodyForTypeScript: p.ts.Parse && context == propertyContextClass, + allowMissingBodyForTypeScript: p.ts.Parse && opts.isClass, }) // "class Foo { foo(): void; foo(): void {} }" @@ -1627,12 +1650,13 @@ func (p *parser) parseProperty( } return ast.Property{ - Kind: kind, - IsComputed: isComputed, - IsMethod: true, - IsStatic: opts.isStatic, - Key: key, - Value: &value, + TSDecorators: opts.tsDecorators, + Kind: kind, + IsComputed: isComputed, + IsMethod: true, + IsStatic: opts.isStatic, + Key: key, + Value: &value, }, true } @@ -2088,7 +2112,13 @@ func (p *parser) convertExprToBinding(expr ast.Expr, invalidLog []ast.Loc) (ast. } } -func (p *parser) parsePrefix(level ast.L, errors *deferredErrors) ast.Expr { +type exprFlag uint8 + +const ( + exprFlagTSDecorator exprFlag = 1 << iota +) + +func (p *parser) parsePrefix(level ast.L, errors *deferredErrors, flags exprFlag) ast.Expr { loc := p.lexer.Loc() switch p.lexer.Token { @@ -2351,7 +2381,7 @@ func (p *parser) parsePrefix(level ast.L, errors *deferredErrors) ast.Expr { return ast.Expr{loc, &ast.ENewTarget{}} } - target := p.parseExpr(ast.LCall) + target := p.parseExprWithFlags(ast.LCall, flags) args := []ast.Expr{} if p.ts.Parse { @@ -2454,7 +2484,7 @@ func (p *parser) parsePrefix(level ast.L, errors *deferredErrors) ast.Expr { } } else { // This property may turn out to be a type in TypeScript, which should be ignored - if property, ok := p.parseProperty(propertyContextObject, ast.PropertyNormal, propertyOpts{}, &selfErrors); ok { + if property, ok := p.parseProperty(ast.PropertyNormal, propertyOpts{}, &selfErrors); ok { properties = append(properties, property) } } @@ -2572,7 +2602,7 @@ func (p *parser) parsePrefix(level ast.L, errors *deferredErrors) ast.Expr { p.lexer.Next() p.skipTypeScriptType(ast.LLowest) p.lexer.ExpectGreaterThan(false /* isInsideJSXElement */) - return p.parsePrefix(level, errors) + return p.parsePrefix(level, errors, flags) } p.lexer.Unexpected() @@ -2698,14 +2728,18 @@ func (p *parser) parseImportExpr(loc ast.Loc) ast.Expr { } func (p *parser) parseExprOrBindings(level ast.L, errors *deferredErrors) ast.Expr { - return p.parseSuffix(p.parsePrefix(level, errors), level, errors) + return p.parseSuffix(p.parsePrefix(level, errors, 0), level, errors, 0) } func (p *parser) parseExpr(level ast.L) ast.Expr { - return p.parseSuffix(p.parsePrefix(level, nil), level, nil) + return p.parseSuffix(p.parsePrefix(level, nil, 0), level, nil, 0) } -func (p *parser) parseSuffix(left ast.Expr, level ast.L, errors *deferredErrors) ast.Expr { +func (p *parser) parseExprWithFlags(level ast.L, flags exprFlag) ast.Expr { + return p.parseSuffix(p.parsePrefix(level, nil, flags), level, nil, flags) +} + +func (p *parser) parseSuffix(left ast.Expr, level ast.L, errors *deferredErrors, flags exprFlag) ast.Expr { // ArrowFunction is a special case in the grammar. Although it appears to be // a PrimaryExpression, it's actually an AssigmentExpression. This means if // a AssigmentExpression ends up producing an ArrowFunction then nothing can @@ -2870,6 +2904,18 @@ func (p *parser) parseSuffix(left ast.Expr, level ast.L, errors *deferredErrors) left = ast.Expr{left.Loc, &ast.ETemplate{&tag, head, headRaw, parts}} case lexer.TOpenBracket: + // When parsing a decorator, ignore EIndex expressions since they may be + // part of a computed property: + // + // class Foo { + // @foo ['computed']() {} + // } + // + // This matches the behavior of the TypeScript compiler. + if (flags & exprFlagTSDecorator) != 0 { + return left + } + p.lexer.Next() // Allow "in" inside the brackets @@ -3822,9 +3868,9 @@ func (p *parser) parseFn(name *ast.LocRef, opts fnOpts) (fn ast.Fn, hadBody bool continue } - // Skip past decorators and recover even though they aren't supported - if p.ts.Parse { - p.parseAndSkipDecorators() + var tsDecorators []ast.Expr + if opts.allowTSDecorators { + tsDecorators = p.parseTSDecorators() } if !hasRestArg && p.lexer.Token == lexer.TDotDotDot { @@ -3888,8 +3934,9 @@ func (p *parser) parseFn(name *ast.LocRef, opts fnOpts) (fn ast.Fn, hadBody bool } args = append(args, ast.Arg{ - Binding: arg, - Default: defaultValue, + TSDecorators: tsDecorators, + Binding: arg, + Default: defaultValue, // We need to track this because it affects code generation IsTypeScriptCtorField: isTypeScriptField, @@ -3952,52 +3999,40 @@ func (p *parser) parseClassStmt(loc ast.Loc, opts parseStmtOpts) ast.Stmt { p.skipTypeScriptTypeParameters() } - class := p.parseClass(name, parseClassOpts{isTypeScriptDeclare: opts.isTypeScriptDeclare}) + classOpts := parseClassOpts{ + allowTSDecorators: true, + isTypeScriptDeclare: opts.isTypeScriptDeclare, + } + if opts.tsDecorators != nil { + classOpts.tsDecorators = opts.tsDecorators.values + } + class := p.parseClass(name, classOpts) return ast.Stmt{loc, &ast.SClass{class, opts.isExport}} } -func (p *parser) parseAndSkipDecorators() { - for p.lexer.Token == lexer.TAt { - r := p.lexer.Range() - p.lexer.Next() - - // Eat an identifier - r.Len = p.lexer.Range().End() - r.Loc.Start - p.lexer.Expect(lexer.TIdentifier) - - // Eat an property access chain - for p.lexer.Token == lexer.TDot { - p.lexer.Next() - r.Len = p.lexer.Range().End() - r.Loc.Start - p.lexer.Expect(lexer.TIdentifier) - } - - // Eat call expression arguments - if p.lexer.Token == lexer.TOpenParen { +func (p *parser) parseTSDecorators() []ast.Expr { + var tsDecorators []ast.Expr + if p.ts.Parse { + for p.lexer.Token == lexer.TAt { p.lexer.Next() - depth := 0 - - // Skip to the matching close parenthesis. This doesn't have to be super - // accurate because we're in error recovery mode. - for p.lexer.Token != lexer.TEndOfFile && (p.lexer.Token != lexer.TCloseParen || depth > 0) { - switch p.lexer.Token { - case lexer.TOpenParen: - depth++ - case lexer.TCloseParen: - depth-- - } - p.lexer.Next() - } - r.Len = p.lexer.Range().End() - r.Loc.Start - p.lexer.Expect(lexer.TCloseParen) + // Parse a new/call expression with "exprFlagTSDecorator" so we ignore + // EIndex expressions, since they may be part of a computed property: + // + // class Foo { + // @foo ['computed']() {} + // } + // + // This matches the behavior of the TypeScript compiler. + tsDecorators = append(tsDecorators, p.parseExprWithFlags(ast.LNew, exprFlagTSDecorator)) } - - p.addRangeError(r, "Decorators are not supported yet") } + return tsDecorators } type parseClassOpts struct { + tsDecorators []ast.Expr + allowTSDecorators bool isTypeScriptDeclare bool } @@ -4037,9 +4072,6 @@ func (p *parser) parseClass(name *ast.LocRef, classOpts parseClassOpts) ast.Clas bodyLoc := p.lexer.Loc() p.lexer.Expect(lexer.TOpenBrace) properties := []ast.Property{} - opts := propertyOpts{ - classHasExtends: extends != nil, - } // Allow "in" and private fields inside class bodies oldAllowIn := p.allowIn @@ -4056,13 +4088,19 @@ func (p *parser) parseClass(name *ast.LocRef, classOpts parseClassOpts) ast.Clas continue } - // Skip past decorators and recover even though they aren't supported - if p.ts.Parse { - p.parseAndSkipDecorators() + opts := propertyOpts{ + isClass: true, + allowTSDecorators: classOpts.allowTSDecorators, + classHasExtends: extends != nil, + } + + // Parse decorators for this property + if opts.allowTSDecorators { + opts.tsDecorators = p.parseTSDecorators() } // This property may turn out to be a type in TypeScript, which should be ignored - if property, ok := p.parseProperty(propertyContextClass, ast.PropertyNormal, opts, nil); ok { + if property, ok := p.parseProperty(ast.PropertyNormal, opts, nil); ok { properties = append(properties, property) } } @@ -4078,7 +4116,7 @@ func (p *parser) parseClass(name *ast.LocRef, classOpts parseClassOpts) ast.Clas p.allowPrivateIdentifiers = oldAllowPrivateIdentifiers p.lexer.Expect(lexer.TCloseBrace) - return ast.Class{name, extends, bodyLoc, properties} + return ast.Class{classOpts.tsDecorators, name, extends, bodyLoc, properties} } func (p *parser) parseLabelName() *ast.LocRef { @@ -4166,7 +4204,16 @@ func (p *parser) parseFnStmt(loc ast.Loc, opts parseStmtOpts, isAsync bool, asyn return ast.Stmt{loc, &ast.SFunction{fn, opts.isExport}} } +type deferredTSDecorators struct { + values []ast.Expr + + // If this turns out to be a "declare class" statement, we need to undo the + // scopes that were potentially pushed while parsing the decorator arguments. + scopeIndex int +} + type parseStmtOpts struct { + tsDecorators *deferredTSDecorators allowLexicalDecl bool isModuleScope bool isNamespaceScope bool @@ -4191,6 +4238,18 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt { } p.lexer.Next() + // TypeScript decorators only work on class declarations + // "@decorator export class Foo {}" + // "@decorator export abstract class Foo {}" + // "@decorator export default class Foo {}" + // "@decorator export default abstract class Foo {}" + // "@decorator export declare class Foo {}" + // "@decorator export declare abstract class Foo {}" + if opts.tsDecorators != nil && p.lexer.Token != lexer.TClass && p.lexer.Token != lexer.TDefault && + !p.lexer.IsContextualKeyword("abstract") && !p.lexer.IsContextualKeyword("declare") { + p.lexer.Expected(lexer.TClass) + } + switch p.lexer.Token { case lexer.TClass, lexer.TConst, lexer.TFunction, lexer.TLet, lexer.TVar: opts.isExport = true @@ -4268,6 +4327,13 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt { p.currentScope.Generated = append(p.currentScope.Generated, defaultName.Ref) p.lexer.Next() + // TypeScript decorators only work on class declarations + // "@decorator export default class Foo {}" + // "@decorator export default abstract class Foo {}" + if opts.tsDecorators != nil && p.lexer.Token != lexer.TClass && !p.lexer.IsContextualKeyword("abstract") { + p.lexer.Expected(lexer.TClass) + } + if p.lexer.IsContextualKeyword("async") { asyncRange := p.lexer.Range() p.lexer.Next() @@ -4282,23 +4348,19 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt { return stmt // This was just a type annotation } - // Use the statement name if present, since it's a better name - if s, ok := stmt.Data.(*ast.SFunction); ok && s.Fn.Name != nil { - defaultName.Ref = s.Fn.Name.Ref - } - p.recordExport(defaultName.Loc, "default", defaultName.Ref) return ast.Stmt{loc, &ast.SExportDefault{defaultName, ast.ExprOrStmt{Stmt: &stmt}}} } p.recordExport(defaultName.Loc, "default", defaultName.Ref) - expr := p.parseSuffix(p.parseAsyncPrefixExpr(asyncRange), ast.LComma, nil) + expr := p.parseSuffix(p.parseAsyncPrefixExpr(asyncRange), ast.LComma, nil, 0) p.lexer.ExpectOrInsertSemicolon() return ast.Stmt{loc, &ast.SExportDefault{defaultName, ast.ExprOrStmt{Expr: &expr}}} } if p.lexer.Token == lexer.TFunction || p.lexer.Token == lexer.TClass || p.lexer.Token == lexer.TInterface { stmt := p.parseStmt(parseStmtOpts{ + tsDecorators: opts.tsDecorators, isNameOptional: true, allowLexicalDecl: true, }) @@ -4306,18 +4368,6 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt { return stmt // This was just a type annotation } - // Use the statement name if present, since it's a better name - switch s := stmt.Data.(type) { - case *ast.SFunction: - if s.Fn.Name != nil { - defaultName.Ref = s.Fn.Name.Ref - } - case *ast.SClass: - if s.Class.Name != nil { - defaultName.Ref = s.Class.Name.Ref - } - } - p.recordExport(defaultName.Loc, "default", defaultName.Ref) return ast.Stmt{loc, &ast.SExportDefault{defaultName, ast.ExprOrStmt{Stmt: &stmt}}} } @@ -4327,9 +4377,12 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt { expr := p.parseExpr(ast.LComma) // Handle the default export of an abstract class in TypeScript - if p.ts.Parse && isIdentifier && name == "abstract" && p.lexer.Token == lexer.TClass { - if _, ok := expr.Data.(*ast.EIdentifier); ok { - stmt := p.parseClassStmt(loc, parseStmtOpts{isNameOptional: true}) + if p.ts.Parse && isIdentifier && name == "abstract" { + if _, ok := expr.Data.(*ast.EIdentifier); ok && (p.lexer.Token == lexer.TClass || opts.tsDecorators != nil) { + stmt := p.parseClassStmt(loc, parseStmtOpts{ + tsDecorators: opts.tsDecorators, + isNameOptional: true, + }) // Use the statement name if present, since it's a better name if s, ok := stmt.Data.(*ast.SClass); ok && s.Class.Name != nil { @@ -4440,15 +4493,40 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt { return ast.Stmt{loc, &ast.STypeScript{}} case lexer.TAt: - // Skip past decorators and recover even though they aren't supported + // Parse decorators before class statements, which are potentially exported if p.ts.Parse { - p.parseAndSkipDecorators() - if p.lexer.Token == lexer.TExport { - p.lexer.Next() - } - if p.lexer.Token != lexer.TClass { + scopeIndex := len(p.scopesInOrder) + tsDecorators := p.parseTSDecorators() + + // If this turns out to be a "declare class" statement, we need to undo the + // scopes that were potentially pushed while parsing the decorator arguments. + // That can look like any one of the following: + // + // "@decorator declare class Foo {}" + // "@decorator declare abstract class Foo {}" + // "@decorator export declare class Foo {}" + // "@decorator export declare abstract class Foo {}" + // + opts.tsDecorators = &deferredTSDecorators{ + values: tsDecorators, + scopeIndex: scopeIndex, + } + + // "@decorator class Foo {}" + // "@decorator abstract class Foo {}" + // "@decorator declare class Foo {}" + // "@decorator declare abstract class Foo {}" + // "@decorator export class Foo {}" + // "@decorator export abstract class Foo {}" + // "@decorator export declare class Foo {}" + // "@decorator export declare abstract class Foo {}" + // "@decorator export default class Foo {}" + // "@decorator export default abstract class Foo {}" + if p.lexer.Token != lexer.TClass && p.lexer.Token != lexer.TExport && + !p.lexer.IsContextualKeyword("abstract") && !p.lexer.IsContextualKeyword("declare") { p.lexer.Expected(lexer.TClass) } + return p.parseStmt(opts) } @@ -4794,7 +4872,7 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt { case lexer.TOpenParen, lexer.TDot: // "import('path')" // "import.meta" - expr := p.parseSuffix(p.parseImportExpr(loc), ast.LLowest, nil) + expr := p.parseSuffix(p.parseImportExpr(loc), ast.LLowest, nil, 0) p.lexer.ExpectOrInsertSemicolon() return ast.Stmt{loc, &ast.SExpr{expr}} @@ -5053,14 +5131,14 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt { p.lexer.Next() return p.parseFnStmt(asyncRange.Loc, opts, true /* isAsync */, asyncRange) } - expr = p.parseSuffix(p.parseAsyncPrefixExpr(asyncRange), ast.LLowest, nil) + expr = p.parseSuffix(p.parseAsyncPrefixExpr(asyncRange), ast.LLowest, nil, 0) } else { expr = p.parseExpr(ast.LLowest) } if isIdentifier { if ident, ok := expr.Data.(*ast.EIdentifier); ok { - if p.lexer.Token == lexer.TColon { + if p.lexer.Token == lexer.TColon && opts.tsDecorators == nil { p.pushScopeForParsePass(ast.ScopeLabel, loc) defer p.popScope() @@ -5091,7 +5169,7 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt { } case "abstract": - if p.lexer.Token == lexer.TClass { + if p.lexer.Token == lexer.TClass || opts.tsDecorators != nil { return p.parseClassStmt(loc, opts) } @@ -5099,6 +5177,12 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt { opts.allowLexicalDecl = true opts.isTypeScriptDeclare = true + // "@decorator declare class Foo {}" + // "@decorator declare abstract class Foo {}" + if opts.tsDecorators != nil && p.lexer.Token != lexer.TClass && !p.lexer.IsContextualKeyword("abstract") { + p.lexer.Expected(lexer.TClass) + } + // "declare global { ... }" if p.lexer.IsContextualKeyword("global") { p.lexer.Next() @@ -5110,6 +5194,9 @@ func (p *parser) parseStmt(opts parseStmtOpts) ast.Stmt { // "declare const x: any" p.parseStmt(opts) + if opts.tsDecorators != nil { + p.discardScopesUpTo(opts.tsDecorators.scopeIndex) + } return ast.Stmt{loc, &ast.STypeScript{}} } } @@ -5980,7 +6067,8 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) } // We always lower class fields when parsing TypeScript since class fields in - // TypeScript don't follow the JavaScript spec. + // TypeScript don't follow the JavaScript spec. We also need to always lower + // TypeScript-style decorators since they don't have a JavaScript equivalent. if !p.ts.Parse && p.target >= ESNext { if kind == classKindExpr { return nil, expr @@ -5997,6 +6085,8 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) // These expressions are generated after the class body, in this order var computedPropertyCache ast.Expr var staticFields []ast.Expr + var instanceDecorators []ast.Expr + var staticDecorators []ast.Expr // These are only for class expressions that need to be captured var nameFunc func() ast.Expr @@ -6020,15 +6110,34 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) } for _, prop := range class.Properties { + // Merge parameter decorators with method decorators + if p.ts.Parse && prop.IsMethod { + if fn, ok := prop.Value.Data.(*ast.EFunction); ok { + for i, arg := range fn.Fn.Args { + for _, decorator := range arg.TSDecorators { + // Generate a call to "__param()" for this parameter decorator + prop.TSDecorators = append(prop.TSDecorators, + p.callRuntime(decorator.Loc, "__param", []ast.Expr{ + ast.Expr{decorator.Loc, &ast.ENumber{float64(i)}}, + decorator, + }), + ) + } + } + } + } + // Make sure the order of computed property keys doesn't change. These // expressions have side effects and must be evaluated in order. - if prop.IsComputed && (p.ts.Parse || computedPropertyCache.Data != nil || (!prop.IsMethod && p.target < ESNext)) { + keyExprNoSideEffects := prop.Key + if prop.IsComputed && (p.ts.Parse || computedPropertyCache.Data != nil || + (!prop.IsMethod && p.target < ESNext) || len(prop.TSDecorators) > 0) { needsKey := true // The TypeScript class field transform requires removing fields without // initializers. If the field is removed, then we only need the key for // its side effects and we don't need a temporary reference for the key. - if prop.IsMethod || (p.ts.Parse && prop.Initializer == nil) { + if len(prop.TSDecorators) == 0 && (prop.IsMethod || (p.ts.Parse && prop.Initializer == nil)) { needsKey = false } @@ -6044,6 +6153,7 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) Right: prop.Key, }}) prop.Key = ast.Expr{prop.Key.Loc, &ast.EIdentifier{ref}} + keyExprNoSideEffects = prop.Key } // If this is a computed method, the property value will be used @@ -6055,6 +6165,51 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) } } + // Handle decorators + if p.ts.Parse { + // Generate a single call to "__decorate()" for this property + if len(prop.TSDecorators) > 0 { + loc := prop.Key.Loc + + // Clone the key for the property descriptor + var descriptorKey ast.Expr + switch k := keyExprNoSideEffects.Data.(type) { + case *ast.ENumber: + descriptorKey = ast.Expr{loc, &ast.ENumber{k.Value}} + case *ast.EString: + descriptorKey = ast.Expr{loc, &ast.EString{k.Value}} + case *ast.EIdentifier: + descriptorKey = ast.Expr{loc, &ast.EIdentifier{k.Ref}} + default: + panic("Internal error") + } + + // This code tells "__decorate()" if the descriptor should be undefined + descriptorKind := float64(1) + if !prop.IsMethod { + descriptorKind = 2 + } + + decorator := p.callRuntime(loc, "__decorate", []ast.Expr{ + ast.Expr{loc, &ast.EArray{Items: prop.TSDecorators}}, + ast.Expr{loc, &ast.EDot{ + Target: nameFunc(), + Name: "prototype", + NameLoc: loc, + }}, + descriptorKey, + ast.Expr{loc, &ast.ENumber{descriptorKind}}, + }) + + // Static decorators are grouped after instance decorators + if prop.IsStatic { + staticDecorators = append(staticDecorators, decorator) + } else { + instanceDecorators = append(instanceDecorators, decorator) + } + } + } + // Instance and static fields are a JavaScript feature if (p.ts.Parse || p.target < ESNext) && !prop.IsMethod && (prop.IsStatic || prop.Value == nil) { _, isPrivateField := prop.Key.Data.(*ast.EPrivateIdentifier) @@ -6209,7 +6364,8 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) } } - // Pack the class back into an expression + // Pack the class back into an expression. We don't need to handle TypeScript + // decorators for class expressions because TypeScript doesn't support them. if kind == classKindExpr { // Initialize any remaining computed properties immediately after the end // of the class body @@ -6233,16 +6389,31 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) // Pack the class back into a statement, with potentially some extra // statements afterwards var stmts []ast.Stmt - switch kind { - case classKindStmt: - stmts = append(stmts, ast.Stmt{classLoc, &ast.SClass{Class: *class}}) - case classKindExportStmt: - stmts = append(stmts, ast.Stmt{classLoc, &ast.SClass{Class: *class, IsExport: true}}) - case classKindExportDefaultStmt: - stmts = append(stmts, ast.Stmt{classLoc, &ast.SExportDefault{ - DefaultName: defaultName, - Value: ast.ExprOrStmt{Stmt: &ast.Stmt{classLoc, &ast.SClass{Class: *class}}}, + if len(class.TSDecorators) > 0 { + name := nameFunc() + id, _ := name.Data.(*ast.EIdentifier) + classExpr := ast.EClass{Class: *class} + class = &classExpr.Class + stmts = append(stmts, ast.Stmt{classLoc, &ast.SLocal{ + Kind: ast.LocalLet, + IsExport: kind == classKindExportStmt, + Decls: []ast.Decl{ast.Decl{ + Binding: ast.Binding{name.Loc, &ast.BIdentifier{id.Ref}}, + Value: &ast.Expr{classLoc, &classExpr}, + }}, }}) + } else { + switch kind { + case classKindStmt: + stmts = append(stmts, ast.Stmt{classLoc, &ast.SClass{Class: *class}}) + case classKindExportStmt: + stmts = append(stmts, ast.Stmt{classLoc, &ast.SClass{Class: *class, IsExport: true}}) + case classKindExportDefaultStmt: + stmts = append(stmts, ast.Stmt{classLoc, &ast.SExportDefault{ + DefaultName: defaultName, + Value: ast.ExprOrStmt{Stmt: &ast.Stmt{classLoc, &ast.SClass{Class: *class}}}, + }}) + } } // The official TypeScript compiler adds generated code after the class body @@ -6253,7 +6424,30 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) for _, expr := range staticFields { stmts = append(stmts, ast.Stmt{expr.Loc, &ast.SExpr{expr}}) } - + for _, expr := range instanceDecorators { + stmts = append(stmts, ast.Stmt{expr.Loc, &ast.SExpr{expr}}) + } + for _, expr := range staticDecorators { + stmts = append(stmts, ast.Stmt{expr.Loc, &ast.SExpr{expr}}) + } + if len(class.TSDecorators) > 0 { + stmts = append(stmts, ast.Stmt{expr.Loc, &ast.SExpr{ast.Expr{classLoc, &ast.EBinary{ + Op: ast.BinOpAssign, + Left: nameFunc(), + Right: p.callRuntime(classLoc, "__decorate", []ast.Expr{ + ast.Expr{classLoc, &ast.EArray{Items: class.TSDecorators}}, + nameFunc(), + }), + }}}}) + if kind == classKindExportDefaultStmt { + name := nameFunc() + stmts = append(stmts, ast.Stmt{classLoc, &ast.SExportDefault{ + DefaultName: defaultName, + Value: ast.ExprOrStmt{Expr: &name}, + }}) + } + class.Name = nil + } return stmts, ast.Expr{} } @@ -7444,7 +7638,16 @@ func (p *parser) exprForExportedBindingInNamespace(binding ast.Binding, value as } } +func (p *parser) visitTSDecorators(tsDecorators []ast.Expr) []ast.Expr { + for i, decorator := range tsDecorators { + tsDecorators[i] = p.visitExpr(decorator) + } + return tsDecorators +} + func (p *parser) visitClass(class *ast.Class) { + class.TSDecorators = p.visitTSDecorators(class.TSDecorators) + if class.Extends != nil { *class.Extends = p.visitExpr(*class.Extends) } @@ -7457,6 +7660,8 @@ func (p *parser) visitClass(class *ast.Class) { defer p.popScope() for i, property := range class.Properties { + property.TSDecorators = p.visitTSDecorators(property.TSDecorators) + // Special-case EPrivateIdentifier to allow it here if _, ok := property.Key.Data.(*ast.EPrivateIdentifier); !ok { class.Properties[i].Key = p.visitExpr(property.Key) @@ -7474,6 +7679,7 @@ func (p *parser) visitClass(class *ast.Class) { func (p *parser) visitArgs(args []ast.Arg) { for _, arg := range args { + arg.TSDecorators = p.visitTSDecorators(arg.TSDecorators) p.visitBinding(arg.Binding) if arg.Default != nil { *arg.Default = p.visitExpr(*arg.Default) diff --git a/internal/parser/parser_ts_test.go b/internal/parser/parser_ts_test.go index 33bbded8e28..5544275f2c5 100644 --- a/internal/parser/parser_ts_test.go +++ b/internal/parser/parser_ts_test.go @@ -940,36 +940,54 @@ func TestTSDeclare(t *testing.T) { } func TestTSDecorator(t *testing.T) { - expectParseErrorTS(t, "@Dec @Dec class Foo {}", - ": error: Decorators are not supported yet\n"+ - ": error: Decorators are not supported yet\n") - expectParseErrorTS(t, "@Dec @Dec export class Foo {}", - ": error: Decorators are not supported yet\n"+ - ": error: Decorators are not supported yet\n") - expectParseErrorTS(t, "class Foo { @Dec foo() {} @Dec bar() {} }", - ": error: Decorators are not supported yet\n"+ - ": error: Decorators are not supported yet\n") - expectParseErrorTS(t, "class Foo { foo(@Dec x, @Dec y) {} }", - ": error: Decorators are not supported yet\n"+ - ": error: Decorators are not supported yet\n") - - expectParseErrorTS(t, "@Dec(a(), b()) @Dec class Foo {}", - ": error: Decorators are not supported yet\n"+ - ": error: Decorators are not supported yet\n") - expectParseErrorTS(t, "@Dec(a(), b()) @Dec export class Foo {}", - ": error: Decorators are not supported yet\n"+ - ": error: Decorators are not supported yet\n") - expectParseErrorTS(t, "class Foo { @Dec(a(), b()) foo() {} @Dec bar() {} }", - ": error: Decorators are not supported yet\n"+ - ": error: Decorators are not supported yet\n") - expectParseErrorTS(t, "class Foo { foo(@Dec(a(), b()) x, @Dec y) {} }", - ": error: Decorators are not supported yet\n"+ - ": error: Decorators are not supported yet\n") - - expectParseErrorTS(t, "@Dec @Dec let x", - ": error: Decorators are not supported yet\n"+ - ": error: Decorators are not supported yet\n"+ - ": error: Expected \"class\" but found \"let\"\n") + // Tests of "declare class" + expectPrintedTS(t, "@dec(() => 0) declare class Foo {} {let foo}", "{\n let foo;\n}\n") + expectPrintedTS(t, "@dec(() => 0) declare abstract class Foo {} {let foo}", "{\n let foo;\n}\n") + expectPrintedTS(t, "@dec(() => 0) export declare class Foo {} {let foo}", "{\n let foo;\n}\n") + expectPrintedTS(t, "@dec(() => 0) export declare abstract class Foo {} {let foo}", "{\n let foo;\n}\n") + expectPrintedTS(t, "declare class Foo { @dec(() => 0) foo } {let foo}", "{\n let foo;\n}\n") + expectPrintedTS(t, "declare class Foo { @dec(() => 0) foo() } {let foo}", "{\n let foo;\n}\n") + expectPrintedTS(t, "declare class Foo { foo(@dec(() => 0) x) } {let foo}", "{\n let foo;\n}\n") + + // Decorators must only work on class statements + expectParseErrorTS(t, "@dec enum foo {}", ": error: Expected \"class\" but found \"enum\"\n") + expectParseErrorTS(t, "@dec namespace foo {}", ": error: Expected \"class\" but found \"namespace\"\n") + expectParseErrorTS(t, "@dec function foo() {}", ": error: Expected \"class\" but found \"function\"\n") + expectParseErrorTS(t, "@dec abstract", ": error: Expected \"class\" but found end of file\n") + expectParseErrorTS(t, "@dec declare: x", ": error: Expected \"class\" but found \":\"\n") + expectParseErrorTS(t, "@dec declare enum foo {}", ": error: Expected \"class\" but found \"enum\"\n") + expectParseErrorTS(t, "@dec declare namespace foo {}", ": error: Expected \"class\" but found \"namespace\"\n") + expectParseErrorTS(t, "@dec declare function foo()", ": error: Expected \"class\" but found \"function\"\n") + expectParseErrorTS(t, "@dec export {}", ": error: Expected \"class\" but found \"{\"\n") + expectParseErrorTS(t, "@dec export enum foo {}", ": error: Expected \"class\" but found \"enum\"\n") + expectParseErrorTS(t, "@dec export namespace foo {}", ": error: Expected \"class\" but found \"namespace\"\n") + expectParseErrorTS(t, "@dec export function foo() {}", ": error: Expected \"class\" but found \"function\"\n") + expectParseErrorTS(t, "@dec export default abstract", ": error: Expected \"class\" but found end of file\n") + expectParseErrorTS(t, "@dec export declare enum foo {}", ": error: Expected \"class\" but found \"enum\"\n") + expectParseErrorTS(t, "@dec export declare namespace foo {}", ": error: Expected \"class\" but found \"namespace\"\n") + expectParseErrorTS(t, "@dec export declare function foo()", ": error: Expected \"class\" but found \"function\"\n") + + // Decorators must be forbidden outside class statements + expectParseErrorTS(t, "(class { @dec foo })", ": error: Expected identifier but found \"@\"\n") + expectParseErrorTS(t, "(class { @dec foo() {} })", ": error: Expected identifier but found \"@\"\n") + expectParseErrorTS(t, "(class { foo(@dec x) {} })", ": error: Expected identifier but found \"@\"\n") + expectParseErrorTS(t, "({ @dec foo })", ": error: Expected identifier but found \"@\"\n") + expectParseErrorTS(t, "({ @dec foo() {} })", ": error: Expected identifier but found \"@\"\n") + expectParseErrorTS(t, "({ foo(@dec x) {} })", ": error: Expected identifier but found \"@\"\n") + + // Decorators aren't allowed with private names + expectParseErrorTS(t, "class Foo { @dec #foo }", ": error: Expected identifier but found \"#foo\"\n") + expectParseErrorTS(t, "class Foo { @dec #foo = 1 }", ": error: Expected identifier but found \"#foo\"\n") + expectParseErrorTS(t, "class Foo { @dec #foo() {} }", ": error: Expected identifier but found \"#foo\"\n") + expectParseErrorTS(t, "class Foo { @dec *#foo() {} }", ": error: Expected identifier but found \"#foo\"\n") + expectParseErrorTS(t, "class Foo { @dec async #foo() {} }", ": error: Expected identifier but found \"#foo\"\n") + expectParseErrorTS(t, "class Foo { @dec async* #foo() {} }", ": error: Expected identifier but found \"#foo\"\n") + expectParseErrorTS(t, "class Foo { @dec static #foo }", ": error: Expected identifier but found \"#foo\"\n") + expectParseErrorTS(t, "class Foo { @dec static #foo = 1 }", ": error: Expected identifier but found \"#foo\"\n") + expectParseErrorTS(t, "class Foo { @dec static #foo() {} }", ": error: Expected identifier but found \"#foo\"\n") + expectParseErrorTS(t, "class Foo { @dec static *#foo() {} }", ": error: Expected identifier but found \"#foo\"\n") + expectParseErrorTS(t, "class Foo { @dec static async #foo() {} }", ": error: Expected identifier but found \"#foo\"\n") + expectParseErrorTS(t, "class Foo { @dec static async* #foo() {} }", ": error: Expected identifier but found \"#foo\"\n") } func TestTSArrow(t *testing.T) { diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index d4be0c52d38..2e95e2a8dd1 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -4,6 +4,7 @@ const Code = ` let __defineProperty = Object.defineProperty let __hasOwnProperty = Object.prototype.hasOwnProperty let __getOwnPropertySymbols = Object.getOwnPropertySymbols + let __getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor let __propertyIsEnumerable = Object.prototype.propertyIsEnumerable export let __pow = Math.pow @@ -51,4 +52,75 @@ const Code = ` for (let name in all) __defineProperty(target, name, { get: all[name], enumerable: true }) } + + // For TypeScript decorators + // - kind === undefined: class + // - kind === 1: method, parameter + // - kind === 2: field + export let __decorate = (decorators, target, key, kind) => { + var result = kind > 1 ? void 0 : kind ? __getOwnPropertyDescriptor(target, key) : target + for (var i = decorators.length - 1, decorator; i >= 0; i--) + if (decorator = decorators[i]) + result = (kind ? decorator(target, key, result) : decorator(result)) || result + if (kind && result) + __defineProperty(target, key, result) + return result + } + export let __param = (index, decorator) => (target, key) => decorator(target, key, index) ` + +// The TypeScript decorator transform behaves similar to the official +// TypeScript compiler. +// +// One difference is that the "__decorate" function doesn't contain a reference +// to the non-existent "Reflect.decorate" function. This function was never +// standardized and checking for it is wasted code (as well as a potentially +// dangerous cause of unintentional behavior changes in the future). +// +// Another difference is that the "__decorate" function doesn't take in an +// optional property descriptor like it does in the official TypeScript +// compiler's support code. This appears to be a dead code path in the official +// support code that is only there for legacy reasons. +// +// Here are some examples of how esbuild's decorator transform works: +// +// ============================= Class decorator ============================== +// +// // TypeScript // JavaScript +// @dec let C = class { +// class C { }; +// } C = __decorate([ +// dec +// ], C); +// +// ============================ Method decorator ============================== +// +// // TypeScript // JavaScript +// class C { class C { +// @dec foo() {} +// foo() {} } +// } __decorate([ +// dec +// ], C.prototype, 'foo', 1); +// +// =========================== Parameter decorator ============================ +// +// // TypeScript // JavaScript +// class C { class C { +// foo(@dec bar) {} foo(bar) {} +// } } +// __decorate([ +// __param(0, dec) +// ], C.prototype, 'foo', 1); +// +// ============================= Field decorator ============================== +// +// // TypeScript // JavaScript +// class C { class C { +// @dec constructor() { +// foo = 123 this.foo = 123 +// } } +// } +// __decorate([ +// dec +// ], C.prototype, 'foo', 2);