Skip to content

Commit

Permalink
ts: fix await in parameter decorators (#2147)
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Apr 1, 2022
1 parent b4b7261 commit 85f7ffa
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 0 deletions.
50 changes: 50 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,55 @@
# Changelog

## Unreleased

* Change the context of TypeScript parameter decorators

While TypeScript parameter decorators are expressions, they are not evaluated where they exist in the code. They are moved to after the class declaration and evaluated there instead. Specifically this TypeScript code:

```ts
class Foo {
foo(@bar() baz) {}
}
```

becomes this JavaScript code:

```js
class Foo {
foo(baz) {}
}
__decorate([
__param(0, bar())
], Foo.prototype, "foo", null);
```

One consequence of this is that whether `await` is allowed or not depends on whether the class declaration itself is inside an `async` function or not. The TypeScript compiler allows code that does this:

```ts
async function fn(foo) {
class Foo {
foo(@bar(await foo) baz) {}
}
return Foo
}
```

because that becomes the following valid JavaScript:

```js
async function fn(foo) {
class Foo {
foo(baz) {}
}
__decorate([
__param(0, bar(await foo))
], Foo.prototype, "foo", null);
return Foo;
}
```

Previously using `await` like this wasn't allowed. With this release, esbuild now handles `await` correctly in TypeScript parameter decorators. Note that the TypeScript compiler currently has some bugs regarding this behavior: https://github.com/microsoft/TypeScript/issues/48509. Also while TypeScript currently allows `await` to be used like this in `async` functions, it doesn't currently allow `yield` to be used like this in generator functions. It's not yet clear whether this behavior with `yield` is a bug or by design, so I haven't made any changes to esbuild's handling of `yield` inside decorator expressions.

## 0.14.29

* Fix a minification bug with a double-nested `if` inside a label followed by `else` ([#2139](https://github.com/evanw/esbuild/issues/2139))
Expand Down
53 changes: 53 additions & 0 deletions internal/js_parser/js_parser.go
Expand Up @@ -5146,7 +5146,60 @@ func (p *parser) parseFn(name *js_ast.LocRef, classKeyword logger.Range, data fn

var tsDecorators []js_ast.Expr
if data.allowTSDecorators {
oldAwait := p.fnOrArrowDataParse.await
oldNeedsAsyncLoc := p.fnOrArrowDataParse.needsAsyncLoc

// While TypeScript parameter decorators are expressions, they are not
// evaluated where they exist in the code. They are moved to after the
// class declaration and evaluated there instead. Specifically this
// TypeScript code:
//
// class Foo {
// foo(@bar() baz) {}
// }
//
// becomes this JavaScript code:
//
// class Foo {
// foo(baz) {}
// }
// __decorate([
// __param(0, bar())
// ], Foo.prototype, "foo", null);
//
// One consequence of this is that whether "await" is allowed or not
// depends on whether the class declaration itself is inside an "async"
// function or not. The TypeScript compiler allows code that does this:
//
// async function fn(foo) {
// class Foo {
// foo(@bar(await foo) baz) {}
// }
// return Foo
// }
//
// because that becomes the following valid JavaScript:
//
// async function fn(foo) {
// class Foo {
// foo(baz) {}
// }
// __decorate([
// __param(0, bar(await foo))
// ], Foo.prototype, "foo", null);
// return Foo;
// }
//
if oldFnOrArrowData.await == allowExpr {
p.fnOrArrowDataParse.await = allowExpr
} else {
p.fnOrArrowDataParse.needsAsyncLoc = oldFnOrArrowData.needsAsyncLoc
}

tsDecorators = p.parseTypeScriptDecorators()

p.fnOrArrowDataParse.await = oldAwait
p.fnOrArrowDataParse.needsAsyncLoc = oldNeedsAsyncLoc
} else if classKeyword.Len > 0 {
p.logInvalidDecoratorError(classKeyword)
}
Expand Down
65 changes: 65 additions & 0 deletions internal/js_parser/ts_parser_test.go
Expand Up @@ -1457,6 +1457,71 @@ func TestTSDecorator(t *testing.T) {
// Decorators aren't allowed on class constructors
expectParseErrorTS(t, "class Foo { @dec constructor() {} }", "<stdin>: ERROR: TypeScript does not allow decorators on class constructors\n")
expectParseErrorTS(t, "class Foo { @dec public constructor() {} }", "<stdin>: ERROR: TypeScript does not allow decorators on class constructors\n")

// Check use of "await"
friendlyAwaitErrorWithNote := "<stdin>: ERROR: \"await\" can only be used inside an \"async\" function\n" +
"<stdin>: NOTE: Consider adding the \"async\" keyword here:\n"
expectPrintedTS(t, "async function foo() { @dec(await x) class Foo {} }",
"async function foo() {\n let Foo = class {\n };\n Foo = __decorateClass([\n dec(await x)\n ], Foo);\n}\n")
expectPrintedTS(t, "async function foo() { class Foo { @dec(await x) foo() {} } }",
"async function foo() {\n class Foo {\n foo() {\n }\n }\n __decorateClass([\n dec(await x)\n ], Foo.prototype, \"foo\", 1);\n}\n")
expectPrintedTS(t, "async function foo() { class Foo { foo(@dec(await x) y) {} } }",
"async function foo() {\n class Foo {\n foo(y) {\n }\n }\n __decorateClass([\n __decorateParam(0, dec(await x))\n ], Foo.prototype, \"foo\", 1);\n}\n")
expectParseErrorTS(t, "function foo() { @dec(await x) class Foo {} }", friendlyAwaitErrorWithNote)
expectParseErrorTS(t, "function foo() { class Foo { @dec(await x) foo() {} } }", friendlyAwaitErrorWithNote)
expectParseErrorTS(t, "function foo() { class Foo { foo(@dec(await x) y) {} } }", friendlyAwaitErrorWithNote)
expectParseErrorTS(t, "function foo() { class Foo { @dec(await x) async foo() {} } }", friendlyAwaitErrorWithNote)
expectParseErrorTS(t, "function foo() { class Foo { async foo(@dec(await x) y) {} } }",
"<stdin>: ERROR: The keyword \"await\" cannot be used here:\n<stdin>: ERROR: Expected \")\" but found \"x\"\n")

// Check lowered use of "await"
expectPrintedTargetTS(t, 2015, "async function foo() { @dec(await x) class Foo {} }",
`function foo() {
return __async(this, null, function* () {
let Foo = class {
};
Foo = __decorateClass([
dec(yield x)
], Foo);
});
}
`)
expectPrintedTargetTS(t, 2015, "async function foo() { class Foo { @dec(await x) foo() {} } }",
`function foo() {
return __async(this, null, function* () {
class Foo {
foo() {
}
}
__decorateClass([
dec(yield x)
], Foo.prototype, "foo", 1);
});
}
`)
expectPrintedTargetTS(t, 2015, "async function foo() { class Foo { foo(@dec(await x) y) {} } }",
`function foo() {
return __async(this, null, function* () {
class Foo {
foo(y) {
}
}
__decorateClass([
__decorateParam(0, dec(yield x))
], Foo.prototype, "foo", 1);
});
}
`)

// Check use of "yield"
expectPrintedTS(t, "function *foo() { @dec(yield x) class Foo {} }", // We currently allow this but TypeScript doesn't
"function* foo() {\n let Foo = class {\n };\n Foo = __decorateClass([\n dec(yield x)\n ], Foo);\n}\n")
expectPrintedTS(t, "function *foo() { class Foo { @dec(yield x) foo() {} } }", // We currently allow this but TypeScript doesn't
"function* foo() {\n class Foo {\n foo() {\n }\n }\n __decorateClass([\n dec(yield x)\n ], Foo.prototype, \"foo\", 1);\n}\n")
expectParseErrorTS(t, "function *foo() { class Foo { foo(@dec(yield x) y) {} } }", // TypeScript doesn't allow this (although it could because it would work fine)
"<stdin>: ERROR: Cannot use \"yield\" outside a generator function\n")
expectParseErrorTS(t, "function foo() { @dec(yield x) class Foo {} }", "<stdin>: ERROR: Cannot use \"yield\" outside a generator function\n")
expectParseErrorTS(t, "function foo() { class Foo { @dec(yield x) foo() {} } }", "<stdin>: ERROR: Cannot use \"yield\" outside a generator function\n")
}

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

0 comments on commit 85f7ffa

Please sign in to comment.