Skip to content

Commit

Permalink
Clean up scope-chain recursion handling in AbstractScope.
Browse files Browse the repository at this point in the history
In particular, remove the boolean "recurse" parameter in favor of separate methods isDeclared() and isDeclaredInOwnScope().  There are also two less-common methods, isDeclaredWithSameCfgRoot() and isDeclared(String, Predicate<Node>), which are used in a few places.  All callsites have been migrated to the appropriate variant.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=191140890
  • Loading branch information
shicks authored and lauraharker committed Apr 3, 2018
1 parent a1ad8db commit 84e18f7
Show file tree
Hide file tree
Showing 25 changed files with 178 additions and 165 deletions.
92 changes: 48 additions & 44 deletions src/com/google/javascript/jscomp/AbstractScope.java
Expand Up @@ -33,21 +33,27 @@
* points back to its parent scope. A Scope contains information about variables defined in that * points back to its parent scope. A Scope contains information about variables defined in that
* scope. * scope.
* *
* <p>ES6 introduces new scoping rules, which adds some complexity to this class. In particular, * <p>ES 2015 introduces new scoping rules, which adds some complexity to this class. In particular,
* scopes fall into four mutually exclusive categories based on their root node: block, function, * scopes fall into two mutually exclusive categories: <i>block</i> and <i>container</i>. Block
* module, or global. Function, module, and global scopes are collectively referred to as "non-block * scopes are all scopes whose roots are blocks, as well as any control structures whose optional
* scopes". We also define a scope as a "hoist scope" if it is a non-block scope *or* it is the * blocks are omitted. These scopes did not exist at all prior to ES 2015. Container scopes
* outermost block scope within a function (i.e. a "function block scope"). Hoist scopes are * comprise function scopes, global scopes, and module scopes, and (aside from modules, which
* important because "var" declarations are hoisted to the closest hoist scope, as opposed to ES6 * didn't exist in ES5) corresponds to the ES5 scope rules. This corresponds roughly to one
* "let" and "const" which are not hoisted, but instead added directly to whatever scope they're * container scope per CFG root (but not exactly, due to SCRIPT-level CFGs).
* declared in.
* *
* <p>Finally, a caution about function scopes and function block scopes: the language does not * <p>All container scopes, plus the outermost block scope within a function (i.e. the <i>function
* permit any shadowing to occur between them (with the exception of bleeding function names), so in * block scope</i>) are considered <i>hoist scopes</i>. Hoist scopes are relevant because "var"
* many situations these scopes are treated as a single scope. Under block scoping, only function * declarations are hoisted to the closest hoist scope, as opposed to "let" and "const" which always
* parameters (and optionally, bleeding function names) are declared in the function scope. It is * apply to the specific scope in which they occur.
* kept as a separate scope so that default parameter initializers may be evaluated in a separate *
* scope from the function body. * <p>Note that every function actually has two distinct hoist scopes: a container scope on the
* FUNCTION node, and a block-scope on the top-level BLOCK in the function (the "function block").
* Local variables are declared on the function block, while parameters and optionally the function
* name (if it bleeds, i.e. from a named function expression) are declared on the container scope.
* This is required so that default parameter initializers can refer to names from outside the
* function that could possibly be shadowed in the function block. But these scopes are not fully
* independent of one another, since the language does not allow a top-level local variable to
* shadow a parameter name - so in some situations these scopes must be treated as a single scope.
* *
* @see NodeTraversal * @see NodeTraversal
*/ */
Expand Down Expand Up @@ -145,6 +151,9 @@ final void clearVarsInternal() {


@Override @Override
public final V getSlot(String name) { public final V getSlot(String name) {
// TODO(sdh): This behavior is inconsistent with getOwnSlot, hasSlot, and hasOwnSlot
// when it comes to implicit vars. The other three methods all exclude implicits,
// but this one returns them. It would be good to clean this up one way or the other.
return getVar(name); return getVar(name);
} }


Expand Down Expand Up @@ -202,35 +211,21 @@ private final V getImplicitVar(ImplicitVar var, boolean allowDeclaredVars) {
return null; return null;
} }


/** /** Returns true if a variable is declared in this scope, with no recursion. */
* Use only when in a function block scope and want to tell if a name is either at the top of the public final boolean hasOwnSlot(String name) {
* function block scope or the function parameter scope. This obviously only applies to ES6 block return vars.containsKey(name);
* scopes.
*/
public final boolean isDeclaredInFunctionBlockOrParameter(String name) {
// In ES6, we create a separate "function parameter scope" above the function block scope to
// handle default parameters. Since nothing in the function block scope is allowed to shadow
// the variables in the function scope, we treat the two scopes as one in this method.
checkState(isFunctionBlockScope());
return isDeclared(name, false) || (getParent().isDeclared(name, false));
} }


/** /** Returns true if a variable is declared in this or any parent scope. */
* Returns true if a variable is declared. public final boolean hasSlot(String name) {
*/
public final boolean isDeclared(String name, boolean recurse) {
S scope = thisScope(); S scope = thisScope();
while (true) { while (scope != null) {
if (scope.getOwnSlot(name) != null) { if (scope.hasOwnSlot(name)) {
return true; return true;
} }

scope = scope.getParent();
if (scope.getParent() != null && recurse) {
scope = scope.getParent();
continue;
}
return false;
} }
return false;
} }


/** /**
Expand Down Expand Up @@ -333,11 +328,11 @@ final boolean isHoistScope() {
/** /**
* If a var were declared in this scope, return the scope it would be hoisted to. * If a var were declared in this scope, return the scope it would be hoisted to.
* *
* For function scopes, we return back the scope itself, since even though there is no way * <p>For function scopes, we return back the scope itself, since even though there is no way to
* to declare a var inside function parameters, it would make even less sense to say that * declare a var inside function parameters, it would make even less sense to say that such
* such declarations would be "hoisted" somewhere else. * declarations would be "hoisted" somewhere else.
*/ */
public final S getClosestHoistScope() { final S getClosestHoistScope() {
S current = thisScope(); S current = thisScope();
while (current != null) { while (current != null) {
if (current.isHoistScope()) { if (current.isHoistScope()) {
Expand All @@ -349,10 +344,10 @@ public final S getClosestHoistScope() {
} }


/** /**
* Returns the closest non-block scope. This is equivalent to what the current scope would have * Returns the closest container scope. This is equivalent to what the current scope would have
* been for non-block-scope creators, and is thus useful for migrating code to use block scopes. * been for non-ES6 scope creators, and is thus useful for migrating code to use block scopes.
*/ */
public final S getClosestNonBlockScope() { public final S getClosestContainerScope() {
S scope = getClosestHoistScope(); S scope = getClosestHoistScope();
if (scope.isBlockScope()) { if (scope.isBlockScope()) {
scope = scope.getParent(); scope = scope.getParent();
Expand Down Expand Up @@ -385,6 +380,15 @@ void checkRootScope() {
NodeUtil.createsScope(rootNode) || rootNode.isScript() || rootNode.isRoot(), rootNode); NodeUtil.createsScope(rootNode) || rootNode.isScript() || rootNode.isRoot(), rootNode);
} }


/**
* Whether {@code this} and the {@code other} scope have the same container scope (i.e. are in
* the same function, or else both in the global hoist scope). This is equivalent to asking
* whether the two scopes would be equivalent in a pre-ES2015-block-scopes view of the world.
*/
boolean hasSameContainerScope(S other) {
return getClosestContainerScope() == other.getClosestContainerScope();
}

/** /**
* The three implicit var types, which are defined implicitly (at least) in * The three implicit var types, which are defined implicitly (at least) in
* every vanilla function scope without actually being declared. * every vanilla function scope without actually being declared.
Expand Down
Expand Up @@ -227,7 +227,7 @@ public void visit(NodeTraversal t, Node n, Node parent) {


pseudoName = Joiner.on("_").join(allMergedNames); pseudoName = Joiner.on("_").join(allMergedNames);


while (t.getScope().isDeclared(pseudoName, true)) { while (t.getScope().hasSlot(pseudoName)) {
pseudoName += "$"; pseudoName += "$";
} }


Expand Down
2 changes: 1 addition & 1 deletion src/com/google/javascript/jscomp/Es6RenameReferences.java
Expand Up @@ -64,7 +64,7 @@ private void renameReference(NodeTraversal t, Node n) {
n.setString(newName); n.setString(newName);
t.reportCodeChange(); t.reportCodeChange();
return; return;
} else if (current.isDeclared(oldName, false)) { } else if (current.hasOwnSlot(oldName)) {
return; return;
} else { } else {
current = current.getParent(); current = current.getParent();
Expand Down
Expand Up @@ -96,10 +96,10 @@ && inLoop(n)) {
Scope hoistScope = scope.getClosestHoistScope(); Scope hoistScope = scope.getClosestHoistScope();
if (scope != hoistScope) { if (scope != hoistScope) {
String newName = oldName; String newName = oldName;
if (hoistScope.isDeclared(oldName, true) || undeclaredNames.contains(oldName)) { if (hoistScope.hasSlot(oldName) || undeclaredNames.contains(oldName)) {
do { do {
newName = oldName + "$" + compiler.getUniqueNameIdSupplier().get(); newName = oldName + "$" + compiler.getUniqueNameIdSupplier().get();
} while (hoistScope.isDeclared(newName, true)); } while (hoistScope.hasSlot(newName));
nameNode.setString(newName); nameNode.setString(newName);
compiler.reportChangeToEnclosingScope(nameNode); compiler.reportChangeToEnclosingScope(nameNode);
Node scopeRoot = scope.getRootNode(); Node scopeRoot = scope.getRootNode();
Expand Down Expand Up @@ -218,7 +218,7 @@ private void rewriteDeclsToVars() {
private class CollectUndeclaredNames extends AbstractPostOrderCallback { private class CollectUndeclaredNames extends AbstractPostOrderCallback {
@Override @Override
public void visit(NodeTraversal t, Node n, Node parent) { public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isName() && !t.getScope().isDeclared(n.getString(), true)) { if (n.isName() && !t.getScope().hasSlot(n.getString())) {
undeclaredNames.add(n.getString()); undeclaredNames.add(n.getString());
} }
} }
Expand Down
Expand Up @@ -509,7 +509,7 @@ private void maybeVisitColonType(NodeTraversal t, Node n, Node jsDocNode) {


private void visitTypeAlias(NodeTraversal t, Node n, Node parent) { private void visitTypeAlias(NodeTraversal t, Node n, Node parent) {
String alias = n.getString(); String alias = n.getString();
if (t.getScope().isDeclared(alias, true)) { if (t.getScope().hasSlot(alias)) {
compiler.report( compiler.report(
JSError.make(n, TYPE_ALIAS_ALREADY_DECLARED, alias)); JSError.make(n, TYPE_ALIAS_ALREADY_DECLARED, alias));
} }
Expand Down
Expand Up @@ -633,7 +633,7 @@ public boolean apply(Node input) {
public boolean apply(Node input) { public boolean apply(Node input) {
if (input.isName()) { if (input.isName()) {
String name = input.getString(); String name = input.getString();
if (!name.isEmpty() && !usageScope.isDeclared(name, true)) { if (!name.isEmpty() && !usageScope.hasSlot(name)) {
return true; // unsafe to inline. return true; // unsafe to inline.
} }
} }
Expand Down
18 changes: 13 additions & 5 deletions src/com/google/javascript/jscomp/LiveVariablesAnalysis.java
Expand Up @@ -368,11 +368,11 @@ private void addToSetIfLocal(Node node, BitSet set) {
// ES6 separates the scope but if the variable is declared in the param it should be local // ES6 separates the scope but if the variable is declared in the param it should be local
// to the function body. // to the function body.
if (localScope.isFunctionBlockScope()) { if (localScope.isFunctionBlockScope()) {
local = localScope.isDeclaredInFunctionBlockOrParameter(name); local = isDeclaredInFunctionBlockOrParameter(localScope, name);
} else if (localScope == jsScope && jsScopeChild != null) { } else if (localScope == jsScope && jsScopeChild != null) {
local = jsScopeChild.isDeclaredInFunctionBlockOrParameter(name); local = isDeclaredInFunctionBlockOrParameter(jsScopeChild, name);
} else { } else {
local = localScope.isDeclared(name, false); local = localScope.hasOwnSlot(name);
} }


if (!local) { if (!local) {
Expand All @@ -384,6 +384,14 @@ private void addToSetIfLocal(Node node, BitSet set) {
} }
} }


private static boolean isDeclaredInFunctionBlockOrParameter(Scope scope, String name) {
// In ES6, we create a separate container scope above the function block scope to handle
// default parameters. Since nothing in the function block scope is allowed to shadow
// the variables in the function scope, we treat the two scopes as one in this method.
checkState(scope.isFunctionBlockScope());
return scope.hasOwnSlot(name) || scope.getParent().hasOwnSlot(name);
}

/** /**
* Give up computing liveness of formal parameter by putting all the parameter names in the * Give up computing liveness of formal parameter by putting all the parameter names in the
* escaped set. * escaped set.
Expand All @@ -402,12 +410,12 @@ void markAllParametersEscaped() {
private boolean isArgumentsName(Node n) { private boolean isArgumentsName(Node n) {
boolean childDeclared; boolean childDeclared;
if (jsScopeChild != null) { if (jsScopeChild != null) {
childDeclared = jsScopeChild.isDeclared(ARGUMENT_ARRAY_ALIAS, false); childDeclared = jsScopeChild.hasOwnSlot(ARGUMENT_ARRAY_ALIAS);
} else { } else {
childDeclared = true; childDeclared = true;
} }
return n.isName() return n.isName()
&& n.getString().equals(ARGUMENT_ARRAY_ALIAS) && n.getString().equals(ARGUMENT_ARRAY_ALIAS)
&& (!jsScope.isDeclared(ARGUMENT_ARRAY_ALIAS, false) || !childDeclared); && (!jsScope.hasOwnSlot(ARGUMENT_ARRAY_ALIAS) || !childDeclared);
} }
} }
4 changes: 2 additions & 2 deletions src/com/google/javascript/jscomp/NodeTraversal.java
Expand Up @@ -967,7 +967,7 @@ public Node getClosestHoistScopeRoot() {
return scopes.peek().getClosestHoistScope().getRootNode(); return scopes.peek().getClosestHoistScope().getRootNode();
} }


public Node getClosestNonBlockScopeRoot() { public Node getClosestContainerScopeRoot() {
int roots = scopeRoots.size(); int roots = scopeRoots.size();
for (int i = roots; i > 0; i--) { for (int i = roots; i > 0; i--) {
Node rootNode = scopeRoots.get(i - 1); Node rootNode = scopeRoots.get(i - 1);
Expand All @@ -976,7 +976,7 @@ public Node getClosestNonBlockScopeRoot() {
} }
} }


return scopes.peek().getClosestNonBlockScope().getRootNode(); return scopes.peek().getClosestContainerScope().getRootNode();
} }


public AbstractScope<?, ?> getClosestHoistScope() { public AbstractScope<?, ?> getClosestHoistScope() {
Expand Down
2 changes: 1 addition & 1 deletion src/com/google/javascript/jscomp/RemoveUnusedCode.java
Expand Up @@ -235,7 +235,7 @@ private void traverseAndRemoveUnusedReferences(Node root) {
// Create scope from parent of root node, which also has externs as a child, so we'll // Create scope from parent of root node, which also has externs as a child, so we'll
// have extern definitions in scope. // have extern definitions in scope.
Scope scope = scopeCreator.createScope(root.getParent(), null); Scope scope = scopeCreator.createScope(root.getParent(), null);
if (!scope.isDeclared(NodeUtil.JSC_PROPERTY_NAME_FN, /* recurse */ true)) { if (!scope.hasSlot(NodeUtil.JSC_PROPERTY_NAME_FN)) {
// TODO(b/70730762): Passes that add references to this should ensure it is declared. // TODO(b/70730762): Passes that add references to this should ensure it is declared.
// NOTE: null input makes this an extern var. // NOTE: null input makes this an extern var.
scope.declare( scope.declare(
Expand Down
2 changes: 1 addition & 1 deletion src/com/google/javascript/jscomp/ShadowVariables.java
Expand Up @@ -247,7 +247,7 @@ private Assignment findBestShadow(Scope curScope, Var var) {
for (Assignment assignment : varsByFrequency) { for (Assignment assignment : varsByFrequency) {
if (assignment.isLocal) { if (assignment.isLocal) {
if (!scopeUpRefMap.containsEntry(curScope.getRootNode(), assignment.oldName)) { if (!scopeUpRefMap.containsEntry(curScope.getRootNode(), assignment.oldName)) {
if (curScope.isDeclared(assignment.oldName, true)) { if (curScope.hasSlot(assignment.oldName)) {
// Don't shadow if the scopes are the same eg.: // Don't shadow if the scopes are the same eg.:
// function f() { var a = 1; { var a = 2; } } // Unsafe // function f() { var a = 1; { var a = 2; } } // Unsafe
Var toShadow = curScope.getVar(assignment.oldName); Var toShadow = curScope.getVar(assignment.oldName);
Expand Down
2 changes: 1 addition & 1 deletion src/com/google/javascript/jscomp/SymbolTable.java
Expand Up @@ -1212,7 +1212,7 @@ private SymbolScope createScopeFrom(StaticScope otherScope) {
Node otherScopeRoot = otherScope.getRootNode(); Node otherScopeRoot = otherScope.getRootNode();


// NOTE: Kythe is not set up to handle block scopes yet, so only create // NOTE: Kythe is not set up to handle block scopes yet, so only create
// SymbolScopes for CFG root nodes, giving a pre-ES6 view of the world. // SymbolScopes for container scope roots, giving a pre-ES6 view of the world.
while (!NodeUtil.isValidCfgRoot(otherScopeRoot)) { while (!NodeUtil.isValidCfgRoot(otherScopeRoot)) {
otherScopeRoot = otherScopeRoot.getParent(); otherScopeRoot = otherScopeRoot.getParent();
} }
Expand Down
Expand Up @@ -186,7 +186,7 @@ private void declareVar(Node n) {


CompilerInput input = compiler.getInput(inputId); CompilerInput input = compiler.getInput(inputId);
String name = n.getString(); String name = n.getString();
if (!scope.isDeclared(name, false) && !(scope.isLocal() && name.equals(Var.ARGUMENTS))) { if (!scope.hasOwnSlot(name) && (!scope.isLocal() || !name.equals(Var.ARGUMENTS))) {
if (isTyped) { if (isTyped) {
((TypedScope) scope).declare(name, n, null, input); ((TypedScope) scope).declare(name, n, null, input);
} else { } else {
Expand Down
5 changes: 2 additions & 3 deletions src/com/google/javascript/jscomp/TransformAMDToCJSModule.java
Expand Up @@ -181,11 +181,10 @@ private void handleRequire(NodeTraversal t, Node defineNode, Node script,


String aliasName = aliasNode != null ? aliasNode.getString() : null; String aliasName = aliasNode != null ? aliasNode.getString() : null;
Scope globalScope = t.getScope(); Scope globalScope = t.getScope();
if (aliasName != null && if (aliasName != null && globalScope.hasSlot(aliasName)) {
globalScope.isDeclared(aliasName, true)) {
while (true) { while (true) {
String renamed = aliasName + VAR_RENAME_SUFFIX + renameIndex; String renamed = aliasName + VAR_RENAME_SUFFIX + renameIndex;
if (!globalScope.isDeclared(renamed, true)) { if (!globalScope.hasSlot(renamed)) {
NodeTraversal.traverseEs6(compiler, callback, NodeTraversal.traverseEs6(compiler, callback,
new RenameCallback(aliasName, renamed)); new RenameCallback(aliasName, renamed));
aliasName = renamed; aliasName = renamed;
Expand Down
14 changes: 6 additions & 8 deletions src/com/google/javascript/jscomp/TypeCheck.java
Expand Up @@ -483,17 +483,15 @@ public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
case FUNCTION: case FUNCTION:
// normal type checking // normal type checking
final TypedScope outerScope = t.getTypedScope(); final TypedScope outerScope = t.getTypedScope();
final String functionPrivateName = n.getFirstChild().getString(); final TypedVar var = outerScope.getVar(n.getFirstChild().getString());
if (functionPrivateName != null if (var != null
&& functionPrivateName.length() > 0 && var.getScope().hasSameContainerScope(outerScope)
&& outerScope.isDeclared(functionPrivateName, false)
// Ideally, we would want to check whether the type in the scope // Ideally, we would want to check whether the type in the scope
// differs from the type being defined, but then the extern // differs from the type being defined, but then the extern
// redeclarations of built-in types generates spurious warnings. // redeclarations of built-in types generates spurious warnings.
&& !(outerScope.getVar(functionPrivateName).getType() instanceof FunctionType) && !(var.getType() instanceof FunctionType)
&& !TypeValidator.hasDuplicateDeclarationSuppression( && !TypeValidator.hasDuplicateDeclarationSuppression(compiler, var.getNameNode())) {
compiler, outerScope.getVar(functionPrivateName).getNameNode())) { report(t, n, FUNCTION_MASKS_VARIABLE, var.getName());
report(t, n, FUNCTION_MASKS_VARIABLE, functionPrivateName);
} }


// TODO(user): Only traverse the function's body. The function's // TODO(user): Only traverse the function's body. The function's
Expand Down

0 comments on commit 84e18f7

Please sign in to comment.