From 17b34a29591d4ed5483a5acd83e4d831c02a72a0 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Mon, 9 Mar 2015 22:07:05 +1100 Subject: [PATCH] dynamic scope tracking, toot toot - fixes #957 --- .../transformers/internal/modules.js | 9 +- src/babel/traversal/path.js | 87 ++++--- src/babel/traversal/scope.js | 228 +++++++++++++++++- src/babel/types/alias-keys.json | 6 +- src/babel/types/index.js | 2 + 5 files changed, 279 insertions(+), 53 deletions(-) diff --git a/src/babel/transformation/transformers/internal/modules.js b/src/babel/transformation/transformers/internal/modules.js index d0be865e6e..4e63035e78 100644 --- a/src/babel/transformation/transformers/internal/modules.js +++ b/src/babel/transformation/transformers/internal/modules.js @@ -32,9 +32,11 @@ export function ExportDeclaration(node, parent, scope) { if (node.default) { if (t.isClassDeclaration(declar)) { + // export default class Foo {}; + this.node = [getDeclar(), node]; node.declaration = declar.id; - return [getDeclar(), node]; } else if (t.isClassExpression(declar)) { + // export default class {}; var temp = scope.generateUidIdentifier("default"); declar = t.variableDeclaration("var", [ t.variableDeclarator(temp, declar) @@ -42,25 +44,26 @@ export function ExportDeclaration(node, parent, scope) { node.declaration = temp; return [getDeclar(), node]; } else if (t.isFunctionDeclaration(declar)) { + // export default function Foo() {} node._blockHoist = 2; node.declaration = declar.id; return [getDeclar(), node]; } } else { if (t.isFunctionDeclaration(declar)) { + // export function Foo() {} node.specifiers = [t.importSpecifier(declar.id, declar.id)]; node.declaration = null; node._blockHoist = 2; return [getDeclar(), node]; } else if (t.isVariableDeclaration(declar)) { + // export var foo = "bar"; var specifiers = []; - var bindings = t.getBindingIdentifiers(declar); for (var key in bindings) { var id = bindings[key]; specifiers.push(t.exportSpecifier(id, id)); } - return [declar, t.exportDeclaration(null, specifiers)]; } } diff --git a/src/babel/traversal/path.js b/src/babel/traversal/path.js index 41d6a9934f..6aa5364838 100644 --- a/src/babel/traversal/path.js +++ b/src/babel/traversal/path.js @@ -56,15 +56,9 @@ export default class TraversalPath { this.scope = TraversalPath.getScope(this.node, this.parent, this.context.scope); } - refreshScope() { - // todo: remove all binding identifiers associated with this node - // todo: if it hasn't been deleted and just replaced with node/s then add their bindings - } - setContext(parentPath, context, key) { - this.shouldRemove = false; - this.shouldSkip = false; - this.shouldStop = false; + this.shouldSkip = false; + this.shouldStop = false; this.parentPath = parentPath || this.parentPath; this.context = context; @@ -76,8 +70,9 @@ export default class TraversalPath { } remove() { - this.shouldRemove = true; - this.shouldSkip = true; + this.refreshScope(this.node, []); + this.container[this.key] = null; + this.flatten(); } skip() { @@ -93,30 +88,58 @@ export default class TraversalPath { this.context.flatten(); } + refreshScope(oldNode, newNodes) { + var scope = this.scope; + if (!scope) return; + + if (!t.isAssignmentExpression(oldNode)) { + var bindings = t.getBindingIdentifiers(oldNode); + for (var key in bindings) { + if (scope.bindingIdentifierEquals(key, bindings[key])) { + scope.removeBinding(key); + } + } + } + + for (let i = 0; i < newNodes.length; i++) { + var newNode = newNodes[i]; + scope.refreshDeclaration(newNode); + } + } + + refresh() { + var node = this.node; + this.refreshScope(node, [node]); + } + get node() { return this.container[this.key]; } set node(replacement) { - var isArray = Array.isArray(replacement); + if (!replacement) return this.remove(); + + var oldNode = this.node; + var isArray = Array.isArray(replacement); + var replacements = isArray ? replacement : [replacement]; // inherit comments from original node to the first replacement node - var inheritTo = replacement; - if (isArray) inheritTo = replacement[0]; - if (inheritTo) t.inheritsComments(inheritTo, this.node); + var inheritTo = replacements[0]; + if (inheritTo) t.inheritsComments(inheritTo, oldNode); // replace the node this.container[this.key] = replacement; + + // potentially create new scope this.setScope(); + // refresh scope with new/removed bindings + this.refreshScope(oldNode, replacements); + var file = this.scope && this.scope.file; if (file) { - if (isArray) { - for (var i = 0; i < replacement.length; i++) { - file.checkNode(replacement[i], this.scope); - } - } else { - file.checkNode(replacement, this.scope); + for (var i = 0; i < replacements.length; i++) { + file.checkNode(replacements[i], this.scope); } } @@ -145,11 +168,6 @@ export default class TraversalPath { if (replacement) { this.node = replacement; } - - if (this.shouldRemove) { - this.container[this.key] = null; - this.flatten(); - } } isBlacklisted() { @@ -169,16 +187,17 @@ export default class TraversalPath { var node = this.node; var opts = this.opts; - if (Array.isArray(node)) { - // traverse over these replacement nodes we purposely don't call exitNode - // as the original node has been destroyed - for (var i = 0; i < node.length; i++) { - traverse.node(node[i], opts, this.scope, this.state, this); + if (node) { + if (Array.isArray(node)) { + // traverse over these replacement nodes we purposely don't call exitNode + // as the original node has been destroyed + for (var i = 0; i < node.length; i++) { + traverse.node(node[i], opts, this.scope, this.state, this); + } + } else { + traverse.node(node, opts, this.scope, this.state, this); + this.call("exit"); } - } else { - traverse.node(node, opts, this.scope, this.state, this); - - this.call("exit"); } return this.shouldStop; diff --git a/src/babel/traversal/scope.js b/src/babel/traversal/scope.js index cb18153365..1ab1be6245 100644 --- a/src/babel/traversal/scope.js +++ b/src/babel/traversal/scope.js @@ -208,6 +208,14 @@ export default class Scope { return id; } + /** + * Description + * + * @param {String} kind + * @param {String} name + * @param {Node} id + */ + checkBlockScopedCollisions(kind, name, id) { var local = this.getOwnBindingInfo(name); if (!local) return; @@ -220,6 +228,13 @@ export default class Scope { } } + /** + * Description + * + * @param {String} oldName + * @param {String} newName + */ + rename(oldName, newName) { newName ||= this.generateUidIdentifier(oldName).name; @@ -246,12 +261,18 @@ export default class Scope { } }); - this.clearOwnBinding(oldName); + scope.removeOwnBinding(oldName); scope.bindings[newName] = info; binding.name = newName; } + /** + * Description + * + * @param {Node} node + */ + inferType(node) { var target; @@ -284,6 +305,13 @@ export default class Scope { } } + /** + * Description + * + * @param {String} name + * @param {String} genericName + */ + isTypeGeneric(name, genericName) { var info = this.getBindingInfo(name); if (!info) return false; @@ -292,10 +320,24 @@ export default class Scope { return t.isGenericTypeAnnotation(type) && t.isIdentifier(type.id, { name: genericName }); } + /** + * Description + * + * @param {String} name + * @param {Node} type + */ + assignTypeGeneric(name, type) { this.assignType(name, t.genericTypeAnnotation(t.identifier(type))); } + /** + * Description + * + * @param {String} name + * @param {Node} type + */ + assignType(name, type) { var info = this.getBindingInfo(name); if (!info) return; @@ -303,6 +345,14 @@ export default class Scope { info.typeAnnotation = type; } + /** + * Description + * + * @param name + * @param id + * @param {Node} node + */ + getTypeAnnotation(name, id, node) { var info = { annotation: null, @@ -328,6 +378,13 @@ export default class Scope { return info; } + /** + * Description + * + * @param {Node} node + * @param {Number} [i] + */ + toArray(node, i) { var file = this.file; @@ -354,10 +411,28 @@ export default class Scope { return t.callExpression(file.addHelper(helperName), args); } - clearOwnBinding(name) { - delete this.bindings[name]; + /** + * Description + * + * @param {Node} node + */ + + refreshDeclaration(node) { + if (t.isBlockScoped(node)) { + this.getBlockParent().registerDeclaration(node); + } else if (t.isVariableDeclaration(node, { kind: "var" })) { + this.getFunctionParent().registerDeclaration(node); + } else if (node === this.block) { + this.recrawl(); + } } + /** + * Description + * + * @param {Node} node + */ + registerDeclaration(node) { if (t.isFunctionDeclaration(node)) { this.registerBinding("hoisted", node); @@ -374,6 +449,12 @@ export default class Scope { } } + /** + * Description + * + * @param {Node} node + */ + registerBindingReassignment(node) { var ids = t.getBindingIdentifiers(node); for (var name in ids) { @@ -389,6 +470,13 @@ export default class Scope { } } + /** + * Description + * + * @param {String} kind + * @param {Node} node + */ + registerBinding(kind, node) { if (!kind) throw new ReferenceError("no `kind`"); @@ -413,16 +501,21 @@ export default class Scope { } } - registerVariableDeclaration(declar) { - var declars = declar.declarations; - for (var i = 0; i < declars.length; i++) { - this.registerBinding(declar.kind, declars[i]); - } - } + /** + * Description + * + * @param {Node} node + */ addGlobal(node) { this.globals[node.name] = node; - }; + } + + /** + * Description + * + * @param {String} name + */ hasGlobal(name) { var scope = this; @@ -434,6 +527,19 @@ export default class Scope { return false; } + /** + * Description + */ + + recrawl() { + this.block._scopeInfo = null; + this.crawl(); + } + + /** + * Description + */ + crawl() { var block = this.block; var i; @@ -474,6 +580,12 @@ export default class Scope { } } + // Class + + if (t.isClass(block) && block.id) { + this.registerBinding("var", block.id); + } + // Function - params, rest if (t.isFunction(block)) { @@ -556,6 +668,19 @@ export default class Scope { return scope; } + /** + * Walk up the scope tree until we hit either a BlockStatement/Loop or reach the + * very top and hit Program. + */ + + getBlockParent() { + var scope = this; + while (scope.parent && !t.isFunction(scope.block) && !t.isLoop(scope.block) && !t.isFunction(scope.block)) { + scope = scope.parent; + } + return scope; + } + /** * Walks the scope tree and gathers **all** bindings. * @@ -596,13 +721,23 @@ export default class Scope { return ids; } - // misc + /** + * Description + * + * @param {String} name + * @param {Object} node + * @returns {Boolean} + */ bindingIdentifierEquals(name, node) { return this.getBindingIdentifier(name) === node; } - // get + /** + * Description + * + * @param {String} name + */ getBindingInfo(name) { var scope = this; @@ -613,25 +748,54 @@ export default class Scope { } while (scope = scope.parent); } + /** + * Description + * + * @param {String} name + */ + getOwnBindingInfo(name) { return this.bindings[name]; } + /** + * Description + * + * @param {String} name + */ + getBindingIdentifier(name) { var info = this.getBindingInfo(name); return info && info.identifier; } + /** + * Description + * + * @param {String} name + */ + getOwnBindingIdentifier(name) { var binding = this.bindings[name]; return binding && binding.identifier; } + /** + * Description + * + * @param {String} name + */ getOwnImmutableBindingValue(name) { return this._immutableBindingInfoToValue(this.getOwnBindingInfo(name)); } + /** + * Description + * + * @param {String} name + */ + getImmutableBindingValue(name) { return this._immutableBindingInfoToValue(this.getBindingInfo(name)); } @@ -658,12 +822,22 @@ export default class Scope { } } - // has + /** + * Description + * + * @param {String} name + */ hasOwnBinding(name) { return !!this.getOwnBindingInfo(name); } + /** + * Description + * + * @param {String} name + */ + hasBinding(name) { if (!name) return false; if (this.hasOwnBinding(name)) return true; @@ -673,7 +847,35 @@ export default class Scope { return false; } + /** + * Description + * + * @param {String} name + */ + parentHasBinding(name) { return this.parent && this.parent.hasBinding(name); } + + /** + * Description + * + * @param {String} name + */ + + removeOwnBinding(name) { + delete this.bindings[name]; + } + + /** + * Description + * + * @param {String} name + */ + + removeBinding(name) { + var info = this.getBindingInfo(name); + if (!info) return; + info.scope.removeOwnBinding(name); + } } diff --git a/src/babel/types/alias-keys.json b/src/babel/types/alias-keys.json index 05d265b7dd..6443230ed0 100644 --- a/src/babel/types/alias-keys.json +++ b/src/babel/types/alias-keys.json @@ -25,7 +25,7 @@ "ImportSpecifier": ["ModuleSpecifier"], "ExportSpecifier": ["ModuleSpecifier"], - "BlockStatement": ["Statement", "Scopable"], + "BlockStatement": ["Scopable", "Statement"], "Program": ["Scopable"], "CatchClause": ["Scopable"], @@ -36,8 +36,8 @@ "SpreadProperty": ["UnaryLike"], "SpreadElement": ["UnaryLike"], - "ClassDeclaration": ["Statement", "Declaration", "Class"], - "ClassExpression": ["Class", "Expression"], + "ClassDeclaration": ["Scope", "Class", "Statement", "Declaration"], + "ClassExpression": ["Scope", "Class", "Expression"], "ForOfStatement": ["Scopable", "Statement", "For", "Loop"], "ForInStatement": ["Scopable", "Statement", "For", "Loop"], diff --git a/src/babel/types/index.js b/src/babel/types/index.js index 55cea3ac3d..66c30a2358 100644 --- a/src/babel/types/index.js +++ b/src/babel/types/index.js @@ -658,7 +658,9 @@ t.getBindingIdentifiers.keys = { ImportBatchSpecifier: ["name"], VariableDeclarator: ["id"], FunctionDeclaration: ["id"], + FunctionExpression: ["id"], ClassDeclaration: ["id"], + ClassExpression: ["id"], SpreadElement: ["argument"], RestElement: ["argument"], UpdateExpression: ["argument"],