From add8e4ad13b4a44de409e283e99679a904012317 Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Fri, 23 Jun 2017 02:19:08 +0530 Subject: [PATCH 1/5] Helpful error message for @dec export class --- src/parser/statement.js | 5 +++++ .../decorators-2/no-export-decorators-on-class/options.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/parser/statement.js b/src/parser/statement.js index 2cbfa3b52c..5f40ddb277 100644 --- a/src/parser/statement.js +++ b/src/parser/statement.js @@ -173,6 +173,11 @@ export default class StatementParser extends ExpressionParser { return; } + // special error for the common case of @dec export class + if (!allowExport && this.match(tt._export)) { + this.raise(this.state.start, "Using the export keyword between decorators and class is disallowed. Please use `export @dec class` instead"); + } + if (!this.match(tt._class)) { this.raise(this.state.start, "Leading decorators must be attached to a class declaration"); } diff --git a/test/fixtures/experimental/decorators-2/no-export-decorators-on-class/options.json b/test/fixtures/experimental/decorators-2/no-export-decorators-on-class/options.json index 43922816e1..4c5c609080 100644 --- a/test/fixtures/experimental/decorators-2/no-export-decorators-on-class/options.json +++ b/test/fixtures/experimental/decorators-2/no-export-decorators-on-class/options.json @@ -1,4 +1,4 @@ { "sourceType": "module", - "throws": "Leading decorators must be attached to a class declaration (2:0)" + "throws": "Using the export keyword between decorators and class is disallowed. Please use `export @dec class` instead (2:0)" } From 37fa77e84f4ebe8f7fd6ea1fc3e79eae875652db Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Fri, 23 Jun 2017 17:30:06 +0530 Subject: [PATCH 2/5] Support decorator in decorator Fixes #524 --- src/parser/statement.js | 19 +- src/tokenizer/state.js | 7 +- .../decoratorception-class/actual.js | 6 + .../decoratorception-class/expected.json | 269 ++++++++++++++ .../decoratorception-method/actual.js | 9 + .../decoratorception-method/expected.json | 351 ++++++++++++++++++ 6 files changed, 652 insertions(+), 9 deletions(-) create mode 100644 test/fixtures/experimental/decorators-2/decoratorception-class/actual.js create mode 100644 test/fixtures/experimental/decorators-2/decoratorception-class/expected.json create mode 100644 test/fixtures/experimental/decorators-2/decoratorception-method/actual.js create mode 100644 test/fixtures/experimental/decorators-2/decoratorception-method/expected.json diff --git a/src/parser/statement.js b/src/parser/statement.js index 5f40ddb277..b3d7e1a28b 100644 --- a/src/parser/statement.js +++ b/src/parser/statement.js @@ -150,12 +150,13 @@ export default class StatementParser extends ExpressionParser { } takeDecorators(node: N.HasDecorators): void { - if (this.state.decorators.length) { - node.decorators = this.state.decorators; + const decorators = this.state.decoratorStack[this.state.decoratorStack.length - 1]; + if (decorators.length) { + node.decorators = decorators; if (this.hasPlugin("decorators2")) { - this.resetStartLocationFromNode(node, this.state.decorators[0]); + this.resetStartLocationFromNode(node, decorators[0]); } - this.state.decorators = []; + this.state.decoratorStack[this.state.decoratorStack.length - 1] = []; } } @@ -164,9 +165,10 @@ export default class StatementParser extends ExpressionParser { allowExport = false; } + const currentContextDecorators = this.state.decoratorStack[this.state.decoratorStack.length - 1]; while (this.match(tt.at)) { const decorator = this.parseDecorator(); - this.state.decorators.push(decorator); + currentContextDecorators.push(decorator); } if (allowExport && this.match(tt._export)) { @@ -207,7 +209,11 @@ export default class StatementParser extends ExpressionParser { if (this.eat(tt.parenL)) { const node = this.startNodeAt(startPos, startLoc); node.callee = expr; + // Every time a decorator class expression is evaluated, a new empty array is pushed onto the stack + // So that the decorators of any nested class expressions will be dealt with separately + this.state.decoratorStack.push([]); node.arguments = this.parseCallExpressionArguments(tt.parenR, false); + this.state.decoratorStack.pop(); expr = this.finishNode(node, "CallExpression"); this.toReferencedList(expr.arguments); } @@ -1043,7 +1049,8 @@ export default class StatementParser extends ExpressionParser { } } - if (this.state.decorators.length) { + const currentContextDecorators = this.state.decoratorStack[this.state.decoratorStack.length - 1]; + if (currentContextDecorators.length) { const isClass = node.declaration && (node.declaration.type === "ClassDeclaration" || node.declaration.type === "ClassExpression"); if (!node.declaration || !isClass) { throw this.raise(node.start, "You can only use decorators on an export when exporting a class"); diff --git a/src/tokenizer/state.js b/src/tokenizer/state.js index 0a16182af8..31512c12f6 100644 --- a/src/tokenizer/state.js +++ b/src/tokenizer/state.js @@ -31,7 +31,7 @@ export default class State { this.labels = []; - this.decorators = []; + this.decoratorStack = [[]]; this.tokens = []; @@ -89,8 +89,9 @@ export default class State { // Labels in scope. labels: Array<{ kind: ?("loop" | "switch"), statementStart?: number }>; - // Leading decorators. - decorators: Array; + // Leading decorators. Last element of the stack represents the decorators in current context. + // Supports nesting of decorators, e.g. @foo(@bar class {}) class {} + decoratorStack: Array>; // Token store. tokens: Array; diff --git a/test/fixtures/experimental/decorators-2/decoratorception-class/actual.js b/test/fixtures/experimental/decorators-2/decoratorception-class/actual.js new file mode 100644 index 0000000000..c2f38e56ba --- /dev/null +++ b/test/fixtures/experimental/decorators-2/decoratorception-class/actual.js @@ -0,0 +1,6 @@ +@outer({ + store: @inner class Foo {} +}) +class Bar { + +} diff --git a/test/fixtures/experimental/decorators-2/decoratorception-class/expected.json b/test/fixtures/experimental/decorators-2/decoratorception-class/expected.json new file mode 100644 index 0000000000..b18168fe0d --- /dev/null +++ b/test/fixtures/experimental/decorators-2/decoratorception-class/expected.json @@ -0,0 +1,269 @@ +{ + "type": "File", + "start": 0, + "end": 57, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 6, + "column": 1 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 57, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 6, + "column": 1 + } + }, + "sourceType": "script", + "body": [ + { + "type": "ClassDeclaration", + "start": 0, + "end": 57, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 6, + "column": 1 + } + }, + "decorators": [ + { + "type": "Decorator", + "start": 0, + "end": 40, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 2 + } + }, + "expression": { + "type": "CallExpression", + "start": 1, + "end": 40, + "loc": { + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 3, + "column": 2 + } + }, + "callee": { + "type": "Identifier", + "start": 1, + "end": 6, + "loc": { + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 6 + }, + "identifierName": "outer" + }, + "name": "outer" + }, + "arguments": [ + { + "type": "ObjectExpression", + "start": 7, + "end": 39, + "loc": { + "start": { + "line": 1, + "column": 7 + }, + "end": { + "line": 3, + "column": 1 + } + }, + "properties": [ + { + "type": "ObjectProperty", + "start": 11, + "end": 37, + "loc": { + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 28 + } + }, + "method": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 11, + "end": 16, + "loc": { + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 7 + }, + "identifierName": "store" + }, + "name": "store" + }, + "shorthand": false, + "value": { + "type": "ClassExpression", + "start": 18, + "end": 37, + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 28 + } + }, + "decorators": [ + { + "type": "Decorator", + "start": 18, + "end": 24, + "loc": { + "start": { + "line": 2, + "column": 9 + }, + "end": { + "line": 2, + "column": 15 + } + }, + "expression": { + "type": "Identifier", + "start": 19, + "end": 24, + "loc": { + "start": { + "line": 2, + "column": 10 + }, + "end": { + "line": 2, + "column": 15 + }, + "identifierName": "inner" + }, + "name": "inner" + } + } + ], + "id": { + "type": "Identifier", + "start": 31, + "end": 34, + "loc": { + "start": { + "line": 2, + "column": 22 + }, + "end": { + "line": 2, + "column": 25 + }, + "identifierName": "Foo" + }, + "name": "Foo" + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 35, + "end": 37, + "loc": { + "start": { + "line": 2, + "column": 26 + }, + "end": { + "line": 2, + "column": 28 + } + }, + "body": [] + } + } + } + ] + } + ] + } + } + ], + "id": { + "type": "Identifier", + "start": 47, + "end": 50, + "loc": { + "start": { + "line": 4, + "column": 6 + }, + "end": { + "line": 4, + "column": 9 + }, + "identifierName": "Bar" + }, + "name": "Bar" + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 51, + "end": 57, + "loc": { + "start": { + "line": 4, + "column": 10 + }, + "end": { + "line": 6, + "column": 1 + } + }, + "body": [] + } + } + ], + "directives": [] + } +} \ No newline at end of file diff --git a/test/fixtures/experimental/decorators-2/decoratorception-method/actual.js b/test/fixtures/experimental/decorators-2/decoratorception-method/actual.js new file mode 100644 index 0000000000..eb9f1b073c --- /dev/null +++ b/test/fixtures/experimental/decorators-2/decoratorception-method/actual.js @@ -0,0 +1,9 @@ +class Bar{ + @outer( + @classDec class { + @inner + innerMethod() {} + } + ) + outerMethod() {} +} diff --git a/test/fixtures/experimental/decorators-2/decoratorception-method/expected.json b/test/fixtures/experimental/decorators-2/decoratorception-method/expected.json new file mode 100644 index 0000000000..472e83b492 --- /dev/null +++ b/test/fixtures/experimental/decorators-2/decoratorception-method/expected.json @@ -0,0 +1,351 @@ +{ + "type": "File", + "start": 0, + "end": 112, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 9, + "column": 1 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 112, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 9, + "column": 1 + } + }, + "sourceType": "script", + "body": [ + { + "type": "ClassDeclaration", + "start": 0, + "end": 112, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 9, + "column": 1 + } + }, + "id": { + "type": "Identifier", + "start": 6, + "end": 9, + "loc": { + "start": { + "line": 1, + "column": 6 + }, + "end": { + "line": 1, + "column": 9 + }, + "identifierName": "Bar" + }, + "name": "Bar" + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 9, + "end": 112, + "loc": { + "start": { + "line": 1, + "column": 9 + }, + "end": { + "line": 9, + "column": 1 + } + }, + "body": [ + { + "type": "ClassMethod", + "start": 13, + "end": 110, + "loc": { + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 8, + "column": 18 + } + }, + "decorators": [ + { + "type": "Decorator", + "start": 13, + "end": 91, + "loc": { + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 7, + "column": 3 + } + }, + "expression": { + "type": "CallExpression", + "start": 14, + "end": 91, + "loc": { + "start": { + "line": 2, + "column": 3 + }, + "end": { + "line": 7, + "column": 3 + } + }, + "callee": { + "type": "Identifier", + "start": 14, + "end": 19, + "loc": { + "start": { + "line": 2, + "column": 3 + }, + "end": { + "line": 2, + "column": 8 + }, + "identifierName": "outer" + }, + "name": "outer" + }, + "arguments": [ + { + "type": "ClassExpression", + "start": 25, + "end": 87, + "loc": { + "start": { + "line": 3, + "column": 4 + }, + "end": { + "line": 6, + "column": 5 + } + }, + "decorators": [ + { + "type": "Decorator", + "start": 25, + "end": 34, + "loc": { + "start": { + "line": 3, + "column": 4 + }, + "end": { + "line": 3, + "column": 13 + } + }, + "expression": { + "type": "Identifier", + "start": 26, + "end": 34, + "loc": { + "start": { + "line": 3, + "column": 5 + }, + "end": { + "line": 3, + "column": 13 + }, + "identifierName": "classDec" + }, + "name": "classDec" + } + } + ], + "id": null, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 41, + "end": 87, + "loc": { + "start": { + "line": 3, + "column": 20 + }, + "end": { + "line": 6, + "column": 5 + } + }, + "body": [ + { + "type": "ClassMethod", + "start": 50, + "end": 80, + "loc": { + "start": { + "line": 4, + "column": 6 + }, + "end": { + "line": 5, + "column": 22 + } + }, + "decorators": [ + { + "type": "Decorator", + "start": 50, + "end": 56, + "loc": { + "start": { + "line": 4, + "column": 6 + }, + "end": { + "line": 4, + "column": 12 + } + }, + "expression": { + "type": "Identifier", + "start": 51, + "end": 56, + "loc": { + "start": { + "line": 4, + "column": 7 + }, + "end": { + "line": 4, + "column": 12 + }, + "identifierName": "inner" + }, + "name": "inner" + } + } + ], + "static": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 64, + "end": 75, + "loc": { + "start": { + "line": 5, + "column": 6 + }, + "end": { + "line": 5, + "column": 17 + }, + "identifierName": "innerMethod" + }, + "name": "innerMethod" + }, + "kind": "method", + "id": null, + "generator": false, + "expression": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 78, + "end": 80, + "loc": { + "start": { + "line": 5, + "column": 20 + }, + "end": { + "line": 5, + "column": 22 + } + }, + "body": [], + "directives": [] + } + } + ] + } + } + ] + } + } + ], + "static": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 94, + "end": 105, + "loc": { + "start": { + "line": 8, + "column": 2 + }, + "end": { + "line": 8, + "column": 13 + }, + "identifierName": "outerMethod" + }, + "name": "outerMethod" + }, + "kind": "method", + "id": null, + "generator": false, + "expression": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 108, + "end": 110, + "loc": { + "start": { + "line": 8, + "column": 16 + }, + "end": { + "line": 8, + "column": 18 + } + }, + "body": [], + "directives": [] + } + } + ] + } + } + ], + "directives": [] + } +} From 2c8fc756434a7980d23ddcbff5347ad57ef30884 Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Fri, 23 Jun 2017 18:13:51 +0530 Subject: [PATCH 3/5] Add test case for decorated static method --- .../decorators-2/static-method/actual.js | 4 + .../decorators-2/static-method/expected.json | 175 ++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 test/fixtures/experimental/decorators-2/static-method/actual.js create mode 100644 test/fixtures/experimental/decorators-2/static-method/expected.json diff --git a/test/fixtures/experimental/decorators-2/static-method/actual.js b/test/fixtures/experimental/decorators-2/static-method/actual.js new file mode 100644 index 0000000000..e91e239d61 --- /dev/null +++ b/test/fixtures/experimental/decorators-2/static-method/actual.js @@ -0,0 +1,4 @@ +class Foo { + @dec + static bar() {} +} diff --git a/test/fixtures/experimental/decorators-2/static-method/expected.json b/test/fixtures/experimental/decorators-2/static-method/expected.json new file mode 100644 index 0000000000..43de7c771c --- /dev/null +++ b/test/fixtures/experimental/decorators-2/static-method/expected.json @@ -0,0 +1,175 @@ +{ + "type": "File", + "start": 0, + "end": 38, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 4, + "column": 1 + } + }, + "program": { + "type": "Program", + "start": 0, + "end": 38, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 4, + "column": 1 + } + }, + "sourceType": "script", + "body": [ + { + "type": "ClassDeclaration", + "start": 0, + "end": 38, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 4, + "column": 1 + } + }, + "id": { + "type": "Identifier", + "start": 6, + "end": 9, + "loc": { + "start": { + "line": 1, + "column": 6 + }, + "end": { + "line": 1, + "column": 9 + }, + "identifierName": "Foo" + }, + "name": "Foo" + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 10, + "end": 38, + "loc": { + "start": { + "line": 1, + "column": 10 + }, + "end": { + "line": 4, + "column": 1 + } + }, + "body": [ + { + "type": "ClassMethod", + "start": 14, + "end": 36, + "loc": { + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 3, + "column": 17 + } + }, + "decorators": [ + { + "type": "Decorator", + "start": 14, + "end": 18, + "loc": { + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 6 + } + }, + "expression": { + "type": "Identifier", + "start": 15, + "end": 18, + "loc": { + "start": { + "line": 2, + "column": 3 + }, + "end": { + "line": 2, + "column": 6 + }, + "identifierName": "dec" + }, + "name": "dec" + } + } + ], + "static": true, + "computed": false, + "key": { + "type": "Identifier", + "start": 28, + "end": 31, + "loc": { + "start": { + "line": 3, + "column": 9 + }, + "end": { + "line": 3, + "column": 12 + }, + "identifierName": "bar" + }, + "name": "bar" + }, + "kind": "method", + "id": null, + "generator": false, + "expression": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 34, + "end": 36, + "loc": { + "start": { + "line": 3, + "column": 15 + }, + "end": { + "line": 3, + "column": 17 + } + }, + "body": [], + "directives": [] + } + } + ] + } + } + ], + "directives": [] + } +} \ No newline at end of file From c3b992e0317215345e5dc735219590dd27c65996 Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Fri, 23 Jun 2017 22:04:35 +0530 Subject: [PATCH 4/5] Minor change in an error message --- src/parser/statement.js | 2 +- .../decorators-2/no-export-decorators-on-class/options.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser/statement.js b/src/parser/statement.js index b3d7e1a28b..bdf7729f48 100644 --- a/src/parser/statement.js +++ b/src/parser/statement.js @@ -177,7 +177,7 @@ export default class StatementParser extends ExpressionParser { // special error for the common case of @dec export class if (!allowExport && this.match(tt._export)) { - this.raise(this.state.start, "Using the export keyword between decorators and class is disallowed. Please use `export @dec class` instead"); + this.raise(this.state.start, "Using the export keyword between a decorator and a class is not allowed. Please use `export @dec class` instead"); } if (!this.match(tt._class)) { diff --git a/test/fixtures/experimental/decorators-2/no-export-decorators-on-class/options.json b/test/fixtures/experimental/decorators-2/no-export-decorators-on-class/options.json index 4c5c609080..a2ae8cf036 100644 --- a/test/fixtures/experimental/decorators-2/no-export-decorators-on-class/options.json +++ b/test/fixtures/experimental/decorators-2/no-export-decorators-on-class/options.json @@ -1,4 +1,4 @@ { "sourceType": "module", - "throws": "Using the export keyword between decorators and class is disallowed. Please use `export @dec class` instead (2:0)" + "throws": "Using the export keyword between a decorator and a class is not allowed. Please use `export @dec class` instead (2:0)" } From f2ad94d0e34dc67e7516c92b8e5c1b2ca42d9b34 Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Tue, 27 Jun 2017 22:46:43 +0530 Subject: [PATCH 5/5] Incorporate suggestions from review --- src/parser/statement.js | 13 ++++++------- src/tokenizer/state.js | 3 ++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/parser/statement.js b/src/parser/statement.js index bdf7729f48..7a88bc17d2 100644 --- a/src/parser/statement.js +++ b/src/parser/statement.js @@ -171,13 +171,12 @@ export default class StatementParser extends ExpressionParser { currentContextDecorators.push(decorator); } - if (allowExport && this.match(tt._export)) { - return; - } - - // special error for the common case of @dec export class - if (!allowExport && this.match(tt._export)) { - this.raise(this.state.start, "Using the export keyword between a decorator and a class is not allowed. Please use `export @dec class` instead"); + if (this.match(tt._export)) { + if (allowExport) { + return; + } else { + this.raise(this.state.start, "Using the export keyword between a decorator and a class is not allowed. Please use `export @dec class` instead"); + } } if (!this.match(tt._class)) { diff --git a/src/tokenizer/state.js b/src/tokenizer/state.js index 31512c12f6..ff2307f1d6 100644 --- a/src/tokenizer/state.js +++ b/src/tokenizer/state.js @@ -90,7 +90,8 @@ export default class State { labels: Array<{ kind: ?("loop" | "switch"), statementStart?: number }>; // Leading decorators. Last element of the stack represents the decorators in current context. - // Supports nesting of decorators, e.g. @foo(@bar class {}) class {} + // Supports nesting of decorators, e.g. @foo(@bar class inner {}) class outer {} + // where @foo belongs to the outer class and @bar to the inner decoratorStack: Array>; // Token store.