Skip to content

Commit

Permalink
allow decorators to come after export (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Nov 23, 2023
1 parent 7baefdb commit 69c9e7f
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 6 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## Unreleased

* Allow decorators after the `export` keyword ([#104](https://github.com/evanw/esbuild/issues/104))

Previously esbuild's decorator parser followed the original behavior of TypeScript's experimental decorators feature, which only allowed decorators to come before the `export` keyword. However, the upcoming JavaScript decorators feature also allows decorators to come after the `export` keyword. And with TypeScript 5.0, TypeScript now also allows experimental decorators to come after the `export` keyword too. So esbuild now allows this as well:

```js
// This old syntax has always been permitted:
@decorator export class Foo {}
@decorator export default class Foo {}

// This new syntax is now permitted too:
export @decorator class Foo {}
export default @decorator class Foo {}
```

In addition, esbuild's decorator parser has been rewritten to fix several subtle and likely unimportant edge cases with esbuild's parsing of exports and decorators in TypeScript (e.g. TypeScript apparently does automatic semicolon insertion after `interface` and `export interface` but not after `export default interface`).

## 0.19.7

* Add support for bundling code that uses import attributes ([#3384](https://github.com/evanw/esbuild/issues/3384))
Expand Down
17 changes: 13 additions & 4 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -6735,7 +6735,7 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt {
p.lexer.Next()

switch p.lexer.Token {
case js_lexer.TClass, js_lexer.TConst, js_lexer.TFunction, js_lexer.TVar:
case js_lexer.TClass, js_lexer.TConst, js_lexer.TFunction, js_lexer.TVar, js_lexer.TAt:
opts.isExport = true
return p.parseStmt(opts)

Expand Down Expand Up @@ -6877,11 +6877,13 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt {

// "export default class {}"
// "export default class Foo {}"
// "export default @x class {}"
// "export default @x class Foo {}"
// "export default function() {}"
// "export default function foo() {}"
// "export default interface Foo {}"
// "export default interface + 1"
if p.lexer.Token == js_lexer.TFunction || p.lexer.Token == js_lexer.TClass ||
if p.lexer.Token == js_lexer.TFunction || p.lexer.Token == js_lexer.TClass || p.lexer.Token == js_lexer.TAt ||
(p.options.ts.Parse && p.lexer.IsContextualKeyword("interface")) {
stmt := p.parseStmt(parseStmtOpts{
deferredDecorators: opts.deferredDecorators,
Expand Down Expand Up @@ -6921,7 +6923,7 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt {
// "export default abstract class {}"
// "export default abstract class Foo {}"
if p.options.ts.Parse && isIdentifier && name == "abstract" && !p.lexer.HasNewlineBefore {
if _, ok := expr.Data.(*js_ast.EIdentifier); ok && (p.lexer.Token == js_lexer.TClass || opts.deferredDecorators != nil) {
if _, ok := expr.Data.(*js_ast.EIdentifier); ok && p.lexer.Token == js_lexer.TClass {
stmt := p.parseClassStmt(loc, parseStmtOpts{
deferredDecorators: opts.deferredDecorators,
isNameOptional: true,
Expand Down Expand Up @@ -7048,6 +7050,13 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt {
scopeIndex := len(p.scopesInOrder)
decorators := p.parseDecorators(p.currentScope, logger.Range{}, 0)

// "@x export @y class Foo {}"
if opts.deferredDecorators != nil {
p.log.AddError(&p.tracker, logger.Range{Loc: loc, Len: 1}, "Decorators are not valid here")
p.discardScopesUpTo(scopeIndex)
return p.parseStmt(opts)
}

// 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:
Expand Down Expand Up @@ -7825,7 +7834,7 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt {
}

case "abstract":
if !p.lexer.HasNewlineBefore && (p.lexer.Token == js_lexer.TClass || opts.deferredDecorators != nil) {
if !p.lexer.HasNewlineBefore && p.lexer.Token == js_lexer.TClass {
return p.parseClassStmt(loc, opts)
}

Expand Down
14 changes: 14 additions & 0 deletions internal/js_parser/js_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2050,6 +2050,20 @@ func TestDecorators(t *testing.T) {
// Check ASI for "abstract"
expectParseError(t, "@x abstract class Foo {}", "<stdin>: ERROR: Expected \";\" but found \"class\"\n")
expectParseError(t, "@x abstract\nclass Foo {}", "<stdin>: ERROR: Decorators are not valid here\n")

// Check decorator locations in relation to the "export" keyword
expectPrinted(t, "@x export class Foo {}", "@x\nexport class Foo {\n}\n")
expectPrinted(t, "export @x class Foo {}", "@x\nexport class Foo {\n}\n")
expectPrinted(t, "@x export default class {}", "@x\nexport default class {\n}\n")
expectPrinted(t, "export default @x class {}", "@x\nexport default class {\n}\n")
expectPrinted(t, "@x export default class Foo {}", "@x\nexport default class Foo {\n}\n")
expectPrinted(t, "export default @x class Foo {}", "@x\nexport default class Foo {\n}\n")
expectPrinted(t, "export default (@x class {})", "export default (@x class {\n});\n")
expectPrinted(t, "export default (@x class Foo {})", "export default (@x class Foo {\n});\n")
expectParseError(t, "export @x default class {}", "<stdin>: ERROR: Unexpected \"default\"\n")
expectParseError(t, "@x export @y class Foo {}", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseError(t, "@x export default abstract", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseError(t, "@x export @y default class {}", "<stdin>: ERROR: Decorators are not valid here\n<stdin>: ERROR: Unexpected \"default\"\n")
}

func TestGenerator(t *testing.T) {
Expand Down
34 changes: 32 additions & 2 deletions internal/js_parser/ts_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1874,7 +1874,7 @@ func TestTSExperimentalDecorator(t *testing.T) {
expectParseErrorExperimentalDecoratorTS(t, "@dec enum foo {}", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorExperimentalDecoratorTS(t, "@dec namespace foo {}", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorExperimentalDecoratorTS(t, "@dec function foo() {}", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorExperimentalDecoratorTS(t, "@dec abstract", "<stdin>: ERROR: Expected \"class\" but found end of file\n")
expectParseErrorExperimentalDecoratorTS(t, "@dec abstract", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorExperimentalDecoratorTS(t, "@dec declare: x", "<stdin>: ERROR: Unexpected \":\"\n")
expectParseErrorExperimentalDecoratorTS(t, "@dec declare enum foo {}", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorExperimentalDecoratorTS(t, "@dec declare namespace foo {}", "<stdin>: ERROR: Decorators are not valid here\n")
Expand All @@ -1883,7 +1883,7 @@ func TestTSExperimentalDecorator(t *testing.T) {
expectParseErrorExperimentalDecoratorTS(t, "@dec export enum foo {}", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorExperimentalDecoratorTS(t, "@dec export namespace foo {}", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorExperimentalDecoratorTS(t, "@dec export function foo() {}", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorExperimentalDecoratorTS(t, "@dec export default abstract", "<stdin>: ERROR: Expected \"class\" but found end of file\n")
expectParseErrorExperimentalDecoratorTS(t, "@dec export default abstract", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorExperimentalDecoratorTS(t, "@dec export declare enum foo {}", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorExperimentalDecoratorTS(t, "@dec export declare namespace foo {}", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorExperimentalDecoratorTS(t, "@dec export declare function foo()", "<stdin>: ERROR: Decorators are not valid here\n")
Expand Down Expand Up @@ -2013,6 +2013,22 @@ func TestTSExperimentalDecorator(t *testing.T) {
// Check ASI for "abstract"
expectPrintedExperimentalDecoratorTS(t, "@x abstract class Foo {}", "let Foo = class {\n};\nFoo = __decorateClass([\n x\n], Foo);\n")
expectParseErrorExperimentalDecoratorTS(t, "@x abstract\nclass Foo {}", "<stdin>: ERROR: Decorators are not valid here\n")

// Check decorator locations in relation to the "export" keyword
expectPrintedExperimentalDecoratorTS(t, "@x export class Foo {}", "export let Foo = class {\n};\nFoo = __decorateClass([\n x\n], Foo);\n")
expectPrintedExperimentalDecoratorTS(t, "export @x class Foo {}", "export let Foo = class {\n};\nFoo = __decorateClass([\n x\n], Foo);\n")
expectPrintedExperimentalDecoratorTS(t, "@x export default class {}",
"let stdin_default = class {\n};\nstdin_default = __decorateClass([\n x\n], stdin_default);\nexport {\n stdin_default as default\n};\n")
expectPrintedExperimentalDecoratorTS(t, "export default @x class {}",
"let stdin_default = class {\n};\nstdin_default = __decorateClass([\n x\n], stdin_default);\nexport {\n stdin_default as default\n};\n")
expectPrintedExperimentalDecoratorTS(t, "@x export default class Foo {}", "let Foo = class {\n};\nFoo = __decorateClass([\n x\n], Foo);\nexport {\n Foo as default\n};\n")
expectPrintedExperimentalDecoratorTS(t, "export default @x class Foo {}", "let Foo = class {\n};\nFoo = __decorateClass([\n x\n], Foo);\nexport {\n Foo as default\n};\n")
expectParseErrorExperimentalDecoratorTS(t, "export default (@x class {})", "<stdin>: ERROR: Experimental decorators cannot be used in expression position in TypeScript\n")
expectParseErrorExperimentalDecoratorTS(t, "export default (@x class Foo {})", "<stdin>: ERROR: Experimental decorators cannot be used in expression position in TypeScript\n")
expectParseErrorExperimentalDecoratorTS(t, "export @x default class {}", "<stdin>: ERROR: Unexpected \"default\"\n")
expectParseErrorExperimentalDecoratorTS(t, "@x export @y class Foo {}", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorExperimentalDecoratorTS(t, "@x export default abstract", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorExperimentalDecoratorTS(t, "@x export @y default class {}", "<stdin>: ERROR: Decorators are not valid here\n<stdin>: ERROR: Unexpected \"default\"\n")
}

func TestTSDecorators(t *testing.T) {
Expand Down Expand Up @@ -2074,6 +2090,20 @@ func TestTSDecorators(t *testing.T) {
// Check ASI for "abstract"
expectPrintedTS(t, "@x abstract class Foo {}", "@x\nclass Foo {\n}\n")
expectParseErrorTS(t, "@x abstract\nclass Foo {}", "<stdin>: ERROR: Decorators are not valid here\n")

// Check decorator locations in relation to the "export" keyword
expectPrintedTS(t, "@x export class Foo {}", "@x\nexport class Foo {\n}\n")
expectPrintedTS(t, "export @x class Foo {}", "@x\nexport class Foo {\n}\n")
expectPrintedTS(t, "@x export default class {}", "@x\nexport default class {\n}\n")
expectPrintedTS(t, "export default @x class {}", "@x\nexport default class {\n}\n")
expectPrintedTS(t, "@x export default class Foo {}", "@x\nexport default class Foo {\n}\n")
expectPrintedTS(t, "export default @x class Foo {}", "@x\nexport default class Foo {\n}\n")
expectPrintedTS(t, "export default (@x class {})", "export default (@x class {\n});\n")
expectPrintedTS(t, "export default (@x class Foo {})", "export default (@x class Foo {\n});\n")
expectParseErrorTS(t, "export @x default class {}", "<stdin>: ERROR: Unexpected \"default\"\n")
expectParseErrorTS(t, "@x export @y class Foo {}", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorTS(t, "@x export default abstract", "<stdin>: ERROR: Decorators are not valid here\n")
expectParseErrorTS(t, "@x export @y default class {}", "<stdin>: ERROR: Decorators are not valid here\n<stdin>: ERROR: Unexpected \"default\"\n")
}

func TestTSTry(t *testing.T) {
Expand Down

0 comments on commit 69c9e7f

Please sign in to comment.