var assert = require(“assert”); var types = require(“./types”); var Type = types.Type; var namedTypes = types.namedTypes; var Node = namedTypes.Node; var isArray = types.builtInTypes.array; var hasOwn = Object.prototype.hasOwnProperty; var b = types.builders;
function Scope(path, parentScope) {
assert.ok(this instanceof Scope); assert.ok(path instanceof require("./node-path")); ScopeType.assert(path.value); var depth; if (parentScope) { assert.ok(parentScope instanceof Scope); depth = parentScope.depth + 1; } else { parentScope = null; depth = 0; } Object.defineProperties(this, { path: { value: path }, node: { value: path.value }, isGlobal: { value: !parentScope, enumerable: true }, depth: { value: depth }, parent: { value: parentScope }, bindings: { value: {} } });
}
var scopeTypes = [
// Program nodes introduce global scopes. namedTypes.Program, // Function is the supertype of FunctionExpression, // FunctionDeclaration, ArrowExpression, etc. namedTypes.Function, // In case you didn't know, the caught parameter shadows any variable // of the same name in an outer scope. namedTypes.CatchClause
];
var ScopeType = Type.or.apply(Type, scopeTypes);
Scope.isEstablishedBy = function(node) {
return ScopeType.check(node);
};
var Sp = Scope.prototype;
// Will be overridden after an instance lazily calls scanScope. Sp.didScan = false;
Sp.declares = function(name) {
this.scan(); return hasOwn.call(this.bindings, name);
};
Sp.declareTemporary = function(prefix) {
if (prefix) { assert.ok(/^[a-z$_]/i.test(prefix), prefix); } else { prefix = "t$"; } // Include this.depth in the name to make sure the name does not // collide with any variables in nested/enclosing scopes. prefix += this.depth.toString(36) + "$"; this.scan(); var index = 0; while (this.declares(prefix + index)) { ++index; } var name = prefix + index; return this.bindings[name] = types.builders.identifier(name);
};
Sp.injectTemporary = function(identifier, init) {
identifier || (identifier = this.declareTemporary()); var bodyPath = this.path.get("body"); if (namedTypes.BlockStatement.check(bodyPath.value)) { bodyPath = bodyPath.get("body"); } bodyPath.unshift( b.variableDeclaration( "var", [b.variableDeclarator(identifier, init || null)] ) ); return identifier;
};
Sp.scan = function(force) {
if (force || !this.didScan) { for (var name in this.bindings) { // Empty out this.bindings, just in cases. delete this.bindings[name]; } scanScope(this.path, this.bindings); this.didScan = true; }
};
Sp.getBindings = function () {
this.scan(); return this.bindings;
};
function scanScope(path, bindings) {
var node = path.value; ScopeType.assert(node); if (namedTypes.CatchClause.check(node)) { // A catch clause establishes a new scope but the only variable // bound in that scope is the catch parameter. Any other // declarations create bindings in the outer scope. addPattern(path.get("param"), bindings); } else { recursiveScanScope(path, bindings); }
}
function recursiveScanScope(path, bindings) {
var node = path.value; if (path.parent && namedTypes.FunctionExpression.check(path.parent.node) && path.parent.node.id) { addPattern(path.parent.get("id"), bindings); } if (!node) { // None of the remaining cases matter if node is falsy. } else if (isArray.check(node)) { path.each(function(childPath) { recursiveScanChild(childPath, bindings); }); } else if (namedTypes.Function.check(node)) { path.get("params").each(function(paramPath) { addPattern(paramPath, bindings); }); recursiveScanChild(path.get("body"), bindings); } else if (namedTypes.VariableDeclarator.check(node)) { addPattern(path.get("id"), bindings); recursiveScanChild(path.get("init"), bindings); } else if (node.type === "ImportSpecifier" || node.type === "ImportNamespaceSpecifier" || node.type === "ImportDefaultSpecifier") { addPattern( node.name ? path.get("name") : path.get("id"), bindings ); } else if (Node.check(node)) { types.eachField(node, function(name, child) { var childPath = path.get(name); assert.strictEqual(childPath.value, child); recursiveScanChild(childPath, bindings); }); }
}
function recursiveScanChild(path, bindings) {
var node = path.value; if (!node) { // None of the remaining cases matter if node is falsy. } else if (namedTypes.FunctionDeclaration.check(node)) { addPattern(path.get("id"), bindings); } else if (namedTypes.ClassDeclaration && namedTypes.ClassDeclaration.check(node)) { addPattern(path.get("id"), bindings); } else if (Scope.isEstablishedBy(node)) { if (namedTypes.CatchClause.check(node)) { var catchParamName = node.param.name; var hadBinding = hasOwn.call(bindings, catchParamName); // Any declarations that occur inside the catch body that do // not have the same name as the catch parameter should count // as bindings in the outer scope. recursiveScanScope(path.get("body"), bindings); // If a new binding matching the catch parameter name was // created while scanning the catch body, ignore it because it // actually refers to the catch parameter and not the outer // scope that we're currently scanning. if (!hadBinding) { delete bindings[catchParamName]; } } } else { recursiveScanScope(path, bindings); }
}
function addPattern(patternPath, bindings) {
var pattern = patternPath.value; namedTypes.Pattern.assert(pattern); if (namedTypes.Identifier.check(pattern)) { if (hasOwn.call(bindings, pattern.name)) { bindings[pattern.name].push(patternPath); } else { bindings[pattern.name] = [patternPath]; } } else if (namedTypes.SpreadElement && namedTypes.SpreadElement.check(pattern)) { addPattern(patternPath.get("argument"), bindings); }
}
Sp.lookup = function(name) {
for (var scope = this; scope; scope = scope.parent) if (scope.declares(name)) break; return scope;
};
Sp.getGlobalScope = function() {
var scope = this; while (!scope.isGlobal) scope = scope.parent; return scope;
};
module.exports = Scope;