diff options
Diffstat (limited to 'babel-plugin-async-to-promises/lib/looprefactor.js')
-rw-r--r-- | babel-plugin-async-to-promises/lib/looprefactor.js | 257 |
1 files changed, 257 insertions, 0 deletions
diff --git a/babel-plugin-async-to-promises/lib/looprefactor.js b/babel-plugin-async-to-promises/lib/looprefactor.js new file mode 100644 index 0000000..226dbc0 --- /dev/null +++ b/babel-plugin-async-to-promises/lib/looprefactor.js @@ -0,0 +1,257 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _babelTypes = require('babel-types'); + +var _babelTemplate = require('babel-template'); + +var _babelTemplate2 = _interopRequireDefault(_babelTemplate); + +var _jsExtend = require('js-extend'); + +var _utils = require('./utils'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +exports.default = { + LabeledStatement: { + // Babel seems to auto-remove labels from the AST if they don't make sense + // in a position. That makes it hard to keep track of if you're in a loop + // with label. So we move the label onto the node itself, and handle it + // manually (at least, if we're touching the loop, i.e. if it has an await + // somewhere inside). + + enter: function enter(path) { + if ((0, _utils.containsAwait)(path)) { + path.node.body.loopLabel = path.node.label; + } + } + }, + DoWhileStatement: function DoWhileStatement(path) { + // converts + // + // do { + // newBody; + // } while (node.test) + // + // into: + // + // await async function _recursive() { + // newBody; + // if (node.test) { + // return await _recursive(); + // } + // }() + + refactorLoop(path, false, this.addVarDecl, function (functionID) { + var continueBlock = (0, _babelTypes.blockStatement)([continueStatementEquiv(functionID)]); + path.node.body.body.push((0, _babelTypes.ifStatement)(path.node.test, continueBlock)); + path.replaceWith(recursiveWrapFunction(functionID, path.node.body)); + }); + }, + WhileStatement: function WhileStatement(path) { + // converts + // + // while (node.test) { + // newBody; + // } + // + // into: + // + // await async function _recursive() { + // if (node.test) { + // newBody; + // return await _recursive(); + // } + // }() + + refactorLoop(path, false, this.addVarDecl, function (functionID) { + path.node.body.body.push(continueStatementEquiv(functionID)); + var body = (0, _babelTypes.blockStatement)([(0, _babelTypes.ifStatement)(path.node.test, path.node.body)]); + + path.replaceWith(recursiveWrapFunction(functionID, body)); + }); + }, + ForStatement: function ForStatement(path) { + // converts + // + // for(node.init, node.test, node.update) { + // newBody; + // } + // + // into: + // + // { + // node.init; + // await async function _recursive() { + // if (node.test) { + // newBody; + // node.update; + // return await _recursive(); + // } + // }() + // } + ifShouldRefactorLoop(path, (0, _utils.containsAwait)(path.get('update')), function () { + path.node.body.body.push((0, _babelTypes.expressionStatement)(path.node.update)); + path.replaceWithMultiple([(0, _babelTypes.expressionStatement)(path.node.init), (0, _babelTypes.whileStatement)(path.node.test, path.node.body)]); + }); + }, + ForInStatement: function ForInStatement(path) { + var _this = this; + + // converts + // for (node.left in node.right) { + // newBody; + // } + // + // info: + // + // var _items = []; + // for (var _item in node.right) { + // _items.push(_item); + // } + // _items.reverse(); + // await async function _recursive() { + // if (_items.length) { + // node.left = _items.pop(); + // node.body; + // return await _recursive(); + // } + // } + + ifShouldRefactorLoop(path, false, function () { + var KEYS = (0, _babelTypes.identifier)(path.scope.generateUid('keys')); + var OBJECT = (0, _babelTypes.identifier)(path.scope.generateUid('object')); + _this.addVarDecl(KEYS); + _this.addVarDecl(OBJECT); + path.replaceWithMultiple(forInEquiv({ + KEYS: KEYS, OBJECT: OBJECT, + KEY: (0, _babelTypes.identifier)(path.scope.generateUid('key')), + LEFT: path.node.left, + RIGHT: path.node.right, + BODY: path.node.body + })); + }); + } +}; + + +var forInEquiv = (0, _babelTemplate2.default)('\n OBJECT = RIGHT;\n KEYS = [];\n for (var KEY in OBJECT) {\n KEYS.push(KEY);\n }\n KEYS.reverse();\n while(KEYS.length) {\n LEFT = KEYS.pop();\n if (LEFT in OBJECT) {\n BODY;\n }\n }\n'); + +function recursiveWrapFunction(functionID, body) { + var func = (0, _utils.wrapFunction)(body); + func.callee.id = functionID; + + return (0, _utils.awaitStatement)(func); +} + +function insideAwaitContainingLabel(path) { + // walks the path tree to check if inside a label that also contains an await + // statement. (See also the LabeledStatement visitor.) + do { + if (path.node.loopLabel) { + return true; + } + } while (path = path.parentPath); + + // no such label found + return false; +} + +function ifShouldRefactorLoop(path, extraCheck, handler) { + // ensureBlock here is convenient, but has nothing to do with the method name + (0, _babelTypes.ensureBlock)(path.node); + + if (extraCheck || insideAwaitContainingLabel(path) || loopContainsAwait(path.get('body'))) { + handler(); + } +} + +var NoSubLoopsVisitor = { + Loop: function Loop(path) { + path.skip(); + } +}; + +// does the current loop (no subloops) contain an await statement? +var loopContainsAwait = (0, _utils.matcher)(['AwaitExpression'], (0, _jsExtend.extend)({}, _utils.NoSubFunctionsVisitor, NoSubLoopsVisitor)); + +function refactorLoop(path, extraCheck, addVarDecl, handler) { + ifShouldRefactorLoop(path, extraCheck, function () { + // gather info about the function & fix up its body (break + continue + // statements) + var label = path.node.loopLabel; + var functionID = label || (0, _babelTypes.identifier)(path.scope.generateUid('recursive')); + var info = { functionID: functionID }; + path.get('body').traverse(BreakContinueReplacementVisitor, info); + // actual conversion + handler(functionID); + + // if containing a return *or* a break statement that doesn't control the + // own loop (references a label of another loop), add: + // + // .then(function (_resp) { + // _temp = _resp; + // if (_temp !== _recursive) { + // return _temp; + // } + // }); + if (info.addReturnHandler) { + var tmp = (0, _babelTypes.identifier)(path.scope.generateUid('temp')); + addVarDecl(tmp); + path.node.loopLabel = label; + path.replaceWithMultiple(loopReturnHandler({ TMP: tmp, BASE: path.node, FUNC: functionID })); + } + }); +} + +var loopReturnHandler = (0, _babelTemplate2.default)('\n TMP = BASE\n if (_temp !== FUNC) {\n return _temp;\n }\n'); + +var continueStatementEquiv = function continueStatementEquiv(funcID) { + // continue label; -> return await label(); + var stmt = (0, _babelTypes.returnStatement)((0, _babelTypes.awaitExpression)((0, _babelTypes.callExpression)(funcID, []))); + // not a 'real' return + stmt.noHandlerRequired = true; + return stmt; +}; + +var BreakContinueReplacementVisitor = (0, _jsExtend.extend)({ + ReturnStatement: function ReturnStatement(path) { + if (!path.node.noHandlerRequired && path.node.argument) { + // if a return statement added by the user - and actually returning + // something, we need to add a return handler later. + this.addReturnHandler = true; + } + }, + + // replace continue/break with their recursive equivalents + BreakStatement: function BreakStatement(path) { + // a break statement is replaced by returning the name of the loop function + // that should be broken. It's a convenient unique value. + // + // So: break; becomes return _recursive; + // + // and break myLabel; becomes return myLabel; + + var label = getLabel(path, this.functionID); + + var returnStmt = (0, _babelTypes.returnStatement)(getLabel(path, this.functionID)); + if (label === this.functionID) { + // only if this controls the current loop, a return handler is unnecessary + returnStmt.noHandlerRequired = true; + } + path.replaceWith(returnStmt); + }, + ContinueStatement: function ContinueStatement(path) { + // see break, with the difference that the function is called (and thus) + // executed next + path.replaceWith(continueStatementEquiv(getLabel(path, this.functionID))); + } +}, _utils.NoSubFunctionsVisitor, NoSubLoopsVisitor); + +var getLabel = function getLabel(path, functionID) { + return path.node.label || functionID; +};
\ No newline at end of file |