UNPKG

babel-plugin-extensible-destructuring

Version:

Babel plugin that enables extensible destructuring as per https://github.com/vacuumlabs/es-proposals

694 lines (582 loc) 22.6 kB
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } var sourceCode; var writes; function printNode(node) { if (!writes) { return; } _printNode(node, 2, 0); } function print(str) { if (writes) { console.log(str); } } function _printNode(node, lvl, indent) { if (lvl === 0) return; if (node == null) return; if (node.type != null) { var ind = []; for (var i = 0; i < indent + 1; i++) { ind.push(' '); } ind = ind.join(''); console.log(ind + node.type); if ('loc' in node) { var sl = node.loc.start.line; var el = node.loc.end.line; if (sl !== el) { throw new Error('sl != el'); } var toPrint = []; for (var _i = 0; _i < indent + 1; _i++) { toPrint.push(' '); } console.log(ind + ' ' + sourceCode[sl - 1].substring(node.loc.start.column, node.loc.end.column)); } for (var _i2 = 0, _Object$keys = Object.keys(node); _i2 < _Object$keys.length; _i2++) { var k = _Object$keys[_i2]; _printNode(node[k], lvl - 1, indent + 1); } } } export default function (_ref) { var t = _ref.types, version = _ref.version; var majorVersion = Number(version.split('.')[0]); /* Babel 7 renamed RestProperty to RestElement. Check the Babel version number supplied to the plugin and return the result of the preferred function call. */ var isRestElement = function isRestElement(tx, node, opts) { return majorVersion >= 7 ? tx.isRestElement(node, opts) : tx.isRestProperty(node, opts); }; function generateRequire(pkgName, methodName) { return t.variableDeclaration('var', [t.variableDeclarator(t.identifier('__extensible_get__'), t.memberExpression(t.callExpression(t.identifier('require'), [t.stringLiteral(pkgName)]), t.identifier(methodName), false))]); } ; function extensibleGet(obj, prop, def) { var args = [obj, prop]; if (def !== undefined) { args.push(def); } return t.callExpression(t.identifier('__extensible_get__'), args); } /** * Test if a VariableDeclaration's declarations contains any Patterns. */ function variableDeclarationHasPattern(node) { var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = node.declarations[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var declar = _step.value; if (t.isPattern(declar.id)) { return true; } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator["return"] != null) { _iterator["return"](); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return false; } /** * Test if an ArrayPattern's elements contain any RestElements. */ function hasRest(pattern) { var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = pattern.elements[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var elem = _step2.value; if (isRestElement(t, elem)) { return true; } } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) { _iterator2["return"](); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } return false; } var arrayUnpackVisitor = { ReferencedIdentifier: function ReferencedIdentifier(path, state) { if (state.bindings[path.node.name]) { state.deopt = true; path.stop(); } } }; var DestructuringTransformer = /*#__PURE__*/ function () { function DestructuringTransformer(opts) { _classCallCheck(this, DestructuringTransformer); this.blockHoist = opts.blockHoist; this.operator = opts.operator; this.arrays = {}; this.nodes = opts.nodes || []; this.scope = opts.scope; this.file = opts.file; this.kind = opts.kind; } _createClass(DestructuringTransformer, [{ key: "buildVariableAssignment", value: function buildVariableAssignment(id, init) { var op = this.operator; if (t.isMemberExpression(id)) op = '='; var node; if (op) { node = t.expressionStatement(t.assignmentExpression(op, id, init)); } else { node = t.variableDeclaration(this.kind, [t.variableDeclarator(id, init)]); } node._blockHoist = this.blockHoist; return node; } }, { key: "buildVariableDeclaration", value: function buildVariableDeclaration(id, init) { var declar = t.variableDeclaration('var', [t.variableDeclarator(id, init)]); declar._blockHoist = this.blockHoist; return declar; } }, { key: "push", value: function push(id, init) { if (t.isObjectPattern(id)) { this.pushObjectPattern(id, init); } else if (t.isArrayPattern(id)) { this.pushArrayPattern(id, init); } else if (t.isAssignmentPattern(id)) { throw new Error('shouldnt get here, handling of AssignmentPattert is inlined into ObjectPattern'); } else { this.nodes.push(this.buildVariableAssignment(id, init)); } } }, { key: "toArray", value: function toArray(node, count) { if (this.file.opts.loose || t.isIdentifier(node) && this.arrays[node.name]) { return node; } else { return this.scope.toArray(node, count); } } }, { key: "pushObjectRest", value: function pushObjectRest(pattern, objRef, spreadProp, spreadPropIndex) { print('pushObjectRest'); // get all the keys that appear in this object before the current spread var keys = []; for (var i = 0; i < pattern.properties.length; i++) { var prop = pattern.properties[i]; // we've exceeded the index of the spread property to all properties to the // right need to be ignored if (i >= spreadPropIndex) break; // ignore other spread properties if (isRestElement(t, prop)) continue; var key = prop.key; if (t.isIdentifier(key) && !prop.computed) key = t.stringLiteral(prop.key.name); keys.push(key); } keys = t.arrayExpression(keys); var value = t.callExpression(this.file.addHelper('objectWithoutProperties'), [objRef, keys]); this.nodes.push(this.buildVariableAssignment(spreadProp.argument, value)); } }, { key: "pushObjectProperty", value: function pushObjectProperty(prop, propRef) { print('pushObjectProperty'); if (t.isLiteral(prop.key)) prop.computed = true; var pattern = prop.value; var objRef = extensibleGet(propRef, prop.computed ? prop.key : t.stringLiteral(prop.key.name)); if (t.isPattern(pattern)) { if (t.isAssignmentPattern(pattern)) { objRef = extensibleGet(propRef, t.stringLiteral(prop.key.name), pattern.right); print('recursive'); this.push(pattern.left, objRef); } else { print('recursive'); this.push(pattern, objRef); } } else { this.nodes.push(this.buildVariableAssignment(pattern, objRef)); } } }, { key: "pushObjectPattern", value: function pushObjectPattern(pattern, objRef) { print('pushObjectPattern'); printNode(pattern); printNode(objRef); if (!pattern.properties.length) { this.nodes.push(t.expressionStatement(t.callExpression(this.file.addHelper('objectDestructuringEmpty'), [objRef]))); } // if we have more than one properties in this pattern and the objectRef is a // member expression then we need to assign it to a temporary variable so it's // only evaluated once if (pattern.properties.length > 1 && !this.scope.isStatic(objRef)) { var temp = this.scope.generateUidIdentifierBasedOnNode(objRef); var tempFullObjRef = objRef; this.nodes.push(this.buildVariableDeclaration(temp, tempFullObjRef)); objRef = temp; } for (var i = 0; i < pattern.properties.length; i++) { var prop = pattern.properties[i]; if (isRestElement(t, prop)) { this.pushObjectRest(pattern, objRef, prop, i); } else { this.pushObjectProperty(prop, objRef); } } } }, { key: "canUnpackArrayPattern", value: function canUnpackArrayPattern(pattern, arr) { // not an array so there's no way we can deal with this if (!t.isArrayExpression(arr)) return false; // pattern has less elements than the array and doesn't have a rest so some // elements wont be evaluated if (pattern.elements.length > arr.elements.length) return; if (pattern.elements.length < arr.elements.length && !hasRest(pattern)) return false; var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = pattern.elements[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { var elem = _step3.value; // deopt on holes if (!elem) return false; // deopt on member expressions as they may be included in the RHS if (t.isMemberExpression(elem)) return false; } } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3["return"] != null) { _iterator3["return"](); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; try { for (var _iterator4 = arr.elements[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { var _elem = _step4.value; // deopt on spread elements if (t.isSpreadElement(_elem)) return false; } // deopt on reference to left side identifiers } catch (err) { _didIteratorError4 = true; _iteratorError4 = err; } finally { try { if (!_iteratorNormalCompletion4 && _iterator4["return"] != null) { _iterator4["return"](); } } finally { if (_didIteratorError4) { throw _iteratorError4; } } } var bindings = t.getBindingIdentifiers(pattern); var state = { deopt: false, bindings: bindings }; this.scope.traverse(arr, arrayUnpackVisitor, state); return !state.deopt; } }, { key: "pushUnpackedArrayPattern", value: function pushUnpackedArrayPattern(pattern, arr) { for (var i = 0; i < pattern.elements.length; i++) { var elem = pattern.elements[i]; if (isRestElement(t, elem)) { this.push(elem.argument, t.arrayExpression(arr.elements.slice(i))); } else { this.push(elem, arr.elements[i]); } } } }, { key: "pushArrayPattern", value: function pushArrayPattern(pattern, arrayRef) { print('pushArrayPattern'); if (!pattern.elements) return; // optimise basic array destructuring of an array expression // // we can't do this to a pattern of unequal size to it's right hand // array expression as then there will be values that wont be evaluated // // eg: let [a, b] = [1, 2] if (this.canUnpackArrayPattern(pattern, arrayRef)) { return this.pushUnpackedArrayPattern(pattern, arrayRef); } // if we have a rest then we need all the elements so don't tell // `scope.toArray` to only get a certain amount var count = !hasRest(pattern) && pattern.elements.length; // so we need to ensure that the `arrayRef` is an array, `scope.toArray` will // return a locally bound identifier if it's been inferred to be an array, // otherwise it'll be a call to a helper that will ensure it's one var toArray = this.toArray(arrayRef, count); if (t.isIdentifier(toArray)) { // we've been given an identifier so it must have been inferred to be an // array arrayRef = toArray; } else { arrayRef = this.scope.generateUidIdentifierBasedOnNode(arrayRef); this.arrays[arrayRef.name] = true; this.nodes.push(this.buildVariableDeclaration(arrayRef, toArray)); } for (var i = 0; i < pattern.elements.length; i++) { var elem = pattern.elements[i]; // hole if (!elem) continue; var elemRef = void 0; if (isRestElement(t, elem)) { elemRef = this.toArray(arrayRef); if (i > 0) { elemRef = t.callExpression(t.memberExpression(elemRef, t.identifier('slice')), [t.numericLiteral(i)]); } // set the element to the rest element argument since we've dealt with it // being a rest already elem = elem.argument; } else { elemRef = t.memberExpression(arrayRef, t.numericLiteral(i), true); } this.push(elem, elemRef); } } }, { key: "init", value: function init(pattern, ref) { // trying to destructure a value that we can't evaluate more than once so we // need to save it to a variable if (!t.isArrayExpression(ref) && !t.isMemberExpression(ref)) { var memo = this.scope.maybeGenerateMemoised(ref, true); if (memo) { this.nodes.push(this.buildVariableDeclaration(memo, ref)); ref = memo; } } this.push(pattern, ref); return this.nodes; } }]); return DestructuringTransformer; }(); var shouldTransform = false; // 0 - no directive // 1 - 'use extensible' directive // -1 - 'use !extensible' directive function getDirective(path) { var _iteratorNormalCompletion5 = true; var _didIteratorError5 = false; var _iteratorError5 = undefined; try { for (var _iterator5 = path.node.directives[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { var directive = _step5.value; var dirstr = directive.value.value; if (dirstr.startsWith('use ')) { var uses = dirstr.substr(4).split(',').map(function (use) { return use.trim(); }); if (uses.indexOf('extensible') !== -1) { return 1; } if (uses.indexOf('!extensible') !== -1) { return -1; } } } } catch (err) { _didIteratorError5 = true; _iteratorError5 = err; } finally { try { if (!_iteratorNormalCompletion5 && _iterator5["return"] != null) { _iterator5["return"](); } } finally { if (_didIteratorError5) { throw _iteratorError5; } } } return 0; } return { visitor: { Program: function Program(path, state) { sourceCode = path.scope.hub.file.code.split('\n'); // debug purposes var directive = getDirective(path); if (state.opts == null) { state.opts = {}; } if (state.opts.mode == null) { state.opts.mode = 'optin'; } if (state.opts.mode === 'optin') { shouldTransform = directive === 1; } else if (state.opts.mode === 'optout') { shouldTransform = directive !== -1; } if (state.opts.package_name == null) { state.opts.package_name = 'extensible-runtime'; } if (state.opts.impl == null) { state.opts.impl = 'safe'; } writes = state.opts.verbose === true; if (shouldTransform) { path.node.body = [generateRequire(state.opts['package_name'], state.opts['impl'])].concat(_toConsumableArray(path.node.body)); } //console.log(path.scope.hub.file.opts.filename) //console.log(shouldTransform) }, ForXStatement: function ForXStatement(path, file) { if (!shouldTransform) { return; } var node = path.node, scope = path.scope; var left = node.left; if (t.isPattern(left)) { // for ({ length: k } in { abc: 3 }) var temp = scope.generateUidIdentifier('ref'); node.left = t.variableDeclaration('var', [t.variableDeclarator(temp)]); path.ensureBlock(); node.body.body.unshift(t.variableDeclaration('var', [t.variableDeclarator(left, temp)])); return; } if (!t.isVariableDeclaration(left)) return; var pattern = left.declarations[0].id; if (!t.isPattern(pattern)) return; var key = scope.generateUidIdentifier('ref'); node.left = t.variableDeclaration(left.kind, [t.variableDeclarator(key, null)]); var nodes = []; var destructuring = new DestructuringTransformer({ kind: left.kind, file: file, scope: scope, nodes: nodes }); destructuring.init(pattern, key); path.ensureBlock(); var block = node.body; block.body = nodes.concat(block.body); }, CatchClause: function CatchClause(_ref2, file) { var node = _ref2.node, scope = _ref2.scope; if (!shouldTransform) { return; } var pattern = node.param; if (!t.isPattern(pattern)) return; var ref = scope.generateUidIdentifier('ref'); node.param = ref; var nodes = []; var destructuring = new DestructuringTransformer({ kind: 'let', file: file, scope: scope, nodes: nodes }); destructuring.init(pattern, ref); node.body.body = nodes.concat(node.body.body); }, AssignmentExpression: function AssignmentExpression(path, file) { if (!shouldTransform) { return; } var node = path.node, scope = path.scope; if (!t.isPattern(node.left)) return; var nodes = []; var destructuring = new DestructuringTransformer({ operator: node.operator, file: file, scope: scope, nodes: nodes }); var ref; if (path.isCompletionRecord() || !path.parentPath.isExpressionStatement()) { ref = scope.generateUidIdentifierBasedOnNode(node.right, 'ref'); nodes.push(t.variableDeclaration('var', [t.variableDeclarator(ref, node.right)])); if (t.isArrayExpression(node.right)) { destructuring.arrays[ref.name] = true; } } destructuring.init(node.left, ref || node.right); if (ref) { nodes.push(t.expressionStatement(ref)); } path.replaceWithMultiple(nodes); }, VariableDeclaration: function VariableDeclaration(path, file) { if (!shouldTransform) { return; } var node = path.node, scope = path.scope, parent = path.parent; if (t.isForXStatement(parent)) return; if (!parent || !path.container) return; // i don't know why this is necessary - TODO if (!variableDeclarationHasPattern(node)) return; var nodes = []; var declar; for (var i = 0; i < node.declarations.length; i++) { declar = node.declarations[i]; var patternId = declar.init; var pattern = declar.id; var destructuring = new DestructuringTransformer({ blockHoist: node._blockHoist, nodes: nodes, scope: scope, kind: node.kind, file: file }); if (t.isPattern(pattern)) { destructuring.init(pattern, patternId); if (+i !== node.declarations.length - 1) { // we aren't the last declarator so let's just make the // last transformed node inherit from us t.inherits(nodes[nodes.length - 1], declar); } } else { nodes.push(t.inherits(destructuring.buildVariableAssignment(declar.id, declar.init), declar)); } } path.replaceWithMultiple(nodes); } } }; }