From 8598000a69e447ff4795066facb719e0bbbbd141 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Tue, 10 Feb 2015 23:52:23 +1100 Subject: [PATCH] restructure Scope API and internal data structure --- lib/6to5/transformation/file.js | 4 +- .../helpers/explode-assignable-expression.js | 2 +- .../transformation/helpers/name-method.js | 4 +- lib/6to5/transformation/modules/_default.js | 2 +- .../transformers/es6/block-scoping-tdz.js | 2 +- .../transformers/es6/block-scoping.js | 4 +- .../transformers/es6/constants.js | 14 +- .../transformers/es6/parameters.default.js | 4 +- .../transformers/es6/tail-call.js | 2 +- .../transformers/other/self-contained.js | 4 +- lib/6to5/traversal/scope.js | 260 ++++++++---------- 11 files changed, 137 insertions(+), 165 deletions(-) diff --git a/lib/6to5/transformation/file.js b/lib/6to5/transformation/file.js index 2eccb48b3a..87eb3f7ca1 100644 --- a/lib/6to5/transformation/file.js +++ b/lib/6to5/transformation/file.js @@ -456,14 +456,14 @@ File.prototype.generateUid = function (name, scope) { do { uid = this._generateUid(name, i); i++; - } while (scope.hasReference(uid)); + } while (scope.hasBinding(uid) || scope.hasGlobal(uid)); return uid; }; File.prototype.generateUidIdentifier = function (name, scope) { scope = scope || this.scope; var id = t.identifier(this.generateUid(name, scope)); - scope.addBindingToFunctionScope(id, "uid"); + scope.getFunctionParent().registerBinding("uid", id); return id; }; diff --git a/lib/6to5/transformation/helpers/explode-assignable-expression.js b/lib/6to5/transformation/helpers/explode-assignable-expression.js index ada6216287..011a4373cd 100644 --- a/lib/6to5/transformation/helpers/explode-assignable-expression.js +++ b/lib/6to5/transformation/helpers/explode-assignable-expression.js @@ -18,7 +18,7 @@ var getObjRef = function (node, nodes, file, scope) { } else if (t.isMemberExpression(node)) { ref = node.object; - if (t.isIdentifier(ref) && scope.hasReference(ref.name)) { + if (t.isIdentifier(ref) && scope.hasGlobal(ref.name)) { // the object reference that we need to save is locally declared // so as per the previous comment we can be 100% sure evaluating // it multiple times will be safe diff --git a/lib/6to5/transformation/helpers/name-method.js b/lib/6to5/transformation/helpers/name-method.js index 6f47ac95cc..05cbcf1764 100644 --- a/lib/6to5/transformation/helpers/name-method.js +++ b/lib/6to5/transformation/helpers/name-method.js @@ -13,7 +13,7 @@ var visitor = { // check that we don't have a local variable declared as that removes the need // for the wrapper - var localDeclar = scope.getBinding(state.id); + var localDeclar = scope.getBindingIdentifier(state.id); if (localDeclar !== state.outerDeclar) return; state.selfReference = true; @@ -31,7 +31,7 @@ exports.property = function (node, file, scope) { var state = { id: id, selfReference: false, - outerDeclar: scope.getBinding(id), + outerDeclar: scope.getBindingIdentifier(id), }; scope.traverse(node, visitor, state); diff --git a/lib/6to5/transformation/modules/_default.js b/lib/6to5/transformation/modules/_default.js index 908180f1c3..a518583bf5 100644 --- a/lib/6to5/transformation/modules/_default.js +++ b/lib/6to5/transformation/modules/_default.js @@ -146,7 +146,7 @@ DefaultFormatter.prototype.remapExportAssignment = function (node) { DefaultFormatter.prototype.isLocalReference = function (node, scope) { var localExports = this.localExports; var name = node.name; - return t.isIdentifier(node) && localExports[name] && localExports[name] === scope.getBinding(name); + return t.isIdentifier(node) && localExports[name] && localExports[name] === scope.getBindingIdentifier(name); }; DefaultFormatter.prototype.getModuleName = function () { diff --git a/lib/6to5/transformation/transformers/es6/block-scoping-tdz.js b/lib/6to5/transformation/transformers/es6/block-scoping-tdz.js index df8002b671..e851632a28 100644 --- a/lib/6to5/transformation/transformers/es6/block-scoping-tdz.js +++ b/lib/6to5/transformation/transformers/es6/block-scoping-tdz.js @@ -10,7 +10,7 @@ var visitor = { if (!declared) return; // declared node is different in this scope - if (scope.getBinding(node.name) !== declared) return; + if (scope.getBindingIdentifier(node.name) !== declared) return; var assert = t.callExpression( state.file.addHelper("temporal-assert-defined"), diff --git a/lib/6to5/transformation/transformers/es6/block-scoping.js b/lib/6to5/transformation/transformers/es6/block-scoping.js index 5acadcc184..275bf751ae 100644 --- a/lib/6to5/transformation/transformers/es6/block-scoping.js +++ b/lib/6to5/transformation/transformers/es6/block-scoping.js @@ -136,7 +136,7 @@ function replace(node, parent, scope, remaps) { var remap = remaps[node.name]; if (!remap) return; - var ownBinding = scope.getBinding(node.name); + var ownBinding = scope.getBindingIdentifier(node.name); if (ownBinding === remap.binding) { node.name = remap.uid; } else { @@ -175,7 +175,7 @@ BlockScoping.prototype.remap = function () { // this is the defining identifier of a declaration var ref = letRefs[key]; - if (scope.parentHasReference(key)) { + if (scope.parentHasBinding(key) || scope.hasGlobal(key)) { var uid = scope.generateUidIdentifier(ref.name).name; ref.name = uid; diff --git a/lib/6to5/transformation/transformers/es6/constants.js b/lib/6to5/transformation/transformers/es6/constants.js index ce70395e9b..47b257afdc 100644 --- a/lib/6to5/transformation/transformers/es6/constants.js +++ b/lib/6to5/transformation/transformers/es6/constants.js @@ -11,23 +11,25 @@ var visitor = { if (t.isAssignmentExpression(node) || t.isUpdateExpression(node)) { var ids = t.getBindingIdentifiers(node); - for (var key in ids) { - var id = ids[key]; + for (var name in ids) { + var id = ids[name]; - var constant = state.constants[key]; + var constant = state.constants[name]; // no constant exists if (!constant) continue; + var constantIdentifier = constant.identifier; + // check if the assignment id matches the constant declaration id // if it does then it was the id used to initially declare the // constant so we can just ignore it - if (id === constant) continue; + if (id === constantIdentifier) continue; // check if there's been a local binding that shadows this constant - if (!scope.bindingEquals(key, constant)) continue; + if (!scope.bindingIdentifierEquals(name, constantIdentifier)) continue; - throw state.file.errorWithNode(id, key + " is read-only"); + throw state.file.errorWithNode(id, name + " is read-only"); } } else if (t.isScope(node)) { this.skip(); diff --git a/lib/6to5/transformation/transformers/es6/parameters.default.js b/lib/6to5/transformation/transformers/es6/parameters.default.js index 040d517ddf..7736d63f89 100644 --- a/lib/6to5/transformation/transformers/es6/parameters.default.js +++ b/lib/6to5/transformation/transformers/es6/parameters.default.js @@ -18,7 +18,7 @@ var iifeVisitor = { enter: function (node, parent, scope, state) { if (!t.isReferencedIdentifier(node, parent)) return; if (!state.scope.hasOwnBinding(node.name)) return; - if (state.scope.bindingEquals(node.name, node)) return; + if (state.scope.bindingIdentifierEquals(node.name, node)) return; state.iife = true; this.stop(); @@ -78,7 +78,7 @@ exports.Function = function (node, parent, scope, file) { node.params[i] = placeholder; if (!state.iife) { - if (t.isIdentifier(right) && scope.hasOwnReference(right.name)) { + if (t.isIdentifier(right) && scope.hasOwnBinding(right.name)) { state.iife = true; } else { scope.traverse(right, iifeVisitor, state); diff --git a/lib/6to5/transformation/transformers/es6/tail-call.js b/lib/6to5/transformation/transformers/es6/tail-call.js index 37581f7a96..fb0325d7aa 100644 --- a/lib/6to5/transformation/transformers/es6/tail-call.js +++ b/lib/6to5/transformation/transformers/es6/tail-call.js @@ -219,7 +219,7 @@ TailCallTransformer.prototype.subTransformCallExpression = function (node) { } // only tail recursion can be optimized as for now - if (!t.isIdentifier(callee) || !this.scope.bindingEquals(callee.name, this.ownerId)) { + if (!t.isIdentifier(callee) || !this.scope.bindingIdentifierEquals(callee.name, this.ownerId)) { return; } diff --git a/lib/6to5/transformation/transformers/other/self-contained.js b/lib/6to5/transformation/transformers/other/self-contained.js index d97b4063ad..ace9c2bae5 100644 --- a/lib/6to5/transformation/transformers/other/self-contained.js +++ b/lib/6to5/transformation/transformers/other/self-contained.js @@ -30,11 +30,11 @@ var astVisitor = { if (!t.isReferenced(obj, node)) return; - if (!node.computed && coreHas(obj) && has(core[obj.name], prop.name) && !scope.getBinding(obj.name)) { + if (!node.computed && coreHas(obj) && has(core[obj.name], prop.name) && !scope.getBindingIdentifier(obj.name)) { this.skip(); return t.prependToMemberExpression(node, file.get("coreIdentifier")); } - } else if (t.isReferencedIdentifier(node, parent) && !t.isMemberExpression(parent) && contains(ALIASABLE_CONSTRUCTORS, node.name) && !scope.getBinding(node.name)) { + } else if (t.isReferencedIdentifier(node, parent) && !t.isMemberExpression(parent) && contains(ALIASABLE_CONSTRUCTORS, node.name) && !scope.getBindingIdentifier(node.name)) { // Symbol() -> _core.Symbol(); new Promise -> new _core.Promise return t.memberExpression(file.get("coreIdentifier"), node); } else if (t.isCallExpression(node)) { diff --git a/lib/6to5/traversal/scope.js b/lib/6to5/traversal/scope.js index 55c758ee5c..c039c561fc 100644 --- a/lib/6to5/traversal/scope.js +++ b/lib/6to5/traversal/scope.js @@ -134,29 +134,28 @@ Scope.prototype.generateTempBasedOnNode = function (node) { return id; }; -Scope.prototype.checkBlockScopedCollisions = function (kind, key, id) { - if (kind === "param") return; - if (kind === "hoisted" && this.bindingKinds["let"][key]) return; +Scope.prototype.checkBlockScopedCollisions = function (kind, name, id) { + var local = this.getOwnBindingInfo(name); + if (!local) return; - if (this.bindingKinds["let"][key] || this.bindingKinds["const"][key]) { - throw this.file.errorWithNode(id, "Duplicate declaration " + key, TypeError); + if (kind === "param") return; + if (kind === "hoisted" && local.kind === "let") return; + + if (local.kind === "let" || local.kind === "const") { + throw this.file.errorWithNode(id, "Duplicate declaration " + name, TypeError); } }; Scope.prototype.rename = function (oldName, newName) { - var info = this.getBindingWithScope(oldName); - console.log(info); + var info = this.getBindingInfo(oldName); if (!info) return; - var binding = info.binding; + var binding = info.identifier; var scope = info.scope; - scope.references[newName] = binding; + this.clearOwnBinding(oldName); scope.bindings[newName] = binding; - delete scope.references[oldName]; - delete scope.bindings[oldName]; - binding.name = newName; scope.traverse(scope.block, { @@ -164,7 +163,7 @@ Scope.prototype.rename = function (oldName, newName) { if (t.isReferencedIdentifier(node, parent) && node.name === oldName) { node.name = newName; } else if (t.isScope(node)) { - if (t.getBinding(oldName) !== binding) { + if (t.getBindingIdentifier(oldName) !== binding) { this.skip(); } } @@ -188,11 +187,11 @@ Scope.prototype.inferType = function (node) { } if (t.isIdentifier(target)) { - return this.getType(target.name); + // todo } }; -Scope.prototype.registerType = function (key, id, node) { +Scope.prototype.getTypeAnnotation = function (key, id, node) { var type; if (id.typeAnnotation) { @@ -205,38 +204,53 @@ Scope.prototype.registerType = function (key, id, node) { if (type) { if (t.isTypeAnnotation(type)) type = type.typeAnnotation; - this.types[key] = type; + return type; } }; -Scope.prototype.register = function (node, reference, kind) { - if (t.isVariableDeclaration(node)) { - return this.registerVariableDeclaration(node); +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.registerBinding = function (kind, node) { + if (!kind) throw new ReferenceError("no `kind`"); var ids = t.getBindingIdentifiers(node); - extend(this.references, ids); + for (var name in ids) { + var id = ids[name]; - if (reference) return; + this.checkBlockScopedCollisions(kind, name, id); - for (var key in ids) { - var id = ids[key]; - - this.checkBlockScopedCollisions(kind, key, id); - - this.registerType(key, id, node); - this.bindings[key] = id; + this.bindings[name] = { + typeAnnotation: this.getTypeAnnotation(name, id, node), + identifier: id, + scope: this, + kind: kind + }; } - - var kinds = this.bindingKinds[kind]; - if (kinds) extend(kinds, ids); }; Scope.prototype.registerVariableDeclaration = function (declar) { var declars = declar.declarations; for (var i = 0; i < declars.length; i++) { - this.register(declars[i], false, declar.kind); + this.registerBinding(declars[i], declar.kind); } }; @@ -245,7 +259,7 @@ var functionVariableVisitor = { if (t.isFor(node)) { each(t.FOR_INIT_KEYS, function (key) { var declar = node[key]; - if (t.isVar(declar)) state.scope.register(declar); + if (t.isVar(declar)) state.scope.registerBinding("var", declar); }); } @@ -263,26 +277,38 @@ var functionVariableVisitor = { if (t.isExportDeclaration(node) && t.isDeclaration(node.declaration)) return; // we've ran into a declaration! - if (t.isDeclaration(node)) state.scope.register(node); + 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.hasReference(node.name)) { - state.register(node, true); + if (t.isReferencedIdentifier(node, parent) && !scope.hasBinding(node.name)) { + state.addGlobal(node); } else if (t.isLabeledStatement(node)) { - state.register(node.label, true); + state.addGlobal(node); } } }; var blockVariableVisitor = { enter: function (node, parent, scope, state) { - if (t.isFunctionDeclaration(node)) { - state.register(node, false, "hoisted"); - } else if (t.isBlockScoped(node)) { - state.register(node, false, "let"); + if (t.isFunctionDeclaration(node) || t.isBlockScoped(node)) { + state.registerDeclaration(node); } else if (t.isScope(node)) { this.skip(); } @@ -303,18 +329,8 @@ Scope.prototype.crawl = function () { } info = block._scopeInfo = { - bindingKinds: { - hoisted: object(), - "const": object(), - param: object(), - "var": object(), - "let": object(), - uid: object() - }, - - references: object(), - bindings: object(), - types: object(), + bindings: object(), + globals: object() }; extend(this, info); @@ -333,7 +349,7 @@ Scope.prototype.crawl = function () { 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.register(node, false, "let"); + if (t.isBlockScoped(node)) this.registerBinding("let", node); } if (t.isBlockStatement(block.body)) { @@ -345,7 +361,7 @@ Scope.prototype.crawl = function () { if (t.isFunctionExpression(block) && block.id) { if (!t.isProperty(this.parentBlock, { method: true })) { - this.register(block.id, null, "var"); + this.registerBinding("var", block.id); } } @@ -353,7 +369,7 @@ Scope.prototype.crawl = function () { if (t.isFunction(block)) { for (i = 0; i < block.params.length; i++) { - this.register(block.params[i], null, "param"); + this.registerBinding("param", block.params[i]); } this.traverse(block.body, blockVariableVisitor, this); } @@ -367,13 +383,13 @@ Scope.prototype.crawl = function () { // CatchClause - param if (t.isCatchClause(block)) { - this.register(block.param, null, "let"); + this.registerBinding("let", block.param); } // ComprehensionExpression - blocks if (t.isComprehensionExpression(block)) { - this.register(block, null, "let"); + this.registerBinding("let", block); } // Program, Function - var variables @@ -418,46 +434,6 @@ Scope.prototype.push = function (opts) { } }; -/** - * Walk up the scope tree until we hit a `Function` and then - * push our `node` to it's references. - * - * @param {Object} node - * @param {String} [kind] - */ - -Scope.prototype.addBindingToFunctionScope = function (node, kind) { - var scope = this.getFunctionParent(); - var ids = t.getBindingIdentifiers(node); - - extend(scope.bindings, ids); - extend(scope.references, ids); - - if (kind) extend(scope.bindingKinds[kind], ids); -}; - -/** - * Walk up the scope tree and check to see if it has a binding with the provided - * name, if it does return the binding identifier and scope. - * - * @param {String} name - * @returns {Object} { binding, scope } - */ - -Scope.prototype.getBindingWithScope = function (name) { - var scope = this; - - do { - binding = scope.getOwnBinding(name); - var if (binding) { - return { - binding: binding, - scope: scope - }; - } - } while (scope = scope.parent); -}; - /** * Walk up the scope tree until we hit either a Function or reach the * very top and hit Program. @@ -501,67 +477,61 @@ Scope.prototype.getAllBindingsOfKind = function (kind) { var scope = this; do { - defaults(ids, scope.bindingKinds[kind]); + 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.get = function (id, type) { - return id && (this.getOwn(id, type) || this.parentGet(id, type)); +Scope.prototype.bindingIdentifierEquals = function (name, node) { + return this.getBindingIdentifier(name) === node; }; -Scope.prototype.getOwn = function (id, type) { - var refs = { - reference: this.references, - binding: this.bindings, - type: this.types - }[type]; - return refs && has(refs, id) && refs[id]; +// 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.parentGet = function (id, type) { - return this.parent && this.parent.get(id, type); +Scope.prototype.getOwnBindingInfo = function (name) { + return this.bindings[name]; }; -Scope.prototype.has = function (id, type) { - if (!id) return false; - if (this.hasOwn(id, type)) return true; - if (this.parentHas(id, type)) return true; - if (contains(Scope.defaultDeclarations, id)) return true; +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 (contains(Scope.defaultDeclarations, name)) return true; return false; }; -Scope.prototype.hasOwn = function (id, type) { - return !!this.getOwn(id, type); +Scope.prototype.parentHasBinding = function (name) { + return this.parent && this.parent.hasBinding(name); }; - -Scope.prototype.parentHas = function (id, type) { - return this.parent && this.parent.has(id, type); -}; - -each({ - reference: "Reference", - binding: "Binding", - type: "Type" -}, function (title, type) { - Scope.prototype[type + "Equals"] = function (id, node) { - return this["get" + title](id) === node; - }; - - each([ - "get", - "has", - "getOwn", - "hasOwn", - "parentGet", - "parentHas", - ], function (methodName) { - Scope.prototype[methodName + title] = function (id) { - return this[methodName](id, type); - }; - }); -});