UNPKG

babel-core

Version:

Turn ES6 code into readable vanilla ES5 with source maps

647 lines (515 loc) 15 kB
"use strict"; module.exports = Scope; var includes = require("lodash/collection/includes"); var traverse = require("./index"); var defaults = require("lodash/object/defaults"); var messages = require("../messages"); var globals = require("globals"); var flatten = require("lodash/array/flatten"); var extend = require("lodash/object/extend"); var object = require("../helpers/object"); var each = require("lodash/collection/each"); var t = require("../types"); /** * This searches the current "scope" and collects all references/bindings * within. * * @param {Node} block * @param {Node} parentBlock * @param {Scope} [parent] * @param {File} [file] */ function Scope(block, parentBlock, parent, file) { this.parent = parent; this.file = parent ? parent.file : file; this.parentBlock = parentBlock; this.block = block; this.crawl(); } Scope.globals = flatten([globals.builtin, globals.browser, globals.node].map(Object.keys)); /** * Description * * @param {Object} node * @param {Object} opts * @param [state] */ Scope.prototype.traverse = function (node, opts, state) { traverse(node, opts, this, state); }; /** * Description * * @param {String} [name="temp"] */ Scope.prototype.generateTemp = function (name) { var id = this.generateUidIdentifier(name || "temp"); this.push({ key: id.name, id: id }); return id; }; /** * Description * * @param {String} name */ Scope.prototype.generateUidIdentifier = function (name) { var id = t.identifier(this.generateUid(name)); this.getFunctionParent().registerBinding("uid", id); return id; }; /** * Description * * @param {String} name */ Scope.prototype.generateUid = function (name) { name = t.toIdentifier(name).replace(/^_+/, ""); var uid; var i = 0; do { uid = this._generateUid(name, i); i++; } while (this.hasBinding(uid) || this.hasGlobal(uid)); return uid; }; Scope.prototype._generateUid = function (name, i) { var id = name; if (i > 1) id += i; return "_" + id; }; /* * Description * * @param {Object} parent * @returns {Object} */ Scope.prototype.generateUidBasedOnNode = function (parent) { var node = parent; if (t.isAssignmentExpression(parent)) { node = parent.left; } else if (t.isVariableDeclarator(parent)) { node = parent.id; } else if (t.isProperty(node)) { node = node.key; } var parts = []; var add = function (node) { if (t.isMemberExpression(node)) { add(node.object); add(node.property); } else if (t.isIdentifier(node)) { parts.push(node.name); } else if (t.isLiteral(node)) { parts.push(node.value); } else if (t.isCallExpression(node)) { add(node.callee); } }; add(node); var id = parts.join("$"); id = id.replace(/^_/, "") || "ref"; return this.generateUidIdentifier(id); }; /** * Description * * @param {Object} node * @returns {Object} */ Scope.prototype.generateTempBasedOnNode = function (node) { if (t.isIdentifier(node) && this.hasBinding(node.name)) { return null; } var id = this.generateUidBasedOnNode(node); this.push({ key: id.name, id: id }); return id; }; Scope.prototype.checkBlockScopedCollisions = function (kind, name, id) { var local = this.getOwnBindingInfo(name); if (!local) return; if (kind === "param") return; if (kind === "hoisted" && local.kind === "let") return; if (local.kind === "let" || local.kind === "const" || local.kind === "module") { throw this.file.errorWithNode(id, messages.get("scopeDuplicateDeclaration", name), TypeError); } }; Scope.prototype.rename = function (oldName, newName) { newName = newName || this.generateUidIdentifier(oldName).name; var info = this.getBindingInfo(oldName); if (!info) return; var binding = info.identifier; var scope = info.scope; scope.traverse(scope.block, { enter: function (node, parent, scope) { if (t.isReferencedIdentifier(node, parent) && node.name === oldName) { node.name = newName; } else if (t.isDeclaration(node)) { var ids = t.getBindingIdentifiers(node); for (var name in ids) { if (name === oldName) ids[name].name = newName; } } else if (t.isScope(node, parent)) { if (!scope.bindingIdentifierEquals(oldName, binding)) { this.skip(); } } } }); this.clearOwnBinding(oldName); scope.bindings[newName] = info; binding.name = newName; }; Scope.prototype.inferType = function (node) { var target; if (t.isVariableDeclarator(node)) { target = node.init; } if (t.isArrayExpression(target)) { return t.genericTypeAnnotation(t.identifier("Array")); } if (t.isObjectExpression(target)) { return; } if (t.isLiteral(target)) { return; } if (t.isCallExpression(target) && t.isIdentifier(target.callee)) { var funcInfo = this.getBindingInfo(target.callee.name); if (funcInfo) { var funcNode = funcInfo.node; return !funcInfo.reassigned && t.isFunction(funcNode) && node.returnType; } } if (t.isIdentifier(target)) { return; } }; Scope.prototype.isTypeGeneric = function (name, genericName) { var info = this.getBindingInfo(name); if (!info) return false; var type = info.typeAnnotation; return t.isGenericTypeAnnotation(type) && t.isIdentifier(type.id, { name: genericName }); }; Scope.prototype.assignTypeGeneric = function (name, type) { this.assignType(name, t.genericTypeAnnotation(t.identifier(type))); }; Scope.prototype.assignType = function (name, type) { var info = this.getBindingInfo(name); if (!info) return; info.identifier.typeAnnotation = info.typeAnnotation = type; }; Scope.prototype.getTypeAnnotation = function (name, id, node) { var info = { annotation: null, inferred: false }; var type; if (id.typeAnnotation) { type = id.typeAnnotation; } if (!type) { info.inferred = true; type = this.inferType(node); } if (type) { if (t.isTypeAnnotation(type)) type = type.typeAnnotation; info.annotation = type; } return info; }; Scope.prototype.toArray = function (node, i) { var file = this.file; if (t.isIdentifier(node) && this.isTypeGeneric(node.name, "Array")) { return node; } if (t.isArrayExpression(node)) { return node; } if (t.isIdentifier(node, { name: "arguments" })) { return t.callExpression(t.memberExpression(file.addHelper("slice"), t.identifier("call")), [node]); } var helperName = "to-array"; var args = [node]; if (i === true) { helperName = "to-consumable-array"; } else if (i) { args.push(t.literal(i)); helperName = "sliced-to-array"; } return t.callExpression(file.addHelper(helperName), args); }; Scope.prototype.clearOwnBinding = function (name) { delete this.bindings[name]; }; Scope.prototype.registerDeclaration = function (node) { if (t.isFunctionDeclaration(node)) { this.registerBinding("hoisted", node); } else if (t.isVariableDeclaration(node)) { for (var i = 0; i < node.declarations.length; i++) { this.registerBinding(node.kind, node.declarations[i]); } } else if (t.isClassDeclaration(node)) { this.registerBinding("let", node); } else if (t.isImportDeclaration(node) || t.isExportDeclaration(node)) { this.registerBinding("module", node); } else { this.registerBinding("unknown", node); } }; Scope.prototype.registerBindingReassignment = function (node) { var ids = t.getBindingIdentifiers(node); for (var name in ids) { var info = this.getBindingInfo(name); if (info) { info.reassigned = true; if (info.typeAnnotationInferred) { // destroy the inferred typeAnnotation info.typeAnnotation = null; } } } }; Scope.prototype.registerBinding = function (kind, node) { if (!kind) throw new ReferenceError("no `kind`"); var ids = t.getBindingIdentifiers(node); for (var name in ids) { var id = ids[name]; this.checkBlockScopedCollisions(kind, name, id); var typeInfo = this.getTypeAnnotation(name, id, node); this.bindings[name] = { typeAnnotationInferred: typeInfo.inferred, typeAnnotation: typeInfo.annotation, reassigned: false, identifier: id, scope: this, node: node, kind: kind }; } }; Scope.prototype.registerVariableDeclaration = function (declar) { var declars = declar.declarations; for (var i = 0; i < declars.length; i++) { this.registerBinding(declars[i], declar.kind); } }; var functionVariableVisitor = { enter: function (node, parent, scope, state) { if (t.isFor(node)) { each(t.FOR_INIT_KEYS, function (key) { var declar = node[key]; if (t.isVar(declar)) state.scope.registerBinding("var", declar); }); } // this block is a function so we'll stop since none of the variables // declared within are accessible if (t.isFunction(node)) return this.skip(); // function identifier doesn't belong to this scope if (state.blockId && node === state.blockId) return; // delegate block scope handling to the `blockVariableVisitor` if (t.isBlockScoped(node)) return; // this will be hit again once we traverse into it after this iteration if (t.isExportDeclaration(node) && t.isDeclaration(node.declaration)) return; // we've ran into a declaration! if (t.isDeclaration(node)) state.scope.registerDeclaration(node); } }; Scope.prototype.addGlobal = function (node) { this.globals[node.name] = node; }; Scope.prototype.hasGlobal = function (name) { var scope = this; do { if (scope.globals[name]) return true; } while (scope = scope.parent); return false; }; var programReferenceVisitor = { enter: function (node, parent, scope, state) { if (t.isReferencedIdentifier(node, parent) && !scope.hasBinding(node.name)) { state.addGlobal(node); } else if (t.isLabeledStatement(node)) { state.addGlobal(node); } else if (t.isAssignmentExpression(node) || t.isUpdateExpression(node) || (t.isUnaryExpression(node) && node.operator === "delete")) { scope.registerBindingReassignment(node); } } }; var blockVariableVisitor = { enter: function (node, parent, scope, state) { if (t.isFunctionDeclaration(node) || t.isBlockScoped(node)) { state.registerDeclaration(node); } else if (t.isScope(node, parent)) { this.skip(); } } }; Scope.prototype.crawl = function () { var block = this.block; var i; // var info = block._scopeInfo; if (info) { extend(this, info); return; } info = block._scopeInfo = { bindings: object(), globals: object() }; extend(this, info); // ForStatement - left, init if (t.isLoop(block)) { for (i = 0; i < t.FOR_INIT_KEYS.length; i++) { var node = block[t.FOR_INIT_KEYS[i]]; if (t.isBlockScoped(node)) this.registerBinding("let", node); } if (t.isBlockStatement(block.body)) { block = block.body; } } // FunctionExpression - id if (t.isFunctionExpression(block) && block.id) { if (!t.isProperty(this.parentBlock, { method: true })) { this.registerBinding("var", block.id); } } // Function - params, rest if (t.isFunction(block)) { for (i = 0; i < block.params.length; i++) { this.registerBinding("param", block.params[i]); } this.traverse(block.body, blockVariableVisitor, this); } // Program, BlockStatement, Function - let variables if (t.isBlockStatement(block) || t.isProgram(block)) { this.traverse(block, blockVariableVisitor, this); } // CatchClause - param if (t.isCatchClause(block)) { this.registerBinding("let", block.param); } // ComprehensionExpression - blocks if (t.isComprehensionExpression(block)) { this.registerBinding("let", block); } // Program, Function - var variables if (t.isProgram(block) || t.isFunction(block)) { this.traverse(block, functionVariableVisitor, { blockId: block.id, scope: this }); } // Program if (t.isProgram(block)) { this.traverse(block, programReferenceVisitor, this); } }; /** * Description * * @param {Object} opts */ Scope.prototype.push = function (opts) { var block = this.block; if (t.isLoop(block) || t.isCatchClause(block) || t.isFunction(block)) { t.ensureBlock(block); block = block.body; } if (t.isBlockStatement(block) || t.isProgram(block)) { block._declarations = block._declarations || {}; block._declarations[opts.key] = { kind: opts.kind, id: opts.id, init: opts.init }; } else { throw new TypeError("cannot add a declaration here in node type " + block.type); } }; /** * Walk up the scope tree until we hit either a Function or reach the * very top and hit Program. */ Scope.prototype.getFunctionParent = function () { var scope = this; while (scope.parent && !t.isFunction(scope.block)) { scope = scope.parent; } return scope; }; /** * Walks the scope tree and gathers **all** bindings. * * @returns {Object} */ Scope.prototype.getAllBindings = function () { var ids = object(); var scope = this; do { defaults(ids, scope.bindings); scope = scope.parent; } while (scope); return ids; }; /** * Walks the scope tree and gathers all declarations of `kind`. * * @param {String} kind * @returns {Object} */ Scope.prototype.getAllBindingsOfKind = function (kind) { var ids = object(); var scope = this; do { for (var name in scope.bindings) { var binding = scope.bindings[name]; if (binding.kind === kind) ids[name] = binding; } scope = scope.parent; } while (scope); return ids; }; // misc Scope.prototype.bindingIdentifierEquals = function (name, node) { return this.getBindingIdentifier(name) === node; }; // get Scope.prototype.getBindingInfo = function (name) { var scope = this; do { var binding = scope.getOwnBindingInfo(name); if (binding) return binding; } while (scope = scope.parent); }; Scope.prototype.getOwnBindingInfo = function (name) { return this.bindings[name]; }; Scope.prototype.getBindingIdentifier = function (name) { var info = this.getBindingInfo(name); return info && info.identifier; }; Scope.prototype.getOwnBindingIdentifier = function (name) { var binding = this.bindings[name]; return binding && binding.identifier; }; // has Scope.prototype.hasOwnBinding = function (name) { return !!this.getOwnBindingInfo(name); }; Scope.prototype.hasBinding = function (name) { if (!name) return false; if (this.hasOwnBinding(name)) return true; if (this.parentHasBinding(name)) return true; if (includes(Scope.globals, name)) return true; return false; }; Scope.prototype.parentHasBinding = function (name) { return this.parent && this.parent.hasBinding(name); };