From e04ecc79c7c3709b5b510d135db8032922af2636 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Sun, 22 Mar 2015 04:07:38 +1100 Subject: [PATCH] add support for class decorators --- src/acorn/src/state.js | 2 + src/acorn/src/statement.js | 83 ++++++++----------- src/acorn/src/tokenize.js | 1 + src/acorn/src/tokentype.js | 1 + src/acorn/test/tests-babel.js | 2 + src/babel/generation/generators/classes.js | 8 ++ .../generation/generators/expressions.js | 5 ++ src/babel/transformation/file/index.js | 2 +- .../templates/class-decorator.js | 1 + .../transformation/templates/create-class.js | 6 +- .../templates/create-computed-class.js | 16 ---- .../templates/create-decorated-class.js | 30 +++++++ .../transformers/es6/classes.js | 28 +++++-- .../transformers/es7/decorators.js | 4 + .../transformation/transformers/index.js | 1 + src/babel/types/visitor-keys.json | 1 + 16 files changed, 118 insertions(+), 73 deletions(-) create mode 100644 src/babel/transformation/templates/class-decorator.js delete mode 100644 src/babel/transformation/templates/create-computed-class.js create mode 100644 src/babel/transformation/templates/create-decorated-class.js create mode 100644 src/babel/transformation/transformers/es7/decorators.js diff --git a/src/acorn/src/state.js b/src/acorn/src/state.js index c8fb9928ba..ecc2e77cb0 100755 --- a/src/acorn/src/state.js +++ b/src/acorn/src/state.js @@ -51,6 +51,8 @@ export function Parser(options, input, startPos) { // Labels in scope. this.labels = [] + this.decorators = [] + // If enabled, skip leading hashbang line. if (this.pos === 0 && this.options.allowHashBang && this.input.slice(0, 2) === '#!') this.skipLineComment(2) diff --git a/src/acorn/src/statement.js b/src/acorn/src/statement.js index cd179ad643..0e77cdc308 100755 --- a/src/acorn/src/statement.js +++ b/src/acorn/src/statement.js @@ -51,9 +51,23 @@ pp.parseStatement = function(declaration, topLevel) { case tt._function: if (!declaration && this.options.ecmaVersion >= 6) this.unexpected() return this.parseFunctionStatement(node) + + case tt.at: + while (this.type === tt.at) { + this.decorators.push(this.parseDecorator()); + } + if (this.type !== tt._class) { + this.raise(this.start, "Leading decorators must be attached to a class declaration"); + } + case tt._class: if (!declaration) this.unexpected() + if (this.decorators.length) { + node.decorators = this.decorators + this.decorators = [] + } return this.parseClass(node, true) + case tt._if: return this.parseIfStatement(node) case tt._return: return this.parseReturnStatement(node) case tt._switch: return this.parseSwitchStatement(node) @@ -99,6 +113,13 @@ pp.parseStatement = function(declaration, topLevel) { } } +pp.parseDecorator = function() { + let node = this.startNode() + this.next() + node.expression = this.parseMaybeAssign() + return this.finishNode(node, "Decorator") +} + pp.parseBreakContinueStatement = function(node, keyword) { let isBreak = keyword == "break" this.next() @@ -423,52 +444,6 @@ pp.parseFunctionParams = function(node) { // Parse a class declaration or literal (depending on the // `isStatement` parameter). -pp.parseClass = function(node, isStatement) { - this.next() - node.id = this.type === tt.name ? this.parseIdent() : isStatement ? this.unexpected() : null - node.superClass = this.eat(tt._extends) ? this.parseExprSubscripts() : null - let classBody = this.startNode() - classBody.body = [] - this.expect(tt.braceL) - while (!this.eat(tt.braceR)) { - if (this.eat(tt.semi)) continue - let method = this.startNode() - let isGenerator = this.eat(tt.star) - this.parsePropertyName(method) - if (this.type !== tt.parenL && !method.computed && method.key.type === "Identifier" && - method.key.name === "static") { - if (isGenerator) this.unexpected() - method['static'] = true - isGenerator = this.eat(tt.star) - this.parsePropertyName(method) - } else { - method['static'] = false - } - if (this.options.features["es7.asyncFunctions"] && this.type !== tt.parenL && - !method.computed && method.key.type === "Identifier" && method.key.name === "async") { - isAsync = true; - this.parsePropertyName(method); - } - method.kind = "method" - if (!method.computed && !isGenerator) { - if (method.key.type === "Identifier") { - if (this.type !== tt.parenL && (method.key.name === "get" || method.key.name === "set")) { - method.kind = method.key.name - this.parsePropertyName(method) - } else if (!method['static'] && method.key.name === "constructor") { - method.kind = "constructor" - } - } else if (!method['static'] && method.key.type === "Literal" && method.key.value === "constructor") { - method.kind = "constructor" - } - } - method.value = this.parseMethod(isGenerator) - classBody.body.push(this.finishNode(method, "MethodDefinition")) - } - node.body = this.finishNode(classBody, "ClassBody") - return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression") -} - pp.parseClass = function(node, isStatement) { this.next() this.parseClassId(node, isStatement) @@ -476,8 +451,13 @@ pp.parseClass = function(node, isStatement) { var classBody = this.startNode() classBody.body = [] this.expect(tt.braceL) + var decorators = [] while (!this.eat(tt.braceR)) { if (this.eat(tt.semi)) continue + if (this.options.features["es7.decorators"] && this.type === tt.at) { + decorators.push(this.parseDecorator()); + continue; + } var method = this.startNode() var isGenerator = this.eat(tt.star), isAsync = false this.parsePropertyName(method) @@ -508,8 +488,15 @@ pp.parseClass = function(node, isStatement) { method.kind = "constructor" } } + if (this.options.features["es7.decorators"] && decorators.length) { + method.decorators = decorators + decorators = [] + } this.parseClassMethod(classBody, method, isGenerator, isAsync) } + if (decorators.length) { + raise(this.start, "You have trailing decorators with no method"); + } node.body = this.finishNode(classBody, "ClassBody") return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression") } @@ -540,11 +527,11 @@ pp.parseExport = function(node) { } if (this.eat(tt._default)) { // export default ... let expr = this.parseMaybeAssign() - let needsSemi = false + let needsSemi = true if (expr.id) switch (expr.type) { case "FunctionExpression": expr.type = "FunctionDeclaration"; break case "ClassExpression": expr.type = "ClassDeclaration"; break - default: needsSemi = true + default: needsSemi = false } node.declaration = expr if (needsSemi) this.semicolon() diff --git a/src/acorn/src/tokenize.js b/src/acorn/src/tokenize.js index 7fffbc5a07..fbb0617659 100755 --- a/src/acorn/src/tokenize.js +++ b/src/acorn/src/tokenize.js @@ -322,6 +322,7 @@ pp.getTokenFromCode = function(code) { case 125: ++this.pos; return this.finishToken(tt.braceR) case 58: ++this.pos; return this.finishToken(tt.colon) case 63: ++this.pos; return this.finishToken(tt.question) + case 64: ++this.pos; return this.finishToken(tt.at) case 96: // '`' if (this.options.ecmaVersion < 6) break diff --git a/src/acorn/src/tokentype.js b/src/acorn/src/tokentype.js index 99e6f44a58..aa0ecdabe2 100755 --- a/src/acorn/src/tokentype.js +++ b/src/acorn/src/tokentype.js @@ -61,6 +61,7 @@ export const types = { ellipsis: new TokenType("...", beforeExpr), backQuote: new TokenType("`", startsExpr), dollarBraceL: new TokenType("${", {beforeExpr: true, startsExpr: true}), + at: new TokenType("@"), // Operators. These carry several kinds of properties to help the // parser use them properly (the presence of these properties is diff --git a/src/acorn/test/tests-babel.js b/src/acorn/test/tests-babel.js index 538a2ce9e2..c0d2a26e3e 100644 --- a/src/acorn/test/tests-babel.js +++ b/src/acorn/test/tests-babel.js @@ -2056,3 +2056,5 @@ test('export async function foo(){}', { sourceType: "module", features: { "es7.asyncFunctions": true } }); + +// ES7 decorators diff --git a/src/babel/generation/generators/classes.js b/src/babel/generation/generators/classes.js index 4dbedb4152..251b2faf12 100644 --- a/src/babel/generation/generators/classes.js +++ b/src/babel/generation/generators/classes.js @@ -1,4 +1,8 @@ export function ClassDeclaration(node, print) { + if (node.decorators && node.decorators.length) { + print.list(node.decorators); + } + this.push("class"); if (node.id) { @@ -41,6 +45,10 @@ export function ClassBody(node, print) { } export function MethodDefinition(node, print) { + if (node.decorators && node.decorators.length) { + print.list(node.decorators); + } + if (node.static) { this.push("static "); } diff --git a/src/babel/generation/generators/expressions.js b/src/babel/generation/generators/expressions.js index d9ba2e640e..f23e8a98e4 100644 --- a/src/babel/generation/generators/expressions.js +++ b/src/babel/generation/generators/expressions.js @@ -61,6 +61,11 @@ export function Super() { this.push("super"); } +export function Decorator(node, print) { + this.push("@"); + print(node.expression); +} + export function CallExpression(node, print) { print(node.callee); diff --git a/src/babel/transformation/file/index.js b/src/babel/transformation/file/index.js index 65eefee04f..cd4417b4df 100644 --- a/src/babel/transformation/file/index.js +++ b/src/babel/transformation/file/index.js @@ -57,7 +57,7 @@ export default class File { "inherits", "defaults", "create-class", - "create-computed-class", + "create-decorated-class", "apply-constructor", "tagged-template-literal", "tagged-template-literal-loose", diff --git a/src/babel/transformation/templates/class-decorator.js b/src/babel/transformation/templates/class-decorator.js new file mode 100644 index 0000000000..565c4ac8b6 --- /dev/null +++ b/src/babel/transformation/templates/class-decorator.js @@ -0,0 +1 @@ +CLASS_REF = DECORATOR(CLASS_REF) || CLASS_REF; diff --git a/src/babel/transformation/templates/create-class.js b/src/babel/transformation/templates/create-class.js index c04f76eb60..4ebbc7e61e 100644 --- a/src/babel/transformation/templates/create-class.js +++ b/src/babel/transformation/templates/create-class.js @@ -1,11 +1,11 @@ (function() { function defineProperties(target, props) { - for (var key in props) { - var prop = props[key]; + for (var i = 0; i < props.length; i ++) { + var prop = props[i]; prop.configurable = true; if (prop.value) prop.writable = true; + Object.defineProperty(target, prop.key, prop); } - Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { diff --git a/src/babel/transformation/templates/create-computed-class.js b/src/babel/transformation/templates/create-computed-class.js deleted file mode 100644 index 4ebbc7e61e..0000000000 --- a/src/babel/transformation/templates/create-computed-class.js +++ /dev/null @@ -1,16 +0,0 @@ -(function() { - function defineProperties(target, props) { - for (var i = 0; i < props.length; i ++) { - var prop = props[i]; - prop.configurable = true; - if (prop.value) prop.writable = true; - Object.defineProperty(target, prop.key, prop); - } - } - - return function (Constructor, protoProps, staticProps) { - if (protoProps) defineProperties(Constructor.prototype, protoProps); - if (staticProps) defineProperties(Constructor, staticProps); - return Constructor; - }; -})() diff --git a/src/babel/transformation/templates/create-decorated-class.js b/src/babel/transformation/templates/create-decorated-class.js new file mode 100644 index 0000000000..2a16ebf5b9 --- /dev/null +++ b/src/babel/transformation/templates/create-decorated-class.js @@ -0,0 +1,30 @@ +(function() { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i ++) { + var descriptor = props[i]; + + descriptor.enumerable = false; + descriptor.configurable = true; + if (descriptor.value) descriptor.writable = true; + + if (descriptor.decorators) { + for (var i = 0; i < descriptor.decorators.length; i++) { + var decorator = descriptor.decorators[i]; + if (typeof decorator === "function") { + descriptor = decorator(target, descriptor.key, descriptor) || descriptor; + } else { + throw new TypeError("The decorator for method " + descriptor.key + " is of the invalid type " + typeof decorator); + } + } + } + + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + return function (Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; +})() diff --git a/src/babel/transformation/transformers/es6/classes.js b/src/babel/transformation/transformers/es6/classes.js index e1e8e0bdb4..a123c57dd1 100644 --- a/src/babel/transformation/transformers/es6/classes.js +++ b/src/babel/transformation/transformers/es6/classes.js @@ -67,6 +67,7 @@ class ClassTransformer { this.hasInstanceMutators = false; this.hasStaticMutators = false; + this.hasDecorators = false; this.instanceMutatorMap = {}; this.staticMutatorMap = {}; @@ -139,6 +140,16 @@ class ClassTransformer { this.buildBody(); + var decorators = this.node.decorators; + if (decorators) { + for (var i = 0; i < decorators.length; i++) { + var decorator = decorators[i]; + body.push(util.template("class-decorator", { + DECORATOR: decorator.expression, + CLASS_REF: classRef + }, true)); + } + } if (this.className) { // named class with only a constructor @@ -218,6 +229,7 @@ class ClassTransformer { var instanceProps; var staticProps; var classHelper = "create-class"; + if (this.hasDecorators) classHelper = "create-decorated-class"; if (this.hasInstanceMutators) { instanceProps = defineMap.toClassObject(this.instanceMutatorMap); @@ -228,11 +240,8 @@ class ClassTransformer { } if (instanceProps || staticProps) { - if (defineMap.hasComputed(this.instanceMutatorMap) || defineMap.hasComputed(this.staticMutatorMap)) { - if (instanceProps) instanceProps = defineMap.toComputedObjectFromClass(instanceProps); - if (staticProps) staticProps = defineMap.toComputedObjectFromClass(staticProps); - classHelper = "create-computed-class"; - } + if (instanceProps) instanceProps = defineMap.toComputedObjectFromClass(instanceProps); + if (staticProps) staticProps = defineMap.toComputedObjectFromClass(staticProps); instanceProps ||= t.literal(null); @@ -300,6 +309,15 @@ class ClassTransformer { } defineMap.push(mutatorMap, methodName, kind, node.computed, node); + + var decorators = node.decorators; + if (decorators && decorators.length) { + for (var i = 0; i < decorators.length; i++) { + decorators[i] = decorators[i].expression; + } + defineMap.push(mutatorMap, methodName, "decorators", node.computed, t.arrayExpression(decorators)); + this.hasDecorators = true; + } } /** diff --git a/src/babel/transformation/transformers/es7/decorators.js b/src/babel/transformation/transformers/es7/decorators.js new file mode 100644 index 0000000000..1761c24b0a --- /dev/null +++ b/src/babel/transformation/transformers/es7/decorators.js @@ -0,0 +1,4 @@ +export var metadata = { + experimental: true, + optional: true +}; diff --git a/src/babel/transformation/transformers/index.js b/src/babel/transformation/transformers/index.js index 40fd40d29f..cad654c1e9 100644 --- a/src/babel/transformation/transformers/index.js +++ b/src/babel/transformation/transformers/index.js @@ -1,5 +1,6 @@ export default { "es7.asyncFunctions": require("./es7/async-functions"), + "es7.decorators": require("./es7/decorators"), strict: require("./other/strict"), diff --git a/src/babel/types/visitor-keys.json b/src/babel/types/visitor-keys.json index 1ff8df5b8b..86c6f73ff0 100644 --- a/src/babel/types/visitor-keys.json +++ b/src/babel/types/visitor-keys.json @@ -17,6 +17,7 @@ "ComprehensionExpression": ["filter", "blocks", "body"], "ConditionalExpression": ["test", "consequent", "alternate"], "ContinueStatement": ["label"], + "Decorator": ["expression"], "DebuggerStatement": [], "DoWhileStatement": ["body", "test"], "DoExpression": ["body"],