Normalize.java
/*
* Copyright 2008 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Preconditions;
import com.google.javascript.jscomp.AbstractCompiler.LifeCycleStage;
import com.google.javascript.jscomp.MakeDeclaredNamesUnique.BoilerplateRenamer;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.Callback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* The goal with this pass is to simplify the other passes, by making less complex statements.
*
* <p>Starting with statements like: {@code var a = 0, b = foo();}
*
* <p>Which become: {@code var a = 0; var b = foo();}
*
* <p>The key here is only to break down things that help the other passes and can be put back
* together in a form that is at least as small when all is said and done.
*
* <p>This pass currently does the following:
*
* <ol>
* <li>Simplifies the AST by splitting var/let/const statements, moving initializers out of for
* loops, and converting whiles to fors.
* <li>Moves hoisted functions to the top of function scopes.
* <li>Rewrites unhoisted named function declarations to be var declarations.
* <li>Makes all variable names globally unique (extern or otherwise) so that no value is ever
* shadowed (note: "arguments" may require special handling).
* <li>Removes duplicate variable declarations.
* <li>Marks constants with the IS_CONSTANT_NAME annotation.
* <li>Finds properties marked @expose, and rewrites them in [] notation.
* <li>Rewrite body of arrow function as a block.
* <li>Take var statements out from for-loop initializer.
* This: for(var a = 0;a<0;a++) {} becomes: var a = 0; for(var a;a<0;a++) {}
* </ol>
*
* @author johnlenz@google.com (johnlenz)
*/
class Normalize implements CompilerPass {
private final AbstractCompiler compiler;
private final boolean assertOnChange;
Normalize(AbstractCompiler compiler, boolean assertOnChange) {
this.compiler = compiler;
this.assertOnChange = assertOnChange;
// TODO(nicksantos): assertOnChange should only be true if the tree
// is normalized.
}
static void normalizeSyntheticCode(
AbstractCompiler compiler, Node js, String prefix) {
NodeTraversal.traverseEs6(compiler, js,
new Normalize.NormalizeStatements(compiler, false));
NodeTraversal.traverseEs6(
compiler,
js,
new MakeDeclaredNamesUnique(
new BoilerplateRenamer(
compiler.getCodingConvention(),
compiler.getUniqueNameIdSupplier(),
prefix)));
}
static Node parseAndNormalizeTestCode(
AbstractCompiler compiler, String code) {
Node js = compiler.parseTestCode(code);
NodeTraversal.traverseEs6(compiler, js,
new Normalize.NormalizeStatements(compiler, false));
return js;
}
private void reportCodeChange(String changeDescription, Node n) {
if (assertOnChange) {
throw new IllegalStateException(
"Normalize constraints violated:\n" + changeDescription);
}
compiler.reportChangeToEnclosingScope(n);
}
@Override
public void process(Node externs, Node root) {
NodeTraversal.traverseEs6(compiler, root, new RemoveEmptyClassMembers());
NodeTraversal.traverseRootsEs6(
compiler, new NormalizeStatements(compiler, assertOnChange), externs, root);
removeDuplicateDeclarations(externs, root);
MakeDeclaredNamesUnique renamer = new MakeDeclaredNamesUnique();
NodeTraversal.traverseRootsEs6(compiler, renamer, externs, root);
new PropagateConstantAnnotationsOverVars(compiler, assertOnChange)
.process(externs, root);
FindExposeAnnotations findExposeAnnotations = new FindExposeAnnotations();
NodeTraversal.traverseEs6(compiler, root, findExposeAnnotations);
if (!findExposeAnnotations.exposedProperties.isEmpty()) {
NodeTraversal.traverseEs6(compiler, root,
new RewriteExposedProperties(
findExposeAnnotations.exposedProperties));
}
if (!compiler.getLifeCycleStage().isNormalized()) {
compiler.setLifeCycleStage(LifeCycleStage.NORMALIZED);
}
}
private class RemoveEmptyClassMembers extends AbstractPostOrderCallback {
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isEmpty() && parent.isClassMembers()) {
reportCodeChange("empty member in class", n);
n.detach();
}
}
}
/**
* Find all the @expose annotations.
*/
private static class FindExposeAnnotations extends AbstractPostOrderCallback {
private final Set<String> exposedProperties = new HashSet<>();
@Override public void visit(NodeTraversal t, Node n, Node parent) {
if (NodeUtil.isExprAssign(n)) {
Node assign = n.getFirstChild();
Node lhs = assign.getFirstChild();
if (lhs.isGetProp() && isMarkedExpose(assign)) {
exposedProperties.add(lhs.getLastChild().getString());
}
} else if (n.isStringKey() && isMarkedExpose(n)) {
exposedProperties.add(n.getString());
} else if (n.isGetProp() && n.getParent().isExprResult()
&& isMarkedExpose(n)) {
exposedProperties.add(n.getLastChild().getString());
}
}
private static boolean isMarkedExpose(Node n) {
JSDocInfo info = n.getJSDocInfo();
return info != null && info.isExpose();
}
}
/**
* Rewrite all exposed properties in [] form.
*/
private class RewriteExposedProperties
extends AbstractPostOrderCallback {
private final Set<String> exposedProperties;
RewriteExposedProperties(Set<String> exposedProperties) {
this.exposedProperties = exposedProperties;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isGetProp()) {
String propName = n.getLastChild().getString();
if (exposedProperties.contains(propName)) {
Node obj = n.removeFirstChild();
Node prop = n.removeFirstChild();
compiler.reportChangeToEnclosingScope(n);
n.replaceWith(IR.getelem(obj, prop));
}
} else if (n.isStringKey()) {
String propName = n.getString();
if (exposedProperties.contains(propName)) {
if (!n.isQuotedString()) {
compiler.reportChangeToEnclosingScope(n);
n.setQuotedString();
}
}
}
}
}
/**
* Propagate constant annotations over the Var graph.
*/
static class PropagateConstantAnnotationsOverVars
extends AbstractPostOrderCallback
implements CompilerPass {
private final AbstractCompiler compiler;
private final boolean assertOnChange;
PropagateConstantAnnotationsOverVars(
AbstractCompiler compiler, boolean forbidChanges) {
this.compiler = compiler;
this.assertOnChange = forbidChanges;
}
@Override
public void process(Node externs, Node root) {
NodeTraversal.traverseRootsEs6(compiler, this, externs, root);
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
// Note: Constant properties annotations are not propagated.
if (n.isName() || n.isStringKey()) {
if (n.getString().isEmpty()) {
return;
}
JSDocInfo info = null;
// Find the JSDocInfo for a top-level variable.
Var var = t.getScope().getVar(n.getString());
if (var != null) {
info = var.getJSDocInfo();
}
boolean shouldBeConstant =
(info != null && info.isConstant())
|| NodeUtil.isConstantByConvention(compiler.getCodingConvention(), n);
boolean isMarkedConstant = n.getBooleanProp(Node.IS_CONSTANT_NAME);
if (shouldBeConstant && !isMarkedConstant) {
if (assertOnChange) {
String name = n.getString();
throw new IllegalStateException(
"Unexpected const change.\n"
+ " name: " + name + "\n"
+ " parent:" + n.getParent().toStringTree());
}
n.putBooleanProp(Node.IS_CONSTANT_NAME, true);
}
}
}
}
/**
* Walk the AST tree and verify that constant names are used consistently.
*/
static class VerifyConstants extends AbstractPostOrderCallback
implements CompilerPass {
private final AbstractCompiler compiler;
private final boolean checkUserDeclarations;
VerifyConstants(AbstractCompiler compiler, boolean checkUserDeclarations) {
this.compiler = compiler;
this.checkUserDeclarations = checkUserDeclarations;
}
@Override
public void process(Node externs, Node root) {
Node externsAndJs = root.getParent();
checkState(externsAndJs != null);
checkState(externsAndJs.hasChild(externs));
NodeTraversal.traverseRootsEs6(compiler, this, externs, root);
}
private final Map<String, Boolean> constantMap = new HashMap<>();
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (n.isName()) {
String name = n.getString();
if (n.getString().isEmpty()) {
return;
}
boolean isConst = n.getBooleanProp(Node.IS_CONSTANT_NAME);
if (checkUserDeclarations) {
boolean expectedConst = false;
CodingConvention convention = compiler.getCodingConvention();
if (NodeUtil.isConstantName(n)
|| NodeUtil.isConstantByConvention(convention, n)) {
expectedConst = true;
} else {
expectedConst = false;
JSDocInfo info = null;
Var var = t.getScope().getVar(n.getString());
if (var != null) {
info = var.getJSDocInfo();
}
if (info != null && info.isConstant()) {
expectedConst = true;
} else {
expectedConst = false;
}
}
if (expectedConst) {
Preconditions.checkState(expectedConst == isConst,
"The name %s is not annotated as constant.", name);
} else {
Preconditions.checkState(expectedConst == isConst,
"The name %s should not be annotated as constant.", name);
}
}
Boolean value = constantMap.get(name);
if (value == null) {
constantMap.put(name, isConst);
} else {
Preconditions.checkState(value.booleanValue() == isConst,
"The name %s is not consistently annotated as constant.", name);
}
}
}
}
/**
* Simplify the AST:
* - VAR declarations split, so they represent exactly one child
* declaration.
* - WHILEs are converted to FORs
* - FOR loop are initializers are moved out of the FOR structure
* - LABEL node of children other than LABEL, BLOCK, WHILE, FOR, or DO are
* moved into a block.
* - Add constant annotations based on coding convention.
*/
static class NormalizeStatements implements Callback {
private final AbstractCompiler compiler;
private final boolean assertOnChange;
NormalizeStatements(AbstractCompiler compiler, boolean assertOnChange) {
this.compiler = compiler;
this.assertOnChange = assertOnChange;
}
private void reportCodeChange(String changeDescription, Node n) {
if (assertOnChange) {
throw new IllegalStateException(
"Normalize constraints violated:\n" + changeDescription);
}
compiler.reportChangeToEnclosingScope(n);
}
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
doStatementNormalizations(n);
return true;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
switch (n.getToken()) {
case WHILE:
Node expr = n.getFirstChild();
n.setToken(Token.FOR);
Node empty = IR.empty();
empty.useSourceInfoIfMissingFrom(n);
n.addChildBefore(empty, expr);
n.addChildAfter(empty.cloneNode(), expr);
reportCodeChange("WHILE node", n);
break;
case FUNCTION:
if (visitFunction(n, compiler)) {
reportCodeChange("Function declaration", n);
}
break;
case EXPORT:
splitExportDeclaration(n);
break;
case NAME:
case STRING:
case GETTER_DEF:
case SETTER_DEF:
annotateConstantsByConvention(n, parent);
break;
case CAST:
compiler.reportChangeToEnclosingScope(n);
parent.replaceChild(n, n.removeFirstChild());
break;
default:
break;
}
}
/**
* Mark names and properties that are constants by convention.
*/
private void annotateConstantsByConvention(Node n, Node parent) {
checkState(
n.isName() || n.isString() || n.isStringKey() || n.isGetterDef() || n.isSetterDef());
// Need to check that variables have not been renamed, to determine whether
// coding conventions still apply.
if (compiler.getLifeCycleStage().isNormalizedObfuscated()) {
return;
}
// There are only two cases where a string token
// may be a variable reference: The right side of a GETPROP
// or an OBJECTLIT key.
boolean isObjLitKey = NodeUtil.isObjectLitKey(n);
boolean isProperty = isObjLitKey || (parent.isGetProp() && parent.getLastChild() == n);
if (n.isName() || isProperty) {
boolean isMarkedConstant = n.getBooleanProp(Node.IS_CONSTANT_NAME);
if (!isMarkedConstant
&& NodeUtil.isConstantByConvention(compiler.getCodingConvention(), n)) {
if (assertOnChange) {
String name = n.getString();
throw new IllegalStateException(
"Unexpected const change.\n"
+ " name: "
+ name
+ "\n"
+ " parent:"
+ n.getParent().toStringTree());
}
n.putBooleanProp(Node.IS_CONSTANT_NAME, true);
}
}
}
/**
* Splits ES6 export combined with a variable or function declaration.
*
*/
private void splitExportDeclaration(Node n) {
if (n.getBooleanProp(Node.EXPORT_DEFAULT)) {
return;
}
Node c = n.getFirstChild();
if (NodeUtil.isDeclaration(c)) {
n.removeChild(c);
Node exportSpecs = new Node(Token.EXPORT_SPECS).srcref(n);
n.addChildToFront(exportSpecs);
Iterable<Node> names;
if (c.isClass() || c.isFunction()) {
names = Collections.singleton(c.getFirstChild());
n.getParent().addChildBefore(c, n);
} else {
names = NodeUtil.findLhsNodesInNode(c);
// Split up var declarations onto separate lines.
for (Node child : c.children()) {
c.removeChild(child);
Node newDeclaration = new Node(c.getToken(), child).srcref(n);
n.getParent().addChildBefore(newDeclaration, n);
}
}
for (Node name : names) {
Node exportSpec = new Node(Token.EXPORT_SPEC).srcref(name);
exportSpec.addChildToFront(name.cloneNode());
exportSpec.addChildToFront(name.cloneNode());
exportSpecs.addChildToBack(exportSpec);
}
compiler.reportChangeToEnclosingScope(n.getParent());
}
}
/**
* Rewrite named unhoisted functions declarations to a known
* consistent behavior so we don't to different logic paths for the same
* code.
*
* From:
* function f() {}
* to:
* var f = function () {};
* and move it to the top of the block. This actually breaks
* semantics, but the semantics are also not well-defined
* cross-browser.
*
* See <a href="https://github.com/google/closure-compiler/pull/429">#429</a>
*/
static boolean visitFunction(Node n, AbstractCompiler compiler) {
checkState(n.isFunction(), n);
if (NodeUtil.isFunctionDeclaration(n) && !NodeUtil.isHoistedFunctionDeclaration(n)) {
rewriteFunctionDeclaration(n, compiler);
return true;
} else if (n.isFunction() && !NodeUtil.getFunctionBody(n).isNormalBlock()) {
Node returnValue = NodeUtil.getFunctionBody(n);
Node body = IR.block(IR.returnNode(returnValue.detach()));
body.useSourceInfoIfMissingFromForTree(returnValue);
n.addChildToBack(body);
compiler.reportChangeToEnclosingScope(body);
}
return false;
}
/**
* Rewrite the function declaration from:
* function x() {}
* FUNCTION
* NAME x
* PARAM_LIST
* BLOCK
* to:
* var x = function() {};
* VAR
* NAME x
* FUNCTION
* NAME (w/ empty string)
* PARAM_LIST
* BLOCK
*/
private static void rewriteFunctionDeclaration(Node n, AbstractCompiler compiler) {
// Prepare a spot for the function.
Node oldNameNode = n.getFirstChild();
Node fnNameNode = oldNameNode.cloneNode();
Node var = IR.var(fnNameNode).srcref(n);
// Prepare the function
oldNameNode.setString("");
compiler.reportChangeToEnclosingScope(oldNameNode);
// Move the function to the front of the parent
Node parent = n.getParent();
parent.removeChild(n);
parent.addChildToFront(var);
compiler.reportChangeToEnclosingScope(var);
fnNameNode.addChildToFront(n);
}
/**
* Do normalizations that introduce new siblings or parents.
*/
private void doStatementNormalizations(Node n) {
if (n.isLabel()) {
normalizeLabels(n);
}
// Only inspect the children of SCRIPTs, BLOCKs and LABELs, as all these
// are the only legal place for VARs and FOR statements.
if (NodeUtil.isStatementBlock(n) || n.isLabel()) {
extractForInitializer(n, null, null);
}
// Only inspect the children of SCRIPTs, BLOCKs, as all these
// are the only legal place for VARs.
if (NodeUtil.isStatementBlock(n)) {
splitVarDeclarations(n);
}
if (n.isFunction()) {
moveNamedFunctions(n.getLastChild());
}
if (NodeUtil.isCompoundAssignmentOp(n)) {
normalizeAssignShorthand(n);
}
}
// TODO(johnlenz): Move this to NodeTypeNormalizer once the unit tests are
// fixed.
/**
* Limit the number of special cases where LABELs need to be handled. Only
* BLOCK and loops are allowed to be labeled. Loop labels must remain in
* place as the named continues are not allowed for labeled blocks.
*/
private void normalizeLabels(Node n) {
checkArgument(n.isLabel());
Node last = n.getLastChild();
// TODO(moz): Avoid adding blocks for cases like "label: let x;"
switch (last.getToken()) {
case LABEL:
case BLOCK:
case FOR:
case FOR_IN:
case FOR_OF:
case WHILE:
case DO:
return;
default:
Node block = IR.block();
block.useSourceInfoIfMissingFrom(last);
n.replaceChild(last, block);
block.addChildToFront(last);
reportCodeChange("LABEL normalization", n);
return;
}
}
/**
* Bring the initializers out of FOR loops. These need to be placed
* before any associated LABEL nodes. This needs to be done from the top
* level label first so this is called as a pre-order callback (from
* shouldTraverse).
*
* @param n The node to inspect.
* @param before The node to insert the initializer before.
* @param beforeParent The parent of the node before which the initializer
* will be inserted.
*/
private void extractForInitializer(
Node n, Node before, Node beforeParent) {
for (Node next, c = n.getFirstChild(); c != null; c = next) {
next = c.getNext();
Node insertBefore = (before == null) ? c : before;
Node insertBeforeParent = (before == null) ? n : beforeParent;
switch (c.getToken()) {
case LABEL:
extractForInitializer(c, insertBefore, insertBeforeParent);
break;
case FOR_IN:
case FOR_OF:
Node first = c.getFirstChild();
if (first.isVar()) {
Node lhs = first.getFirstChild();
if (lhs.isDestructuringLhs()) {
// Transform:
// for (var [a, b = 3] in c) {}
// to:
// var a; var b; for ([a, b = 3] in c) {}
List<Node> lhsNodes = NodeUtil.findLhsNodesInNode(lhs);
for (Node name : lhsNodes) {
// Add a declaration outside the for loop for the given name.
checkState(
name.isName(),
"lhs in destructuring declaration should be a simple name.",
name);
Node newName = IR.name(name.getString()).srcref(name);
Node newVar = IR.var(newName).srcref(name);
insertBeforeParent.addChildBefore(newVar, insertBefore);
}
// Transform for (var [a, b]... ) to for ([a, b]...
Node destructuringPattern = lhs.removeFirstChild();
c.replaceChild(first, destructuringPattern);
} else {
// Transform:
// for (var a = 1 in b) {}
// to:
// var a = 1; for (a in b) {};
Node newStatement = first;
// Clone just the node, to remove any initialization.
Node name = newStatement.getFirstChild().cloneNode();
first.replaceWith(name);
insertBeforeParent.addChildBefore(newStatement, insertBefore);
}
reportCodeChange("FOR-IN var declaration", n);
}
break;
case FOR:
if (!c.getFirstChild().isEmpty()) {
Node init = c.getFirstChild();
if (init.isLet() || init.isConst() || init.isClass() || init.isFunction()) {
return;
}
Node empty = IR.empty();
empty.useSourceInfoIfMissingFrom(c);
c.replaceChild(init, empty);
Node newStatement;
// Only VAR statements, and expressions are allowed,
// but are handled differently.
if (init.isVar()) {
newStatement = init;
} else {
newStatement = NodeUtil.newExpr(init);
}
insertBeforeParent.addChildBefore(newStatement, insertBefore);
reportCodeChange("FOR initializer", n);
}
break;
default:
break;
}
}
}
/**
* Split a var (or let or const) node such as:
* var a, b;
* into individual statements:
* var a;
* var b;
* @param n The whose children we should inspect.
*/
private void splitVarDeclarations(Node n) {
for (Node next, c = n.getFirstChild(); c != null; c = next) {
next = c.getNext();
if (NodeUtil.isNameDeclaration(c)) {
if (assertOnChange && !c.hasChildren()) {
throw new IllegalStateException("Empty VAR node.");
}
while (c.getFirstChild() != c.getLastChild()) {
Node name = c.getFirstChild();
c.removeChild(name);
Node newVar = new Node(c.getToken(), name).srcref(n);
n.addChildBefore(newVar, c);
reportCodeChange("VAR with multiple children", n);
}
}
}
}
/**
* Move all the functions that are valid at the execution of the first
* statement of the function to the beginning of the function definition.
*/
private void moveNamedFunctions(Node functionBody) {
checkState(functionBody.getParent().isFunction());
Node insertAfter = null;
Node current = functionBody.getFirstChild();
// Skip any declarations at the beginning of the function body, they
// are already in the right place.
while (current != null && NodeUtil.isFunctionDeclaration(current)) {
insertAfter = current;
current = current.getNext();
}
// Find any remaining declarations and move them.
while (current != null) {
// Save off the next node as the current node maybe removed.
Node next = current.getNext();
if (NodeUtil.isFunctionDeclaration(current)) {
// Remove the declaration from the body.
functionBody.removeChild(current);
// Read the function at the top of the function body (after any
// previous declarations).
insertAfter = addToFront(functionBody, current, insertAfter);
reportCodeChange("Move function declaration not at top of function", functionBody);
}
current = next;
}
}
private void normalizeAssignShorthand(Node shorthand) {
if (shorthand.getFirstChild().isName()) {
Node name = shorthand.getFirstChild();
shorthand.setToken(NodeUtil.getOpFromAssignmentOp(shorthand));
Node parent = shorthand.getParent();
Node insertPoint = IR.empty();
parent.replaceChild(shorthand, insertPoint);
Node assign = IR.assign(name.cloneNode().useSourceInfoFrom(name), shorthand)
.useSourceInfoFrom(shorthand);
assign.setJSDocInfo(shorthand.getJSDocInfo());
shorthand.setJSDocInfo(null);
parent.replaceChild(insertPoint, assign);
compiler.reportChangeToEnclosingScope(assign);
}
}
/**
* @param after The child node to insert the newChild after, or null if
* newChild should be added to the front of parent's child list.
* @return The inserted child node.
*/
private static Node addToFront(Node parent, Node newChild, Node after) {
if (after == null) {
parent.addChildToFront(newChild);
} else {
parent.addChildAfter(newChild, after);
}
return newChild;
}
}
/**
* Remove duplicate VAR declarations.
*/
private void removeDuplicateDeclarations(Node externs, Node root) {
Callback tickler = new ScopeTicklingCallback();
ScopeCreator scopeCreator =
new Es6SyntacticScopeCreator(compiler, new DuplicateDeclarationHandler());
NodeTraversal t = new NodeTraversal(compiler, tickler, scopeCreator);
t.traverseRoots(externs, root);
}
/**
* ScopeCreator duplicate declaration handler.
*/
private final class DuplicateDeclarationHandler implements
Es6SyntacticScopeCreator.RedeclarationHandler {
private final Set<Var> hasOkDuplicateDeclaration = new HashSet<>();
/**
* Remove duplicate VAR declarations discovered during scope creation.
*/
@Override
public void onRedeclaration(Scope s, String name, Node n, CompilerInput input) {
checkState(n.isName());
Node parent = n.getParent();
Var v = s.getVar(name);
if (s.isGlobal()) {
// We allow variables to be duplicate declared if one
// declaration appears in source and the other in externs.
// This deals with issues where a browser built-in is declared
// in one browser but not in another.
if (v.isExtern() && !input.isExtern()) {
if (hasOkDuplicateDeclaration.add(v)) {
return;
}
}
}
if (parent.isFunction()) {
if (v.getParentNode().isVar()) {
s.undeclare(v);
s.declare(name, n, v.input);
replaceVarWithAssignment(v.getNameNode(), v.getParentNode(),
v.getParentNode().getParent());
}
} else if (parent.isVar()) {
checkState(parent.hasOneChild());
replaceVarWithAssignment(n, parent, parent.getParent());
}
}
/**
* Remove the parent VAR. There are three cases that need to be handled:
* 1) "var a = b;" which is replaced with "a = b"
* 2) "label:var a;" which is replaced with "label:;". Ideally, the
* label itself would be removed but that is not possible in the
* context in which "onRedeclaration" is called.
* 3) "for (var a in b) ..." which is replaced with "for (a in b)..."
* Cases we don't need to handle are VARs with multiple children,
* which have already been split into separate declarations, so there
* is no need to handle that here, and "for (var a;;);", which has
* been moved out of the loop.
* The result of this is that in each case the parent node is replaced
* which is generally dangerous in a traversal but is fine here with
* the scope creator, as the next node of interest is the parent's
* next sibling.
*/
private void replaceVarWithAssignment(Node n, Node parent, Node grandparent) {
if (n.hasChildren()) {
// The * is being initialize, preserve the new value.
parent.removeChild(n);
// Convert "var name = value" to "name = value"
Node value = n.getFirstChild();
n.removeChild(value);
Node replacement = IR.assign(n, value);
replacement.setJSDocInfo(parent.getJSDocInfo());
replacement.useSourceInfoIfMissingFrom(parent);
Node statement = NodeUtil.newExpr(replacement);
grandparent.replaceChild(parent, statement);
reportCodeChange("Duplicate VAR declaration", statement);
} else {
// It is an empty reference remove it.
if (NodeUtil.isStatementBlock(grandparent)) {
grandparent.removeChild(parent);
} else if (grandparent.isForIn() || grandparent.isForOf()) {
// This is the "for (var a in b)..." case. We don't need to worry
// about initializers in "for (var a;;)..." as those are moved out
// as part of the other normalizations.
parent.removeChild(n);
grandparent.replaceChild(parent, n);
} else {
// We should never get here. LABELs with a single VAR statement should
// already have been normalized to have a BLOCK.
checkState(grandparent.isLabel(), grandparent);
}
reportCodeChange("Duplicate VAR declaration", grandparent);
}
}
}
/**
* A simple class that causes scope to be created.
*/
private static final class ScopeTicklingCallback implements NodeTraversal.ScopedCallback {
@Override
public void enterScope(NodeTraversal t) {
// Cause the scope to be created, which will cause duplicate
// to be found.
t.getScope();
}
@Override
public void exitScope(NodeTraversal t) {
// Nothing to do.
}
@Override
public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
return true;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
// Nothing to do.
}
}
}