diff --git a/.gitignore b/.gitignore index 2f8e0c3df..5950c1a0c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ test/mocha.js test/mocha.css test/*.es5.js +test/*.regenerator.js test/tests.browser.js .idea diff --git a/packages/transform/src/emit.js b/packages/transform/src/emit.js index 5c9e830b3..3fd6a84ee 100644 --- a/packages/transform/src/emit.js +++ b/packages/transform/src/emit.js @@ -764,6 +764,10 @@ Ep.explodeStatement = function(path, labelId) { break; + case "ClassDeclaration": + self.emit(self.explodeClass(path)); + break; + default: throw new Error( "unknown Statement of type " + @@ -900,6 +904,50 @@ Ep.updateContextPrevLoc = function(loc) { this.emitAssign(this.contextProperty("prev"), loc); }; + +// In order to save the rest of explodeExpression from a combinatorial +// trainwreck of special cases, explodeViaTempVar is responsible for +// deciding when a subexpression needs to be "exploded," which is my +// very technical term for emitting the subexpression as an assignment +// to a temporary variable and the substituting the temporary variable +// for the original subexpression. Think of exploded view diagrams, not +// Michael Bay movies. The point of exploding subexpressions is to +// control the precise order in which the generated code realizes the +// side effects of those subexpressions. +Ep.explodeViaTempVar = function(tempVar, childPath, hasLeapingChildren, ignoreChildResult) { + assert.ok( + !ignoreChildResult || !tempVar, + "Ignoring the result of a child expression but forcing it to " + + "be assigned to a temporary variable?" + ); + const t = util.getTypes(); + + let result = this.explodeExpression(childPath, ignoreChildResult); + + if (ignoreChildResult) { + // Side effects already emitted above. + + } else if (tempVar || (hasLeapingChildren && + !t.isLiteral(result))) { + // If tempVar was provided, then the result will always be assigned + // to it, even if the result does not otherwise need to be assigned + // to a temporary variable. When no tempVar is provided, we have + // the flexibility to decide whether a temporary variable is really + // necessary. Unfortunately, in general, a temporary variable is + // required whenever any child contains a yield expression, since it + // is difficult to prove (at all, let alone efficiently) whether + // this result would evaluate to the same value before and after the + // yield (see #206). One narrow case where we can prove it doesn't + // matter (and thus we do not need a temporary variable) is when the + // result in question is a Literal value. + result = this.emitAssign( + tempVar || this.makeTempVar(), + result + ); + } + return result; +}; + Ep.explodeExpression = function(path, ignoreResult) { const t = util.getTypes(); let expr = path.node; @@ -917,6 +965,7 @@ Ep.explodeExpression = function(path, ignoreResult) { t.assertExpression(expr); if (ignoreResult) { self.emit(expr); + return expr; } else { return expr; } @@ -934,48 +983,6 @@ Ep.explodeExpression = function(path, ignoreResult) { // side effects relative to the leaping child(ren). let hasLeapingChildren = meta.containsLeap.onlyChildren(expr); - // In order to save the rest of explodeExpression from a combinatorial - // trainwreck of special cases, explodeViaTempVar is responsible for - // deciding when a subexpression needs to be "exploded," which is my - // very technical term for emitting the subexpression as an assignment - // to a temporary variable and the substituting the temporary variable - // for the original subexpression. Think of exploded view diagrams, not - // Michael Bay movies. The point of exploding subexpressions is to - // control the precise order in which the generated code realizes the - // side effects of those subexpressions. - function explodeViaTempVar(tempVar, childPath, ignoreChildResult) { - assert.ok( - !ignoreChildResult || !tempVar, - "Ignoring the result of a child expression but forcing it to " + - "be assigned to a temporary variable?" - ); - - let result = self.explodeExpression(childPath, ignoreChildResult); - - if (ignoreChildResult) { - // Side effects already emitted above. - - } else if (tempVar || (hasLeapingChildren && - !t.isLiteral(result))) { - // If tempVar was provided, then the result will always be assigned - // to it, even if the result does not otherwise need to be assigned - // to a temporary variable. When no tempVar is provided, we have - // the flexibility to decide whether a temporary variable is really - // necessary. Unfortunately, in general, a temporary variable is - // required whenever any child contains a yield expression, since it - // is difficult to prove (at all, let alone efficiently) whether - // this result would evaluate to the same value before and after the - // yield (see #206). One narrow case where we can prove it doesn't - // matter (and thus we do not need a temporary variable) is when the - // result in question is a Literal value. - result = self.emitAssign( - tempVar || self.makeTempVar(), - result - ); - } - return result; - } - // If ignoreResult is true, then we must take full responsibility for // emitting the expression with all its side effects, and we should not // return a result. @@ -985,7 +992,7 @@ Ep.explodeExpression = function(path, ignoreResult) { return finish(t.memberExpression( self.explodeExpression(path.get("object")), expr.computed - ? explodeViaTempVar(null, path.get("property")) + ? self.explodeViaTempVar(null, path.get("property"), hasLeapingChildren) : expr.property, expr.computed )); @@ -1011,15 +1018,16 @@ Ep.explodeExpression = function(path, ignoreResult) { // expression, then we must be careful that the object of the // member expression still gets bound to `this` for the call. - let newObject = explodeViaTempVar( + let newObject = self.explodeViaTempVar( // Assign the exploded callee.object expression to a temporary // variable so that we can use it twice without reevaluating it. self.makeTempVar(), - calleePath.get("object") + calleePath.get("object"), + hasLeapingChildren ); let newProperty = calleePath.node.computed - ? explodeViaTempVar(null, calleePath.get("property")) + ? self.explodeViaTempVar(null, calleePath.get("property"), hasLeapingChildren) : calleePath.node.property; injectFirstArg = newObject; @@ -1039,7 +1047,7 @@ Ep.explodeExpression = function(path, ignoreResult) { } } else { - newCallee = explodeViaTempVar(null, calleePath); + newCallee = self.explodeViaTempVar(null, calleePath, hasLeapingChildren); if (t.isMemberExpression(newCallee)) { // If the callee was not previously a MemberExpression, then the @@ -1058,7 +1066,7 @@ Ep.explodeExpression = function(path, ignoreResult) { } if (hasLeapingArgs) { - newArgs = argsPath.map(argPath => explodeViaTempVar(null, argPath)); + newArgs = argsPath.map(argPath => self.explodeViaTempVar(null, argPath, hasLeapingChildren)); if (injectFirstArg) newArgs.unshift(injectFirstArg); newArgs = newArgs.map(arg => t.cloneDeep(arg)); @@ -1070,9 +1078,9 @@ Ep.explodeExpression = function(path, ignoreResult) { case "NewExpression": return finish(t.newExpression( - explodeViaTempVar(null, path.get("callee")), - path.get("arguments").map(function(argPath) { - return explodeViaTempVar(null, argPath); + self.explodeViaTempVar(null, path.get("callee"), hasLeapingChildren), + path.get("arguments").map(function(argPath) { + return self.explodeViaTempVar(null, argPath, hasLeapingChildren); }) )); @@ -1082,7 +1090,7 @@ Ep.explodeExpression = function(path, ignoreResult) { if (propPath.isObjectProperty()) { return t.objectProperty( propPath.node.key, - explodeViaTempVar(null, propPath.get("value")), + self.explodeViaTempVar(null, propPath.get("value"), hasLeapingChildren), propPath.node.computed ); } else { @@ -1096,10 +1104,10 @@ Ep.explodeExpression = function(path, ignoreResult) { path.get("elements").map(function(elemPath) { if (elemPath.isSpreadElement()) { return t.spreadElement( - explodeViaTempVar(null, elemPath.get("argument")) + self.explodeViaTempVar(null, elemPath.get("argument"), hasLeapingChildren) ); } else { - return explodeViaTempVar(null, elemPath); + return self.explodeViaTempVar(null, elemPath, hasLeapingChildren); } }) )); @@ -1124,7 +1132,7 @@ Ep.explodeExpression = function(path, ignoreResult) { result = self.makeTempVar(); } - let left = explodeViaTempVar(result, path.get("left")); + let left = self.explodeViaTempVar(result, path.get("left"), hasLeapingChildren); if (expr.operator === "&&") { self.jumpIfNot(left, after); @@ -1133,7 +1141,7 @@ Ep.explodeExpression = function(path, ignoreResult) { self.jumpIf(left, after); } - explodeViaTempVar(result, path.get("right"), ignoreResult); + self.explodeViaTempVar(result, path.get("right"), hasLeapingChildren, ignoreResult); self.mark(after); @@ -1150,11 +1158,11 @@ Ep.explodeExpression = function(path, ignoreResult) { result = self.makeTempVar(); } - explodeViaTempVar(result, path.get("consequent"), ignoreResult); + self.explodeViaTempVar(result, path.get("consequent"), hasLeapingChildren, ignoreResult); self.jump(after); self.mark(elseLoc); - explodeViaTempVar(result, path.get("alternate"), ignoreResult); + self.explodeViaTempVar(result, path.get("alternate"), hasLeapingChildren, ignoreResult); self.mark(after); @@ -1172,8 +1180,8 @@ Ep.explodeExpression = function(path, ignoreResult) { case "BinaryExpression": return finish(t.binaryExpression( expr.operator, - explodeViaTempVar(null, path.get("left")), - explodeViaTempVar(null, path.get("right")) + self.explodeViaTempVar(null, path.get("left"), hasLeapingChildren), + self.explodeViaTempVar(null, path.get("right"), hasLeapingChildren) )); case "AssignmentExpression": @@ -1254,9 +1262,39 @@ Ep.explodeExpression = function(path, ignoreResult) { return self.contextProperty("sent"); + case "ClassExpression": + return finish(self.explodeClass(path)); + default: throw new Error( "unknown Expression of type " + JSON.stringify(expr.type)); } }; + +Ep.explodeClass = function(path) { + const explodingChildren = []; + + if (path.node.superClass) { + explodingChildren.push(path.get("superClass")); + } + for (let i = 0; i < path.node.body.body.length; i++) { + const member = path.get("body.body")[i]; + if (member.node.computed) { + explodingChildren.push(member.get("key")); + } + } + + for (let i = 0; i < explodingChildren.length; i++) { + const child = explodingChildren[i]; + const isLast = i === explodingChildren.length - 1; + + if (isLast) { + child.replaceWith(this.explodeExpression(child)); + } else { + child.replaceWith(this.explodeViaTempVar(null, child, true)); + } + } + + return path.node; +}; diff --git a/test/class.js b/test/class.js index fb96f904c..171094d1f 100644 --- a/test/class.js +++ b/test/class.js @@ -68,4 +68,41 @@ describe("class methods", function () { assert.strictEqual(args[1], 2); assert.strictEqual(args[2], 3) }); + + it("should allow yield as super expression", function () { + function* gen() { + return class extends (yield) {} + } + + class B {} + + const it = gen(); + it.next(); + const res = it.next(B).value; + + assert.ok(new res instanceof B); + }); + + it("should allow yield as computed key", function () { + if (class {}.toString().indexOf("class") !== 0) { + return; + // The class transform is broken: + // https://github.com/babel/babel/issues/8300 + } + + function* gen() { + return class { + [yield]() { return 1 } + [yield]() { return 2 } + } + } + + const it = gen(); + it.next(); + it.next("one"); + const res = it.next("two").value; + + assert.strictEqual(new res().one(), 1); + assert.strictEqual(new res().two(), 2); + }); }); diff --git a/test/run.js b/test/run.js index e293f3c64..9bd37c64f 100644 --- a/test/run.js +++ b/test/run.js @@ -129,6 +129,47 @@ enqueue(convert, [ "./test/class.es5.js" ]); +enqueue(convertWithRegeneratorPluginOnly, [ + "./test/class.js", + "./test/class.regenerator.js" +]); + +Error.stackTraceLimit = 1000; + +/** + * Comvert without using the preset (which also transforms things like classes and arrows) + */ +function convertWithRegeneratorPluginOnly(inputFile, outputFile, callback) { + var transformOptions = { + plugins:[require("regenerator-transform")], + parserOpts: { + sourceType: "module", + allowImportExportEverywhere: true, + allowReturnOutsideFunction: true, + allowSuperOutsideMethod: true, + strictMode: false, + plugins: ["*", "jsx", "flow"] + }, + ast: true + }; + + fs.readFile(inputFile, "utf-8", function(err, input) { + if (err) { + return callback(err); + } + + var { code: output, ast } = babel.transformSync(input, transformOptions); + fs.writeFileSync(outputFile, output); + try { + checkDuplicatedNodes(babel, ast); + } catch (err) { + err.message = "Occured while transforming: " + inputFile + "\n" + err.message; + throw err; + } + callback(); + }); +} + function convertWithParamsTransform(es6File, es5File, callback) { var transformOptions = { presets:[require("regenerator-preset")], @@ -343,6 +384,14 @@ enqueue("mocha", [ "./test/async.es5.js", ]); +if (semver.gte(process.version, "6.0.0")) { + enqueue("mocha", [ + "--harmony", + "--reporter", "spec", + "--require", "./test/runtime.js", + "./test/class.regenerator.js", + ]); +} enqueue("mocha", [ "--harmony", "--reporter", "spec",