public void testReport() {
    final List<JSError> errors = new ArrayList<JSError>();

    Compiler compiler =
        new Compiler(
            new BasicErrorManager() {

              @Override
              public void report(CheckLevel level, JSError error) {
                errors.add(error);
              }

              @Override
              public void println(CheckLevel level, JSError error) {}

              @Override
              protected void printSummary() {}
            });
    compiler.initCompilerOptionsIfTesting();

    NodeTraversal t = new NodeTraversal(compiler, null);
    DiagnosticType dt = DiagnosticType.warning("FOO", "{0}, {1} - {2}");

    t.report(null, dt, "Foo", "Bar", "Hello");
    assertEquals(1, errors.size());
    assertEquals("Foo, Bar - Hello", errors.get(0).description);
  }
/**
 * Converts ES6 code to valid ES3 code.
 *
 * @author [email protected] (Tyler Breisacher)
 */
public class Es6ToEs3Converter implements NodeTraversal.Callback, HotSwapCompilerPass {
  private final AbstractCompiler compiler;

  static final DiagnosticType CANNOT_CONVERT =
      DiagnosticType.error(
          "JSC_CANNOT_CONVERT", "This code cannot be converted from ES6 to ES3. {0}");

  // TODO(tbreisacher): Remove this once all ES6 features are transpilable.
  static final DiagnosticType CANNOT_CONVERT_YET =
      DiagnosticType.error(
          "JSC_CANNOT_CONVERT_YET", "ES6-to-ES3 conversion of ''{0}'' is not yet implemented.");

  static final DiagnosticType DYNAMIC_EXTENDS_TYPE =
      DiagnosticType.error(
          "JSC_DYNAMIC_EXTENDS_TYPE", "The class in an extends clause must be a qualified name.");

  static final DiagnosticType NO_SUPERTYPE =
      DiagnosticType.error(
          "JSC_NO_SUPERTYPE",
          "The super keyword may only appear in classes with an extends clause.");

  static final DiagnosticType CLASS_REASSIGNMENT =
      DiagnosticType.error(
          "CLASS_REASSIGNMENT", "Class names defined inside a function cannot be reassigned.");

  // The name of the vars that capture 'this' and 'arguments'
  // for converting arrow functions.
  private static final String THIS_VAR = "$jscomp$this";
  private static final String ARGUMENTS_VAR = "$jscomp$arguments";

  private static final String FRESH_SPREAD_VAR = "$jscomp$spread$args";

  private static final String DESTRUCTURING_TEMP_VAR = "$jscomp$destructuring$var";

  private int destructuringVarCounter = 0;

  private static final String FRESH_COMP_PROP_VAR = "$jscomp$compprop";

  private static final String ITER_BASE = "$jscomp$iter$";

  private static final String ITER_RESULT = "$jscomp$key$";

  // These functions are defined in js/es6_runtime.js
  public static final String COPY_PROP = "$jscomp.copyProperties";
  private static final String INHERITS = "$jscomp.inherits";
  static final String MAKE_ITER = "$jscomp.makeIterator";

  public Es6ToEs3Converter(AbstractCompiler compiler) {
    this.compiler = compiler;
  }

  @Override
  public void process(Node externs, Node root) {
    NodeTraversal.traverse(compiler, root, this);
  }

  @Override
  public void hotSwapScript(Node scriptRoot, Node originalRoot) {
    NodeTraversal.traverse(compiler, scriptRoot, this);
  }

  /**
   * Some nodes (such as arrow functions) must be visited pre-order in order to rewrite the
   * references to {@code this} correctly. Everything else is translated post-order in {@link
   * #visit}.
   */
  @Override
  public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
    switch (n.getType()) {
      case Token.FUNCTION:
        if (n.isArrowFunction()) {
          visitArrowFunction(t, n);
        }
        break;
      case Token.CLASS:
        // Need to check for super references before they get rewritten.
        checkClassSuperReferences(n);
        break;
      case Token.PARAM_LIST:
        visitParamList(n, parent);
        break;
    }
    return true;
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    switch (n.getType()) {
      case Token.OBJECTLIT:
        for (Node child : n.children()) {
          if (child.isComputedProp()) {
            visitObjectWithComputedProperty(n, parent);
            break;
          }
        }
        break;
      case Token.MEMBER_DEF:
        if (parent.isObjectLit()) {
          visitMemberDefInObjectLit(n, parent);
        }
        break;
      case Token.FOR_OF:
        visitForOf(n, parent);
        break;
      case Token.SUPER:
        visitSuper(n, parent);
        break;
      case Token.STRING_KEY:
        visitStringKey(n);
        break;
      case Token.CLASS:
        for (Node member = n.getLastChild().getFirstChild();
            member != null;
            member = member.getNext()) {
          if (member.isGetterDef()
              || member.isSetterDef()
              || member.getBooleanProp(Node.COMPUTED_PROP_GETTER)
              || member.getBooleanProp(Node.COMPUTED_PROP_SETTER)) {
            cannotConvert(member, "getters or setters in class definitions");
            return;
          }
        }
        visitClass(n, parent);
        break;
      case Token.ARRAYLIT:
      case Token.NEW:
      case Token.CALL:
        for (Node child : n.children()) {
          if (child.isSpread()) {
            visitArrayLitOrCallWithSpread(n, parent);
            break;
          }
        }
        break;
      case Token.TEMPLATELIT:
        Es6TemplateLiterals.visitTemplateLiteral(t, n);
        break;
      case Token.ARRAY_PATTERN:
        visitArrayPattern(t, n, parent);
        break;
      case Token.OBJECT_PATTERN:
        visitObjectPattern(t, n, parent);
        break;
    }
  }

  private void visitObjectPattern(NodeTraversal t, Node objectPattern, Node parent) {
    Node rhs, nodeToDetach;
    if (NodeUtil.isNameDeclaration(parent) && !NodeUtil.isEnhancedFor(parent.getParent())) {
      rhs = objectPattern.getLastChild();
      nodeToDetach = parent;
    } else if (parent.isAssign() && parent.getParent().isExprResult()) {
      rhs = parent.getLastChild();
      nodeToDetach = parent.getParent();
    } else if (parent.isStringKey() || parent.isArrayPattern() || parent.isDefaultValue()) {
      // Nested object pattern; do nothing. We will visit it after rewriting the parent.
      return;
    } else if (NodeUtil.isEnhancedFor(parent) || NodeUtil.isEnhancedFor(parent.getParent())) {
      visitDestructuringPatternInEnhancedFor(objectPattern);
      return;
    } else {
      Preconditions.checkState(parent.isCatch(), parent);
      cannotConvertYet(
          objectPattern, "OBJECT_PATTERN that is a child of a " + Token.name(parent.getType()));
      return;
    }

    // Convert 'var {a: b, c: d} = rhs' to:
    // var temp = rhs;
    // var b = temp.a;
    // var d = temp.c;
    String tempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++);
    Node tempDecl =
        IR.var(IR.name(tempVarName), rhs.detachFromParent())
            .useSourceInfoIfMissingFromForTree(objectPattern);
    nodeToDetach.getParent().addChildBefore(tempDecl, nodeToDetach);

    for (Node child = objectPattern.getFirstChild(), next; child != null; child = next) {
      next = child.getNext();

      Node newLHS, newRHS;
      if (child.isStringKey()) {
        Preconditions.checkState(child.hasChildren());
        Node getprop =
            new Node(
                child.isQuotedString() ? Token.GETELEM : Token.GETPROP,
                IR.name(tempVarName),
                IR.string(child.getString()));

        Node value = child.removeFirstChild();
        if (!value.isDefaultValue()) {
          newLHS = value;
          newRHS = getprop;
        } else {
          newLHS = value.removeFirstChild();
          Node defaultValue = value.removeFirstChild();
          newRHS = defaultValueHook(getprop, defaultValue);
        }
      } else if (child.isComputedProp()) {
        if (child.getLastChild().isDefaultValue()) {
          newLHS = child.getLastChild().removeFirstChild();
          Node getelem = IR.getelem(IR.name(tempVarName), child.removeFirstChild());

          String intermediateTempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++);
          Node intermediateDecl = IR.var(IR.name(intermediateTempVarName), getelem);
          intermediateDecl.useSourceInfoIfMissingFromForTree(child);
          nodeToDetach.getParent().addChildBefore(intermediateDecl, nodeToDetach);

          newRHS =
              defaultValueHook(
                  IR.name(intermediateTempVarName), child.getLastChild().removeFirstChild());
        } else {
          newRHS = IR.getelem(IR.name(tempVarName), child.removeFirstChild());
          newLHS = child.removeFirstChild();
        }
      } else if (child.isDefaultValue()) {
        newLHS = child.removeFirstChild();
        Node defaultValue = child.removeFirstChild();
        Node getprop = IR.getprop(IR.name(tempVarName), IR.string(newLHS.getString()));
        newRHS = defaultValueHook(getprop, defaultValue);
      } else {
        throw new IllegalStateException("Unexpected OBJECT_PATTERN child: " + child);
      }

      Node newNode;
      if (NodeUtil.isNameDeclaration(parent)) {
        newNode = IR.declaration(newLHS, newRHS, parent.getType());
      } else if (parent.isAssign()) {
        newNode = IR.exprResult(IR.assign(newLHS, newRHS));
      } else {
        throw new IllegalStateException("not reached");
      }
      newNode.useSourceInfoIfMissingFromForTree(child);

      nodeToDetach.getParent().addChildBefore(newNode, nodeToDetach);

      // Explicitly visit the LHS of the new node since it may be a nested
      // destructuring pattern.
      visit(t, newLHS, newLHS.getParent());
    }

    nodeToDetach.detachFromParent();
    compiler.reportCodeChange();
  }

  private void visitArrayPattern(NodeTraversal t, Node arrayPattern, Node parent) {
    Node rhs, nodeToDetach;
    if (NodeUtil.isNameDeclaration(parent) && !NodeUtil.isEnhancedFor(parent.getParent())) {
      // The array pattern is the only child, because Es6SplitVariableDeclarations
      // has already run.
      Preconditions.checkState(arrayPattern.getNext() == null);
      rhs = arrayPattern.getLastChild();
      nodeToDetach = parent;
    } else if (parent.isAssign()) {
      rhs = arrayPattern.getNext();
      nodeToDetach = parent.getParent();
      Preconditions.checkState(nodeToDetach.isExprResult());
    } else if (parent.isArrayPattern() || parent.isDefaultValue() || parent.isStringKey()) {
      // This is a nested array pattern. Don't do anything now; we'll visit it
      // after visiting the parent.
      return;
    } else if (NodeUtil.isEnhancedFor(parent) || NodeUtil.isEnhancedFor(parent.getParent())) {
      visitDestructuringPatternInEnhancedFor(arrayPattern);
      return;
    } else {
      Preconditions.checkState(parent.isCatch() || parent.isForOf());
      cannotConvertYet(
          arrayPattern, "ARRAY_PATTERN that is a child of a " + Token.name(parent.getType()));
      return;
    }

    // Convert 'var [x, y] = rhs' to:
    // var temp = rhs;
    // var x = temp[0];
    // var y = temp[1];
    String tempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++);
    Node tempDecl =
        IR.var(IR.name(tempVarName), rhs.detachFromParent())
            .useSourceInfoIfMissingFromForTree(arrayPattern);
    nodeToDetach.getParent().addChildBefore(tempDecl, nodeToDetach);

    int i = 0;
    for (Node child = arrayPattern.getFirstChild(), next; child != null; child = next, i++) {
      next = child.getNext();
      if (child.isEmpty()) {
        continue;
      }

      Node newLHS, newRHS;
      if (child.isDefaultValue()) {
        Node getElem = IR.getelem(IR.name(tempVarName), IR.number(i));
        //   [x = defaultValue] = rhs;
        // becomes
        //   var temp = rhs;
        //   x = (temp[0] === undefined) ? defaultValue : temp[0];
        newLHS = child.getFirstChild().detachFromParent();
        newRHS = defaultValueHook(getElem, child.getLastChild().detachFromParent());
      } else if (child.isRest()) {
        newLHS = child.detachFromParent();
        newLHS.setType(Token.NAME);
        // [].slice.call(temp, i)
        newRHS =
            IR.call(
                IR.getprop(IR.getprop(IR.arraylit(), IR.string("slice")), IR.string("call")),
                IR.name(tempVarName),
                IR.number(i));
      } else {
        newLHS = child.detachFromParent();
        newRHS = IR.getelem(IR.name(tempVarName), IR.number(i));
      }
      Node newNode;
      if (parent.isAssign()) {
        Node assignment = IR.assign(newLHS, newRHS);
        newNode = IR.exprResult(assignment);
      } else {
        newNode = IR.declaration(newLHS, newRHS, parent.getType());
      }
      newNode.useSourceInfoIfMissingFromForTree(arrayPattern);

      nodeToDetach.getParent().addChildBefore(newNode, nodeToDetach);
      // Explicitly visit the LHS of the new node since it may be a nested
      // destructuring pattern.
      visit(t, newLHS, newLHS.getParent());
    }
    nodeToDetach.detachFromParent();
    compiler.reportCodeChange();
  }

  private void visitDestructuringPatternInEnhancedFor(Node pattern) {
    Node forNode;
    int declarationType;
    if (NodeUtil.isEnhancedFor(pattern.getParent())) {
      forNode = pattern.getParent();
      declarationType = Token.ASSIGN;
    } else {
      forNode = pattern.getParent().getParent();
      declarationType = pattern.getParent().getType();
      Preconditions.checkState(NodeUtil.isEnhancedFor(forNode));
    }

    String tempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++);
    Node block = forNode.getLastChild();
    if (declarationType == Token.ASSIGN) {
      pattern.getParent().replaceChild(pattern, IR.declaration(IR.name(tempVarName), Token.LET));
      block.addChildToFront(IR.exprResult(IR.assign(pattern, IR.name(tempVarName))));
    } else {
      pattern.getParent().replaceChild(pattern, IR.name(tempVarName));
      block.addChildToFront(IR.declaration(pattern, IR.name(tempVarName), declarationType));
    }
  }

  /**
   * Converts a member definition in an object literal to an ES3 key/value pair. Member definitions
   * in classes are handled in {@link #visitClass}.
   */
  private void visitMemberDefInObjectLit(Node n, Node parent) {
    String name = n.getString();
    Node stringKey = IR.stringKey(name, n.getFirstChild().detachFromParent());
    parent.replaceChild(n, stringKey);
    compiler.reportCodeChange();
  }

  /** Converts extended object literal {a} to {a:a}. */
  private void visitStringKey(Node n) {
    if (!n.hasChildren()) {
      Node name = IR.name(n.getString());
      name.copyInformationFrom(n);
      n.addChildToBack(name);
      compiler.reportCodeChange();
    }
  }

  private void visitForOf(Node node, Node parent) {
    Node variable = node.removeFirstChild();
    Node iterable = node.removeFirstChild();
    Node body = node.removeFirstChild();

    Node iterName = IR.name(ITER_BASE + compiler.getUniqueNameIdSupplier().get());
    Node getNext = IR.call(IR.getprop(iterName.cloneTree(), IR.string("next")));
    String variableName =
        variable.isName()
            ? variable.getQualifiedName()
            : variable.getFirstChild().getQualifiedName(); // var or let
    Node iterResult = IR.name(ITER_RESULT + variableName);

    Node makeIter =
        IR.call(NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), MAKE_ITER), iterable);

    Node init = IR.var(iterName.cloneTree(), makeIter);
    Node initIterResult = iterResult.cloneTree();
    initIterResult.addChildToFront(getNext.cloneTree());
    init.addChildToBack(initIterResult);

    Node cond = IR.not(IR.getprop(iterResult.cloneTree(), IR.string("done")));
    Node incr = IR.assign(iterResult.cloneTree(), getNext.cloneTree());
    body.addChildToFront(
        IR.var(IR.name(variableName), IR.getprop(iterResult.cloneTree(), IR.string("value"))));

    Node newFor = IR.forNode(init, cond, incr, body);
    newFor.useSourceInfoIfMissingFromForTree(node);
    parent.replaceChild(node, newFor);
    compiler.reportCodeChange();
  }

  private void checkClassReassignment(Node clazz) {
    Node name = NodeUtil.getClassNameNode(clazz);
    Node enclosingFunction = getEnclosingFunction(clazz);
    if (enclosingFunction == null) {
      return;
    }
    CheckClassAssignments checkAssigns = new CheckClassAssignments(name);
    NodeTraversal.traverse(compiler, enclosingFunction, checkAssigns);
  }

  private void visitSuper(Node node, Node parent) {
    Node enclosing = parent;
    Node potentialCallee = node;
    if (!parent.isCall()) {
      enclosing = parent.getParent();
      potentialCallee = parent;
    }
    if (!enclosing.isCall() || enclosing.getFirstChild() != potentialCallee) {
      cannotConvertYet(node, "Only calls to super or to a method of super are supported.");
      return;
    }
    Node clazz = NodeUtil.getEnclosingClass(node);
    if (clazz == null) {
      compiler.report(JSError.make(node, NO_SUPERTYPE));
      return;
    }
    if (NodeUtil.getClassNameNode(clazz) == null) {
      // Unnamed classes of the form:
      //   f(class extends D { ... });
      // give the problem that there is no name to be used in the call to goog.base for the
      // translation of super calls.
      // This will throw an error when the class is processed.
      return;
    }

    Node enclosingMemberDef = NodeUtil.getEnclosingClassMember(node);
    if (enclosingMemberDef.isStaticMember()) {
      Node superName = clazz.getFirstChild().getNext();
      if (!superName.isQualifiedName()) {
        // This has already been reported, just don't need to continue processing the class.
        return;
      }
      Node callTarget;
      potentialCallee.detachFromParent();
      if (potentialCallee == node) {
        // of the form super()
        potentialCallee =
            IR.getprop(superName.cloneTree(), IR.string(enclosingMemberDef.getString()));
        enclosing.putBooleanProp(Node.FREE_CALL, false);
      } else {
        // of the form super.method()
        potentialCallee.replaceChild(node, superName.cloneTree());
      }
      callTarget = IR.getprop(potentialCallee, IR.string("call"));
      enclosing.addChildToFront(callTarget);
      enclosing.addChildAfter(IR.thisNode(), callTarget);
      enclosing.useSourceInfoIfMissingFromForTree(enclosing);
      compiler.reportCodeChange();
      return;
    }

    String methodName;
    Node callName = enclosing.removeFirstChild();
    if (callName.isSuper()) {
      methodName = enclosingMemberDef.getString();
    } else {
      methodName = callName.getLastChild().getString();
    }
    Node baseCall =
        baseCall(clazz, methodName, enclosing.removeChildren())
            .useSourceInfoIfMissingFromForTree(enclosing);
    enclosing.getParent().replaceChild(enclosing, baseCall);
    compiler.reportCodeChange();
  }

  private Node baseCall(Node clazz, String methodName, Node arguments) {
    boolean useUnique = NodeUtil.isStatement(clazz) && !isInFunction(clazz);
    String uniqueClassString =
        useUnique ? getUniqueClassName(NodeUtil.getClassName(clazz)) : NodeUtil.getClassName(clazz);
    Node uniqueClassName =
        NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), uniqueClassString);
    Node base = IR.getprop(uniqueClassName, IR.string("base"));
    Node call = IR.call(base, IR.thisNode(), IR.string(methodName));
    if (arguments != null) {
      call.addChildrenToBack(arguments);
    }
    return call;
  }

  /** Processes trailing default and rest parameters. */
  private void visitParamList(Node paramList, Node function) {
    Node insertSpot = null;
    Node block = function.getLastChild();
    for (int i = 0; i < paramList.getChildCount(); i++) {
      Node param = paramList.getChildAtIndex(i);
      if (param.isDefaultValue()) {
        Node nameOrPattern = param.removeFirstChild();
        Node defaultValue = param.removeFirstChild();
        Node newParam =
            nameOrPattern.isName()
                ? nameOrPattern
                : IR.name(DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++));

        Node lhs = nameOrPattern.cloneTree();
        Node rhs = defaultValueHook(newParam.cloneTree(), defaultValue);
        Node newStatement =
            nameOrPattern.isName() ? IR.exprResult(IR.assign(lhs, rhs)) : IR.var(lhs, rhs);
        newStatement.useSourceInfoIfMissingFromForTree(param);
        block.addChildAfter(newStatement, insertSpot);
        insertSpot = newStatement;

        paramList.replaceChild(param, newParam);
        newParam.setOptionalArg(true);

        compiler.reportCodeChange();
      } else if (param.isRest()) { // rest parameter
        param.setType(Token.NAME);
        param.setVarArgs(true);
        // Transpile to: param = [].slice.call(arguments, i);
        Node newArr =
            IR.exprResult(
                IR.assign(
                    IR.name(param.getString()),
                    IR.call(
                        IR.getprop(
                            IR.getprop(IR.arraylit(), IR.string("slice")), IR.string("call")),
                        IR.name("arguments"),
                        IR.number(i))));
        block.addChildAfter(newArr.useSourceInfoIfMissingFromForTree(param), insertSpot);
        compiler.reportCodeChange();
      } else if (param.isDestructuringPattern()) {
        String tempVarName = DESTRUCTURING_TEMP_VAR + (destructuringVarCounter++);
        paramList.replaceChild(param, IR.name(tempVarName));
        Node newDecl = IR.var(param, IR.name(tempVarName));
        block.addChildAfter(newDecl, insertSpot);
        insertSpot = newDecl;
      }
    }
    // For now, we are running transpilation before type-checking, so we'll
    // need to make sure changes don't invalidate the JSDoc annotations.
    // Therefore we keep the parameter list the same length and only initialize
    // the values if they are set to undefined.
  }

  /**
   * Processes array literals or calls containing spreads. Eg.: [1, 2, ...x, 4, 5] => [1,
   * 2].concat(x, [4, 5]); Eg.: f(...arr) => f.apply(null, arr) Eg.: new F(...args) => new
   * Function.prototype.bind.apply(F, [].concat(args))
   */
  private void visitArrayLitOrCallWithSpread(Node node, Node parent) {
    Preconditions.checkArgument(node.isCall() || node.isArrayLit() || node.isNew());
    List<Node> groups = new ArrayList<>();
    Node currGroup = null;
    Node callee = node.isArrayLit() ? null : node.removeFirstChild();
    Node currElement = node.removeFirstChild();
    while (currElement != null) {
      if (currElement.isSpread()) {
        if (currGroup != null) {
          groups.add(currGroup);
          currGroup = null;
        }
        groups.add(currElement.removeFirstChild());
      } else {
        if (currGroup == null) {
          currGroup = IR.arraylit();
        }
        currGroup.addChildToBack(currElement);
      }
      currElement = node.removeFirstChild();
    }
    if (currGroup != null) {
      groups.add(currGroup);
    }
    Node result = null;
    Node joinedGroups =
        IR.call(
            IR.getprop(IR.arraylit(), IR.string("concat")),
            groups.toArray(new Node[groups.size()]));
    if (node.isArrayLit()) {
      result = joinedGroups;
    } else if (node.isCall()) {
      if (NodeUtil.mayHaveSideEffects(callee) && callee.isGetProp()) {
        Node statement = node;
        while (!NodeUtil.isStatement(statement)) {
          statement = statement.getParent();
        }
        Node freshVar = IR.name(FRESH_SPREAD_VAR + compiler.getUniqueNameIdSupplier().get());
        Node n = IR.var(freshVar.cloneTree());
        n.useSourceInfoIfMissingFromForTree(statement);
        statement.getParent().addChildBefore(n, statement);
        callee.addChildToFront(IR.assign(freshVar.cloneTree(), callee.removeFirstChild()));
        result = IR.call(IR.getprop(callee, IR.string("apply")), freshVar, joinedGroups);
      } else {
        Node context = callee.isGetProp() ? callee.getFirstChild().cloneTree() : IR.nullNode();
        result = IR.call(IR.getprop(callee, IR.string("apply")), context, joinedGroups);
      }
    } else {
      Node bindApply =
          NodeUtil.newQualifiedNameNode(
              compiler.getCodingConvention(), "Function.prototype.bind.apply");
      result = IR.newNode(bindApply, callee, joinedGroups);
    }
    result.useSourceInfoIfMissingFromForTree(node);
    parent.replaceChild(node, result);
    compiler.reportCodeChange();
  }

  private void visitObjectWithComputedProperty(Node obj, Node parent) {
    Preconditions.checkArgument(obj.isObjectLit());
    List<Node> props = new ArrayList<>();
    Node currElement = obj.getFirstChild();

    while (currElement != null) {
      if (currElement.getBooleanProp(Node.COMPUTED_PROP_GETTER)
          || currElement.getBooleanProp(Node.COMPUTED_PROP_SETTER)) {
        cannotConvertYet(currElement, "computed getter/setter");
        return;
      } else if (currElement.isGetterDef() || currElement.isSetterDef()) {
        currElement = currElement.getNext();
      } else {
        Node nextNode = currElement.getNext();
        obj.removeChild(currElement);
        props.add(currElement);
        currElement = nextNode;
      }
    }

    String objName = FRESH_COMP_PROP_VAR + compiler.getUniqueNameIdSupplier().get();

    props = Lists.reverse(props);
    Node result = IR.name(objName);
    for (Node propdef : props) {
      if (propdef.isComputedProp()) {
        Node propertyExpression = propdef.removeFirstChild();
        Node value = propdef.removeFirstChild();
        result =
            IR.comma(IR.assign(IR.getelem(IR.name(objName), propertyExpression), value), result);
      } else {
        if (!propdef.hasChildren()) {
          Node name = IR.name(propdef.getString()).copyInformationFrom(propdef);
          propdef.addChildToBack(name);
        }
        Node val = propdef.removeFirstChild();
        propdef.setType(Token.STRING);
        int type = propdef.isQuotedString() ? Token.GETELEM : Token.GETPROP;
        Node access = new Node(type, IR.name(objName), propdef);
        result = IR.comma(IR.assign(access, val), result);
      }
    }

    Node statement = obj;
    while (!NodeUtil.isStatement(statement)) {
      statement = statement.getParent();
    }

    result.useSourceInfoIfMissingFromForTree(obj);
    parent.replaceChild(obj, result);

    Node var = IR.var(IR.name(objName), obj);
    var.useSourceInfoIfMissingFromForTree(statement);
    statement.getParent().addChildBefore(var, statement);
    compiler.reportCodeChange();
  }

  private void checkClassSuperReferences(Node classNode) {
    Node className = classNode.getFirstChild();
    Node superClassName = className.getNext();
    if (NodeUtil.referencesSuper(classNode) && !superClassName.isQualifiedName()) {
      compiler.report(JSError.make(classNode, NO_SUPERTYPE));
    }
  }

  /**
   * Classes are processed in 3 phases: 1) The class name is extracted. 2) Class members are
   * processed and rewritten. 3) The constructor is built.
   */
  private void visitClass(Node classNode, Node parent) {
    checkClassReassignment(classNode);
    // Collect Metadata
    Node className = classNode.getFirstChild();
    Node superClassName = className.getNext();
    Node classMembers = classNode.getLastChild();

    // This is a statement node. We insert methods of the
    // transpiled class after this node.
    Node insertionPoint;

    if (!superClassName.isEmpty() && !superClassName.isQualifiedName()) {
      compiler.report(JSError.make(superClassName, DYNAMIC_EXTENDS_TYPE));
      return;
    }

    // The fully qualified name of the class, which will be used in the output.
    // May come from the class itself or the LHS of an assignment.
    String fullClassName = null;

    // Whether the constructor function in the output should be anonymous.
    boolean anonymous;

    // If this is a class statement, or a class expression in a simple
    // assignment or var statement, convert it. In any other case, the
    // code is too dynamic, so just call cannotConvert.
    if (NodeUtil.isStatement(classNode)) {
      fullClassName = className.getString();
      anonymous = false;
      insertionPoint = classNode;
    } else if (parent.isAssign() && parent.getParent().isExprResult()) {
      // Add members after the EXPR_RESULT node:
      // example.C = class {}; example.C.prototype.foo = function() {};
      fullClassName = parent.getFirstChild().getQualifiedName();
      if (fullClassName == null) {
        cannotConvert(
            parent,
            "Can only convert classes that are declarations or the right hand"
                + " side of a simple assignment.");
        return;
      }
      anonymous = true;
      insertionPoint = parent.getParent();
    } else if (parent.isName()) {
      // Add members after the 'var' statement.
      // var C = class {}; C.prototype.foo = function() {};
      fullClassName = parent.getString();
      anonymous = true;
      insertionPoint = parent.getParent();
    } else {
      cannotConvert(
          parent,
          "Can only convert classes that are declarations or the right hand"
              + " side of a simple assignment.");
      return;
    }

    if (!className.isEmpty() && !className.getString().equals(fullClassName)) {
      // cannot bind two class names in the case of: var Foo = class Bar {};
      cannotConvertYet(classNode, "named class in an assignment");
      return;
    }

    boolean useUnique = NodeUtil.isStatement(classNode) && !isInFunction(classNode);
    String uniqueFullClassName = useUnique ? getUniqueClassName(fullClassName) : fullClassName;
    String superClassString = superClassName.getQualifiedName();

    Verify.verify(NodeUtil.isStatement(insertionPoint));

    Node constructor = null;
    JSDocInfo ctorJSDocInfo = null;
    // Process all members of the class
    for (Node member : classMembers.children()) {
      if (member.isEmpty()) {
        continue;
      }

      if (member.isMemberDef() && member.getString().equals("constructor")) {
        ctorJSDocInfo = member.getJSDocInfo();
        constructor = member.getFirstChild().detachFromParent();
        if (!anonymous) {
          constructor.replaceChild(constructor.getFirstChild(), className.cloneNode());
        }
      } else {
        Node qualifiedMemberName;
        Node method;
        if (member.isMemberDef()) {
          if (member.isStaticMember()) {
            qualifiedMemberName =
                NodeUtil.newQualifiedNameNode(
                    compiler.getCodingConvention(),
                    Joiner.on(".").join(uniqueFullClassName, member.getString()));
          } else {
            qualifiedMemberName =
                NodeUtil.newQualifiedNameNode(
                    compiler.getCodingConvention(),
                    Joiner.on(".").join(uniqueFullClassName, "prototype", member.getString()));
          }
          method = member.getFirstChild().detachFromParent();
        } else if (member.isComputedProp()) {
          if (member.isStaticMember()) {
            qualifiedMemberName =
                IR.getelem(
                    NodeUtil.newQualifiedNameNode(
                        compiler.getCodingConvention(), uniqueFullClassName),
                    member.removeFirstChild());
          } else {
            qualifiedMemberName =
                IR.getelem(
                    NodeUtil.newQualifiedNameNode(
                        compiler.getCodingConvention(),
                        Joiner.on('.').join(uniqueFullClassName, "prototype")),
                    member.removeFirstChild());
          }
          method = member.getLastChild().detachFromParent();
        } else {
          throw new IllegalStateException("Unexpected class member: " + member);
        }
        Node assign = IR.assign(qualifiedMemberName, method);
        assign.useSourceInfoIfMissingFromForTree(member);

        JSDocInfo info = member.getJSDocInfo();
        if (member.isStaticMember() && NodeUtil.referencesThis(assign.getLastChild())) {
          JSDocInfoBuilder memberDoc;
          if (info == null) {
            memberDoc = new JSDocInfoBuilder(true);
          } else {
            memberDoc = JSDocInfoBuilder.copyFrom(info);
          }
          memberDoc.recordThisType(
              new JSTypeExpression(
                  new Node(Token.BANG, new Node(Token.QMARK)), member.getSourceFileName()));
          info = memberDoc.build(assign);
        }
        if (info != null) {
          info.setAssociatedNode(assign);
          assign.setJSDocInfo(info);
        }

        Node newNode = NodeUtil.newExpr(assign);
        insertionPoint.getParent().addChildAfter(newNode, insertionPoint);
        insertionPoint = newNode;
      }
    }

    // Rewrite constructor
    if (constructor == null) {
      Node body = IR.block();
      if (!superClassName.isEmpty()) {
        Node superCall = baseCall(classNode, "constructor", null);
        body.addChildToBack(IR.exprResult(superCall));
      }
      Node name = anonymous ? IR.name("").srcref(className) : className.detachFromParent();
      constructor =
          IR.function(name, IR.paramList(), body).useSourceInfoIfMissingFromForTree(classNode);
    }
    JSDocInfo classJSDoc = classNode.getJSDocInfo();
    JSDocInfoBuilder newInfo =
        (classJSDoc != null) ? JSDocInfoBuilder.copyFrom(classJSDoc) : new JSDocInfoBuilder(true);

    newInfo.recordConstructor();
    if (!superClassName.isEmpty()) {

      if (newInfo.isInterfaceRecorded()) {
        newInfo.recordExtendedInterface(
            new JSTypeExpression(
                new Node(Token.BANG, IR.string(superClassString)),
                superClassName.getSourceFileName()));
      } else {
        Node inherits =
            IR.call(
                NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), INHERITS),
                NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), fullClassName),
                NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), superClassString));
        Node inheritsCall = IR.exprResult(inherits);
        inheritsCall.useSourceInfoIfMissingFromForTree(classNode);
        Node enclosingStatement = NodeUtil.getEnclosingStatement(classNode);
        enclosingStatement.getParent().addChildAfter(inheritsCall, enclosingStatement);
        newInfo.recordBaseType(
            new JSTypeExpression(
                new Node(Token.BANG, IR.string(superClassString)),
                superClassName.getSourceFileName()));

        Node copyProps =
            IR.call(
                NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), COPY_PROP),
                NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), fullClassName),
                NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), superClassString));
        copyProps.useSourceInfoIfMissingFromForTree(classNode);
        enclosingStatement
            .getParent()
            .addChildAfter(IR.exprResult(copyProps).srcref(classNode), enclosingStatement);
      }
    }

    // Classes are @struct by default.
    if (!newInfo.isUnrestrictedRecorded()
        && !newInfo.isDictRecorded()
        && !newInfo.isStructRecorded()) {
      newInfo.recordStruct();
    }

    if (ctorJSDocInfo != null) {
      newInfo.recordSuppressions(ctorJSDocInfo.getSuppressions());
      for (String param : ctorJSDocInfo.getParameterNames()) {
        newInfo.recordParameter(param, ctorJSDocInfo.getParameterType(param));
      }
    }
    insertionPoint = constructor;

    if (NodeUtil.isStatement(classNode)) {
      constructor.getFirstChild().setString("");
      Node ctorVar = IR.var(IR.name(fullClassName), constructor);
      ctorVar.useSourceInfoIfMissingFromForTree(classNode);
      parent.replaceChild(classNode, ctorVar);
    } else {
      parent.replaceChild(classNode, constructor);
    }

    if (NodeUtil.isStatement(constructor)) {
      insertionPoint.setJSDocInfo(newInfo.build(insertionPoint));
    } else if (parent.isName()) {
      // The constructor function is the RHS of a var statement.
      // Add the JSDoc to the VAR node.
      Node var = parent.getParent();
      var.setJSDocInfo(newInfo.build(var));
    } else if (constructor.getParent().isName()) {
      // Is a newly created VAR node.
      Node var = constructor.getParent().getParent();
      var.setJSDocInfo(newInfo.build(var));
    } else if (parent.isAssign()) {
      // The constructor function is the RHS of an assignment.
      // Add the JSDoc to the ASSIGN node.
      parent.setJSDocInfo(newInfo.build(parent));
    } else {
      throw new IllegalStateException("Unexpected parent node " + parent);
    }

    compiler.reportCodeChange();
  }

  /** Converts ES6 arrow functions to standard anonymous ES3 functions. */
  private void visitArrowFunction(NodeTraversal t, Node n) {
    n.setIsArrowFunction(false);
    Node body = n.getLastChild();
    if (!body.isBlock()) {
      body.detachFromParent();
      body = IR.block(IR.returnNode(body).srcref(body)).srcref(body);
      n.addChildToBack(body);
    }

    UpdateThisAndArgumentsReferences updater = new UpdateThisAndArgumentsReferences();
    NodeTraversal.traverse(compiler, body, updater);
    addVarDecls(t, updater.changedThis, updater.changedArguments);

    compiler.reportCodeChange();
  }

  private void addVarDecls(NodeTraversal t, boolean addThis, boolean addArguments) {
    Scope scope = t.getScope();
    if (scope.isDeclared(THIS_VAR, false)) {
      addThis = false;
    }
    if (scope.isDeclared(ARGUMENTS_VAR, false)) {
      addArguments = false;
    }

    Node parent = t.getScopeRoot();
    if (parent.isFunction()) {
      // Add the new node at the beginning of the function body.
      parent = parent.getLastChild();
    }
    if (parent.isSyntheticBlock() && parent.getFirstChild().isScript()) {
      // Add the new node inside the SCRIPT node instead of the
      // synthetic block that contains it.
      parent = parent.getFirstChild();
    }

    CompilerInput input = compiler.getInput(parent.getInputId());
    if (addArguments) {
      Node name = IR.name(ARGUMENTS_VAR).srcref(parent);
      Node argumentsVar = IR.var(name, IR.name("arguments").srcref(parent));
      argumentsVar.srcref(parent);
      parent.addChildToFront(argumentsVar);
      scope.declare(ARGUMENTS_VAR, name, null, input);
    }
    if (addThis) {
      Node name = IR.name(THIS_VAR).srcref(parent);
      Node thisVar = IR.var(name, IR.thisNode().srcref(parent));
      thisVar.srcref(parent);
      parent.addChildToFront(thisVar);
      scope.declare(THIS_VAR, name, null, input);
    }
  }

  private static String getUniqueClassName(String qualifiedName) {
    return qualifiedName;
  }

  // TODO(mattloring) move this functionality to scopes once class scopes are computed.
  private static Node getEnclosingFunction(Node n) {
    return NodeUtil.getEnclosingType(n, Token.FUNCTION);
  }

  private static boolean isInFunction(Node n) {
    return getEnclosingFunction(n) != null;
  }

  /** Helper for transpiling DEFAULT_VALUE trees. */
  private static Node defaultValueHook(Node getprop, Node defaultValue) {
    return IR.hook(IR.sheq(getprop, IR.name("undefined")), defaultValue, getprop.cloneTree());
  }

  private static class UpdateThisAndArgumentsReferences implements NodeTraversal.Callback {
    private boolean changedThis = false;
    private boolean changedArguments = false;

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (n.isThis()) {
        Node name = IR.name(THIS_VAR).srcref(n);
        parent.replaceChild(n, name);
        changedThis = true;
      } else if (n.isName() && n.getString().equals("arguments")) {
        Node name = IR.name(ARGUMENTS_VAR).srcref(n);
        parent.replaceChild(n, name);
        changedArguments = true;
      }
    }

    @Override
    public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
      return !n.isFunction() || n.isArrowFunction();
    }
  }

  private class CheckClassAssignments extends NodeTraversal.AbstractPostOrderCallback {
    private Node className;

    public CheckClassAssignments(Node className) {
      this.className = className;
    }

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (!n.isAssign() || n.getFirstChild() == className) {
        return;
      }
      if (className.matchesQualifiedName(n.getFirstChild())) {
        compiler.report(JSError.make(n, CLASS_REASSIGNMENT));
      }
    }
  }

  private void cannotConvert(Node n, String message) {
    compiler.report(JSError.make(n, CANNOT_CONVERT, message));
  }

  /**
   * Warns the user that the given ES6 feature cannot be converted to ES3 because the transpilation
   * is not yet implemented. A call to this method is essentially a "TODO(tbreisacher): Implement
   * {@code feature}" comment.
   */
  private void cannotConvertYet(Node n, String feature) {
    compiler.report(JSError.make(n, CANNOT_CONVERT_YET, feature));
  }
}
/**
 * Provides a framework for checking code against a set of user configured conformance rules. The
 * rules are specified by the ConformanceConfig proto, which allows for both standard checks
 * (forbidden properties, variables, or dependencies) and allow for more complex checks using custom
 * rules than specify
 */
@GwtIncompatible("com.google.protobuf")
public final class CheckConformance implements Callback, CompilerPass {

  static final DiagnosticType CONFORMANCE_VIOLATION =
      DiagnosticType.warning("JSC_CONFORMANCE_VIOLATION", "Violation: {0}{1}{2}");

  static final DiagnosticType CONFORMANCE_POSSIBLE_VIOLATION =
      DiagnosticType.warning("JSC_CONFORMANCE_POSSIBLE_VIOLATION", "Possible violation: {0}{1}{2}");

  static final DiagnosticType INVALID_REQUIREMENT_SPEC =
      DiagnosticType.error(
          "JSC_INVALID_REQUIREMENT_SPEC",
          "Invalid requirement. Reason: {0}\nRequirement spec:\n{1}");

  private final AbstractCompiler compiler;
  private final ImmutableList<Rule> rules;

  public static interface Rule {
    /** Perform conformance check */
    void check(NodeTraversal t, Node n);
  }

  /** @param configs The rules to check. */
  CheckConformance(AbstractCompiler compiler, ImmutableList<ConformanceConfig> configs) {
    this.compiler = compiler;
    // Initialize the map of functions to inspect for renaming candidates.
    this.rules = initRules(compiler, configs);
  }

  @Override
  public void process(Node externs, Node root) {
    if (!rules.isEmpty()) {
      NodeTraversal.traverseRootsEs6(compiler, this, externs, root);
    }
  }

  @Override
  public final boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
    // Don't inspect extern files
    return !n.isScript() || !t.getInput().getSourceFile().isExtern();
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    for (int i = 0, len = rules.size(); i < len; i++) {
      Rule rule = rules.get(i);
      rule.check(t, n);
    }
  }

  /** Build the data structures need by this pass from the provided configurations. */
  private static ImmutableList<Rule> initRules(
      AbstractCompiler compiler, ImmutableList<ConformanceConfig> configs) {
    ImmutableList.Builder<Rule> builder = ImmutableList.builder();
    List<Requirement> requirements = mergeRequirements(compiler, configs);
    for (Requirement requirement : requirements) {
      Rule rule = initRule(compiler, requirement);
      if (rule != null) {
        builder.add(rule);
      }
    }
    return builder.build();
  }

  private static final Set<String> EXTENDABLE_FIELDS =
      ImmutableSet.of(
          "extends", "whitelist", "whitelist_regexp", "only_apply_to", "only_apply_to_regexp");

  /**
   * Gets requirements from all configs. Merges whitelists of requirements with 'extends' equal to
   * 'rule_id' of other rule.
   */
  static List<Requirement> mergeRequirements(
      AbstractCompiler compiler, List<ConformanceConfig> configs) {
    List<Requirement.Builder> builders = new ArrayList<>();
    Map<String, Requirement.Builder> extendable = new HashMap<>();
    for (ConformanceConfig config : configs) {
      for (Requirement requirement : config.getRequirementList()) {
        Requirement.Builder builder = requirement.toBuilder();
        if (requirement.hasRuleId()) {
          if (requirement.getRuleId().isEmpty()) {
            reportInvalidRequirement(compiler, requirement, "empty rule_id");
            continue;
          }
          if (extendable.containsKey(requirement.getRuleId())) {
            reportInvalidRequirement(
                compiler,
                requirement,
                "two requirements with the same rule_id: " + requirement.getRuleId());
            continue;
          }
          extendable.put(requirement.getRuleId(), builder);
        }
        if (!requirement.hasExtends()) {
          builders.add(builder);
        }
      }
    }

    for (ConformanceConfig config : configs) {
      for (Requirement requirement : config.getRequirementList()) {
        if (requirement.hasExtends()) {
          Requirement.Builder existing = extendable.get(requirement.getExtends());
          if (existing == null) {
            reportInvalidRequirement(
                compiler, requirement, "no requirement with rule_id: " + requirement.getExtends());
            continue;
          }
          for (Descriptors.FieldDescriptor field : requirement.getAllFields().keySet()) {
            if (!EXTENDABLE_FIELDS.contains(field.getName())) {
              reportInvalidRequirement(
                  compiler, requirement, "extending rules allow only " + EXTENDABLE_FIELDS);
            }
          }
          existing.addAllWhitelist(requirement.getWhitelistList());
          existing.addAllWhitelistRegexp(requirement.getWhitelistRegexpList());
          existing.addAllOnlyApplyTo(requirement.getOnlyApplyToList());
          existing.addAllOnlyApplyToRegexp(requirement.getOnlyApplyToRegexpList());
        }
      }
    }

    List<Requirement> requirements = new ArrayList<>(builders.size());
    for (Requirement.Builder builder : builders) {
      Requirement requirement = builder.build();
      checkRequirementList(compiler, requirement, "whitelist");
      checkRequirementList(compiler, requirement, "whitelist_regexp");
      checkRequirementList(compiler, requirement, "only_apply_to");
      checkRequirementList(compiler, requirement, "only_apply_to_regexp");
      requirements.add(requirement);
    }
    return requirements;
  }

  private static void checkRequirementList(
      AbstractCompiler compiler, Requirement requirement, String field) {
    Set<String> existing = new HashSet<>();
    for (String value : getRequirementList(requirement, field)) {
      if (!existing.add(value)) {
        reportInvalidRequirement(compiler, requirement, "duplicate " + field + " value: " + value);
      }
    }
  }

  private static List<String> getRequirementList(Requirement requirement, String field) {
    switch (field) {
      case "whitelist":
        return requirement.getWhitelistList();
      case "whitelist_regexp":
        return requirement.getWhitelistRegexpList();
      case "only_apply_to":
        return requirement.getOnlyApplyToList();
      case "only_apply_to_regexp":
        return requirement.getOnlyApplyToRegexpList();
      default:
        throw new AssertionError("Unrecognized field: " + field);
    }
  }

  private static Rule initRule(AbstractCompiler compiler, Requirement requirement) {
    try {
      switch (requirement.getType()) {
        case CUSTOM:
          return new ConformanceRules.CustomRuleProxy(compiler, requirement);
        case BANNED_CODE_PATTERN:
          return new ConformanceRules.BannedCodePattern(compiler, requirement);
        case BANNED_DEPENDENCY:
          return new ConformanceRules.BannedDependency(compiler, requirement);
        case BANNED_NAME:
          return new ConformanceRules.BannedName(compiler, requirement);
        case BANNED_PROPERTY:
        case BANNED_PROPERTY_READ:
        case BANNED_PROPERTY_WRITE:
        case BANNED_PROPERTY_CALL:
          return new ConformanceRules.BannedProperty(compiler, requirement);
        case RESTRICTED_NAME_CALL:
          return new ConformanceRules.RestrictedNameCall(compiler, requirement);
        case RESTRICTED_METHOD_CALL:
          return new ConformanceRules.RestrictedMethodCall(compiler, requirement);
        default:
          reportInvalidRequirement(compiler, requirement, "unknown requirement type");
          return null;
      }
    } catch (InvalidRequirementSpec e) {
      reportInvalidRequirement(compiler, requirement, e.getMessage());
      return null;
    }
  }

  public static class InvalidRequirementSpec extends Exception {
    InvalidRequirementSpec(String message) {
      super(message);
    }
  }

  /** @param requirement */
  private static void reportInvalidRequirement(
      AbstractCompiler compiler, Requirement requirement, String reason) {
    compiler.report(
        JSError.make(INVALID_REQUIREMENT_SPEC, reason, TextFormat.printToString(requirement)));
  }
}
/**
 * Creates synthetic blocks to optimizations from moving code past markers in the source.
 *
 * @author [email protected] (John Lenz)
 */
class CreateSyntheticBlocks implements CompilerPass {
  static final DiagnosticType UNMATCHED_START_MARKER =
      DiagnosticType.error("JSC_UNMATCHED_START_MARKER", "Unmatched {0}");

  static final DiagnosticType UNMATCHED_END_MARKER =
      DiagnosticType.error("JSC_UNMATCHED_END_MARKER", "Unmatched {1} - {0} not in the same block");

  static final DiagnosticType INVALID_MARKER_USAGE =
      DiagnosticType.error(
          "JSC_INVALID_MARKER_USAGE",
          "Marker {0} can only be used in a simple " + "call expression");

  private final AbstractCompiler compiler;

  /** Name of the start marker. */
  private final String startMarkerName;

  /** Name of the end marker. */
  private final String endMarkerName;

  /** Markers can be nested. */
  private final Deque<Node> markerStack = new ArrayDeque<>();

  private final List<Marker> validMarkers = new ArrayList<>();

  private static class Marker {
    final Node startMarker;
    final Node endMarker;

    public Marker(Node startMarker, Node endMarker) {
      this.startMarker = startMarker;
      this.endMarker = endMarker;
    }
  }

  public CreateSyntheticBlocks(
      AbstractCompiler compiler, String startMarkerName, String endMarkerName) {
    this.compiler = compiler;
    this.startMarkerName = startMarkerName;
    this.endMarkerName = endMarkerName;
  }

  @Override
  public void process(Node externs, Node root) {
    // Find and validate the markers.
    NodeTraversal.traverseEs6(compiler, root, new Callback());

    // Complain about any unmatched markers.
    for (Node node : markerStack) {
      compiler.report(JSError.make(node, UNMATCHED_START_MARKER, startMarkerName));
    }

    // Add the block for the valid marker sets.
    for (Marker marker : validMarkers) {
      addBlocks(marker);
    }
  }

  /** @param marker The marker to add synthetic blocks for. */
  private void addBlocks(Marker marker) {
    // Add block around the template section so that it looks like this:
    //  BLOCK (synthetic)
    //    START
    //      BLOCK (synthetic)
    //        BODY
    //    END
    // This prevents the start or end markers from mingling with the code
    // in the block body.

    Node originalParent = marker.endMarker.getParent();
    Node outerBlock = IR.block();
    outerBlock.setIsSyntheticBlock(true);
    originalParent.addChildBefore(outerBlock, marker.startMarker);

    Node innerBlock = IR.block();
    innerBlock.setIsSyntheticBlock(true);
    // Move everything after the start Node up to the end Node into the inner
    // block.
    moveSiblingExclusive(originalParent, innerBlock, marker.startMarker, marker.endMarker);

    // Add the start node.
    outerBlock.addChildToBack(originalParent.removeChildAfter(outerBlock));
    // Add the inner block
    outerBlock.addChildToBack(innerBlock);
    // and finally the end node.
    outerBlock.addChildToBack(originalParent.removeChildAfter(outerBlock));

    compiler.reportCodeChange();
  }

  /**
   * Move the Nodes between start and end from the source block to the destination block. If start
   * is null, move the first child of the block. If end is null, move the last child of the block.
   */
  private void moveSiblingExclusive(Node src, Node dest, @Nullable Node start, @Nullable Node end) {
    while (childAfter(src, start) != end) {
      Node child = src.removeFirstOrChildAfter(start);
      dest.addChildToBack(child);
    }
  }

  /** Like Node.getNext, that null is used to signal the child before the block. */
  private static Node childAfter(Node parent, @Nullable Node siblingBefore) {
    if (siblingBefore == null) {
      return parent.getFirstChild();
    } else {
      return siblingBefore.getNext();
    }
  }

  private class Callback extends AbstractPostOrderCallback {
    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (!n.isCall() || !n.getFirstChild().isName()) {
        return;
      }

      Node callTarget = n.getFirstChild();
      String callName = callTarget.getString();

      if (startMarkerName.equals(callName)) {
        if (!parent.isExprResult()) {
          compiler.report(t.makeError(n, INVALID_MARKER_USAGE, startMarkerName));
          return;
        }
        markerStack.push(parent);
        return;
      }

      if (!endMarkerName.equals(callName)) {
        return;
      }

      Node endMarkerNode = parent;
      if (!endMarkerNode.isExprResult()) {
        compiler.report(t.makeError(n, INVALID_MARKER_USAGE, endMarkerName));
        return;
      }

      if (markerStack.isEmpty()) {
        compiler.report(t.makeError(n, UNMATCHED_END_MARKER, startMarkerName, endMarkerName));
        return;
      }

      Node startMarkerNode = markerStack.pop();
      if (endMarkerNode.getParent() != startMarkerNode.getParent()) {
        // The end marker isn't in the same block as the start marker.
        compiler.report(t.makeError(n, UNMATCHED_END_MARKER, startMarkerName, endMarkerName));
        return;
      }

      // This is a valid marker set add it to the list of markers to process.
      validMarkers.add(new Marker(startMarkerNode, endMarkerNode));
    }
  }
}
Example #5
0
/**
 * Converts ES6 code to valid ES3 code.
 *
 * @author [email protected] (Tyler Breisacher)
 */
public class Es6ToEs3Converter implements NodeTraversal.Callback, HotSwapCompilerPass {
  private final AbstractCompiler compiler;

  static final DiagnosticType CANNOT_CONVERT =
      DiagnosticType.error("JSC_CANNOT_CONVERT", "This code cannot be converted from ES6 to ES3.");

  // TODO(tbreisacher): Remove this once all ES6 features are transpilable.
  static final DiagnosticType CANNOT_CONVERT_YET =
      DiagnosticType.error(
          "JSC_CANNOT_CONVERT_YET", "ES6-to-ES3 conversion of ''{0}'' is not yet implemented.");

  static final DiagnosticType DYNAMIC_EXTENDS_TYPE =
      DiagnosticType.error(
          "JSC_DYNAMIC_EXTENDS_TYPE", "The class in an extends clause must be a qualified name.");

  static final DiagnosticType STATIC_METHOD_REFERENCES_THIS =
      DiagnosticType.warning(
          "JSC_STATIC_METHOD_REFERENCES_THIS",
          "This static method uses the 'this' keyword."
              + " The transpiled output may be incorrect.");

  // The name of the var that captures 'this' for converting arrow functions.
  private static final String THIS_VAR = "$jscomp$this";

  private static final String FRESH_SPREAD_VAR = "$jscomp$spread$args";

  private int freshSpreadVarCounter = 0;

  private static final String FRESH_COMP_PROP_VAR = "$jscomp$compprop";

  private int freshPropVarCounter = 0;

  public Es6ToEs3Converter(AbstractCompiler compiler) {
    this.compiler = compiler;
  }

  @Override
  public void process(Node externs, Node root) {
    NodeTraversal.traverse(compiler, root, this);
  }

  @Override
  public void hotSwapScript(Node scriptRoot, Node originalRoot) {
    NodeTraversal.traverse(compiler, scriptRoot, this);
  }

  /**
   * Arrow functions must be visited pre-order in order to rewrite the references to {@code this}
   * correctly. Everything else is translated post-order in {@link #visit}.
   */
  @Override
  public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
    switch (n.getType()) {
      case Token.FUNCTION:
        if (n.isArrowFunction()) {
          visitArrowFunction(t, n);
        }
        break;
      case Token.ARRAY_COMP:
      case Token.ARRAY_PATTERN:
      case Token.FOR_OF:
      case Token.OBJECT_PATTERN:
      case Token.SUPER:
        cannotConvertYet(n, Token.name(n.getType()));
        // Don't bother visiting the children of a node if we
        // already know we can't convert the node itself.
        return false;
    }
    return true;
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    switch (n.getType()) {
      case Token.OBJECTLIT:
        for (Node child : n.children()) {
          if (child.isComputedProp()) {
            visitObjectWithComputedProperty(n, parent);
            break;
          }
        }
        break;
      case Token.STRING_KEY:
        visitStringKey(n);
        break;
      case Token.CLASS:
        visitClass(n, parent);
        break;
      case Token.PARAM_LIST:
        visitParamList(n, parent);
        break;
      case Token.ARRAYLIT:
      case Token.NEW:
      case Token.CALL:
        for (Node child : n.children()) {
          if (child.isSpread()) {
            visitArrayLitOrCallWithSpread(n, parent);
            break;
          }
        }
        break;
    }
  }

  /** Converts extended object literal {a} to {a:a}. */
  private void visitStringKey(Node n) {
    if (!n.hasChildren()) {
      Node name = IR.name(n.getString());
      name.copyInformationFrom(n);
      n.addChildToBack(name);
      compiler.reportCodeChange();
    }
  }

  /** Processes trailing default and rest parameters. */
  private void visitParamList(Node paramList, Node function) {
    Node insertSpot = null;
    Node block = function.getLastChild();
    for (int i = 0; i < paramList.getChildCount(); i++) {
      Node param = paramList.getChildAtIndex(i);
      if (param.hasChildren()) { // default parameter
        param.setOptionalArg(true);
        Node defaultValue = param.removeFirstChild();
        // Transpile to: param === undefined && (param = defaultValue);
        Node name = IR.name(param.getString());
        Node undefined = IR.name("undefined");
        Node stm =
            IR.exprResult(
                IR.and(IR.sheq(name, undefined), IR.assign(name.cloneNode(), defaultValue)));
        block.addChildAfter(stm.useSourceInfoIfMissingFromForTree(param), insertSpot);
        insertSpot = stm;
        compiler.reportCodeChange();
      } else if (param.isRest()) { // rest parameter
        param.setType(Token.NAME);
        param.setVarArgs(true);
        // Transpile to: param = [].slice.call(arguments, i);
        Node newArr =
            IR.exprResult(
                IR.assign(
                    IR.name(param.getString()),
                    IR.call(
                        IR.getprop(
                            IR.getprop(IR.arraylit(), IR.string("slice")), IR.string("call")),
                        IR.name("arguments"),
                        IR.number(i))));
        block.addChildAfter(newArr.useSourceInfoIfMissingFromForTree(param), insertSpot);
        compiler.reportCodeChange();
      }
    }
    // For now, we are running transpilation before type-checking, so we'll
    // need to make sure changes don't invalidate the JSDoc annotations.
    // Therefore we keep the parameter list the same length and only initialize
    // the values if they are set to undefined.
  }

  /**
   * Processes array literals or calls containing spreads. Eg.: [1, 2, ...x, 4, 5] => [1,
   * 2].concat(x, [4, 5]); Eg.: f(...arr) => f.apply(null, arr) Eg.: new F(...args) => new
   * Function.prototype.bind.apply(F, [].concat(args))
   */
  private void visitArrayLitOrCallWithSpread(Node node, Node parent) {
    Preconditions.checkArgument(node.isCall() || node.isArrayLit() || node.isNew());
    List<Node> groups = new ArrayList<>();
    Node currGroup = null;
    Node callee = node.isArrayLit() ? null : node.removeFirstChild();
    Node currElement = node.removeFirstChild();
    while (currElement != null) {
      if (currElement.isSpread()) {
        if (currGroup != null) {
          groups.add(currGroup);
          currGroup = null;
        }
        groups.add(currElement.removeFirstChild());
      } else {
        if (currGroup == null) {
          currGroup = IR.arraylit();
        }
        currGroup.addChildToBack(currElement);
      }
      currElement = node.removeFirstChild();
    }
    if (currGroup != null) {
      groups.add(currGroup);
    }
    Node result = null;
    Node joinedGroups =
        IR.call(
            IR.getprop(IR.arraylit(), IR.string("concat")),
            groups.toArray(new Node[groups.size()]));
    if (node.isArrayLit()) {
      result = joinedGroups;
    } else if (node.isCall()) {
      if (NodeUtil.mayHaveSideEffects(callee) && callee.isGetProp()) {
        Node statement = node;
        while (!NodeUtil.isStatement(statement)) {
          statement = statement.getParent();
        }
        Node freshVar = IR.name(FRESH_SPREAD_VAR + freshSpreadVarCounter++);
        Node n = IR.var(freshVar.cloneTree());
        n.useSourceInfoIfMissingFromForTree(statement);
        statement.getParent().addChildBefore(n, statement);
        callee.addChildToFront(IR.assign(freshVar.cloneTree(), callee.removeFirstChild()));
        result = IR.call(IR.getprop(callee, IR.string("apply")), freshVar, joinedGroups);
      } else {
        Node context = callee.isGetProp() ? callee.getFirstChild().cloneTree() : IR.nullNode();
        result = IR.call(IR.getprop(callee, IR.string("apply")), context, joinedGroups);
      }
    } else {
      Node bindApply =
          NodeUtil.newQualifiedNameNode(
              compiler.getCodingConvention(), "Function.prototype.bind.apply");
      result = IR.newNode(bindApply, callee, joinedGroups);
    }
    result.useSourceInfoIfMissingFromForTree(node);
    parent.replaceChild(node, result);
    compiler.reportCodeChange();
  }

  private void visitObjectWithComputedProperty(Node obj, Node parent) {
    Preconditions.checkArgument(obj.isObjectLit());
    List<Node> props = new ArrayList<>();
    Node currElement = obj.getFirstChild();

    while (currElement != null) {
      if (currElement.isGetterDef() || currElement.isSetterDef()) {
        currElement = currElement.getNext();
      } else {
        Node nextNode = currElement.getNext();
        obj.removeChild(currElement);
        props.add(currElement);
        currElement = nextNode;
      }
    }

    String objName = FRESH_COMP_PROP_VAR + freshPropVarCounter++;

    props = Lists.reverse(props);
    Node result = IR.name(objName);
    for (Node propdef : props) {
      if (propdef.isComputedProp()) {
        Node propertyExpression = propdef.removeFirstChild();
        Node value = propdef.removeFirstChild();
        result =
            IR.comma(IR.assign(IR.getelem(IR.name(objName), propertyExpression), value), result);
      } else {
        if (!propdef.hasChildren()) {
          Node name = IR.name(propdef.getString()).copyInformationFrom(propdef);
          propdef.addChildToBack(name);
        }
        Node val = propdef.removeFirstChild();
        propdef.setType(Token.STRING);
        int type = propdef.isQuotedString() ? Token.GETELEM : Token.GETPROP;
        Node access = new Node(type, IR.name(objName), propdef);
        result = IR.comma(IR.assign(access, val), result);
      }
    }

    Node statement = obj;
    while (!NodeUtil.isStatement(statement)) {
      statement = statement.getParent();
    }

    result.useSourceInfoIfMissingFromForTree(obj);
    parent.replaceChild(obj, result);

    Node var = IR.var(IR.name(objName), obj);
    var.useSourceInfoIfMissingFromForTree(statement);
    statement.getParent().addChildBefore(var, statement);
    compiler.reportCodeChange();
  }

  private void visitClass(Node classNode, Node parent) {
    Node className = classNode.getFirstChild();
    Node superClassName = className.getNext();
    Node classMembers = classNode.getLastChild();

    // This is a statement node. We insert methods of the
    // transpiled class after this node.
    Node insertionPoint;

    // The fully qualified name of the class, which will be used in the output.
    // May come from the class itself or the LHS of an assignment.
    String fullClassName = null;

    // Whether the constructor function in the output should be anonymous.
    boolean anonymous;

    // If this is a class statement, or a class expression in a simple
    // assignment or var statement, convert it. In any other case, the
    // code is too dynamic, so just call cannotConvert.
    if (NodeUtil.isStatement(classNode)) {
      fullClassName = className.getString();
      anonymous = false;
      insertionPoint = classNode;
    } else if (parent.isAssign() && parent.getParent().isExprResult()) {
      // Add members after the EXPR_RESULT node:
      // example.C = class {}; example.C.prototype.foo = function() {};
      fullClassName = parent.getFirstChild().getQualifiedName();
      if (fullClassName == null) {
        cannotConvert(parent);
        return;
      }
      anonymous = true;
      insertionPoint = parent.getParent();
    } else if (parent.isName()) {
      // Add members after the 'var' statement.
      // var C = class {}; C.prototype.foo = function() {};
      fullClassName = parent.getString();
      anonymous = true;
      insertionPoint = parent.getParent();
    } else {
      cannotConvert(parent);
      return;
    }

    Verify.verify(NodeUtil.isStatement(insertionPoint));

    className.detachFromParent();
    Node constructor = null;
    JSDocInfo ctorJSDocInfo = null;
    for (Node member : classMembers.children()) {
      if (member.getString().equals("constructor")) {
        ctorJSDocInfo = member.getJSDocInfo();
        constructor = member.getFirstChild().detachFromParent();
        if (!anonymous) {
          constructor.replaceChild(constructor.getFirstChild(), className);
        }
      } else {
        String qualifiedMemberName;
        if (member.isStaticMember()) {
          if (NodeUtil.referencesThis(member.getFirstChild())) {
            compiler.report(JSError.make(member, STATIC_METHOD_REFERENCES_THIS));
          }
          qualifiedMemberName = Joiner.on(".").join(fullClassName, member.getString());
        } else {
          qualifiedMemberName = Joiner.on(".").join(fullClassName, "prototype", member.getString());
        }
        Node assign =
            IR.assign(
                NodeUtil.newQualifiedNameNode(
                    compiler.getCodingConvention(),
                    qualifiedMemberName,
                    /* basis node */ member,
                    /* original name */ member.getString()),
                member.getFirstChild().detachFromParent());
        assign.srcref(member);

        JSDocInfo info = member.getJSDocInfo();
        if (info != null) {
          info.setAssociatedNode(assign);
          assign.setJSDocInfo(info);
        }

        Node newNode = NodeUtil.newExpr(assign);
        insertionPoint.getParent().addChildAfter(newNode, insertionPoint);
        insertionPoint = newNode;
      }
    }

    if (constructor == null) {
      Node name = anonymous ? IR.name("").srcref(className) : className;
      constructor =
          IR.function(name, IR.paramList().srcref(classNode), IR.block().srcref(classNode));
    }
    JSDocInfo classJSDoc = classNode.getJSDocInfo();
    JSDocInfoBuilder newInfo =
        (classJSDoc != null) ? JSDocInfoBuilder.copyFrom(classJSDoc) : new JSDocInfoBuilder(true);

    newInfo.recordConstructor();
    if (!superClassName.isEmpty()) {
      if (!superClassName.isQualifiedName()) {
        compiler.report(JSError.make(superClassName, DYNAMIC_EXTENDS_TYPE));
        return;
      }

      Node superClassString = IR.string(superClassName.getQualifiedName());
      if (newInfo.isInterfaceRecorded()) {
        newInfo.recordExtendedInterface(
            new JSTypeExpression(
                new Node(Token.BANG, superClassString), superClassName.getSourceFileName()));
      } else {
        // TODO(mattloring) Remove dependency on Closure Library.
        Node inherits =
            NodeUtil.newQualifiedNameNode(compiler.getCodingConvention(), "goog.inherits");
        Node inheritsCall =
            IR.exprResult(IR.call(inherits, className.cloneTree(), superClassName.cloneTree()));
        inheritsCall.useSourceInfoIfMissingFromForTree(classNode);
        parent.addChildAfter(inheritsCall, classNode);
        newInfo.recordBaseType(
            new JSTypeExpression(
                new Node(Token.BANG, superClassString), superClassName.getSourceFileName()));
      }
    }

    // Classes are @struct by default.
    if (!newInfo.isUnrestrictedRecorded()
        && !newInfo.isDictRecorded()
        && !newInfo.isStructRecorded()) {
      newInfo.recordStruct();
    }

    if (ctorJSDocInfo != null) {
      newInfo.recordSuppressions(ctorJSDocInfo.getSuppressions());
      for (String param : ctorJSDocInfo.getParameterNames()) {
        newInfo.recordParameter(param, ctorJSDocInfo.getParameterType(param));
      }
    }
    parent.replaceChild(classNode, constructor);

    if (NodeUtil.isStatement(constructor)) {
      constructor.setJSDocInfo(newInfo.build(constructor));
    } else if (parent.isName()) {
      // The constructor function is the RHS of a var statement.
      // Add the JSDoc to the VAR node.
      Node var = parent.getParent();
      var.setJSDocInfo(newInfo.build(var));
    } else if (parent.isAssign()) {
      // The constructor function is the RHS of an assignment.
      // Add the JSDoc to the ASSIGN node.
      parent.setJSDocInfo(newInfo.build(parent));
    } else {
      throw new IllegalStateException("Unexpected parent node " + parent);
    }

    compiler.reportCodeChange();
  }

  /** Converts ES6 arrow functions to standard anonymous ES3 functions. */
  private void visitArrowFunction(NodeTraversal t, Node n) {
    n.setIsArrowFunction(false);
    Node body = n.getLastChild();
    if (!body.isBlock()) {
      body.detachFromParent();
      Node newBody = IR.block(IR.returnNode(body).srcref(body)).srcref(body);
      n.addChildToBack(newBody);
    }

    UpdateThisNodes thisUpdater = new UpdateThisNodes();
    NodeTraversal.traverse(compiler, body, thisUpdater);
    if (thisUpdater.changed) {
      addThisVar(t);
    }

    compiler.reportCodeChange();
  }

  private void addThisVar(NodeTraversal t) {
    Scope scope = t.getScope();
    if (scope.isDeclared(THIS_VAR, false)) {
      return;
    }

    Node parent = t.getScopeRoot();
    if (parent.isFunction()) {
      // Add the new node at the beginning of the function body.
      parent = parent.getLastChild();
    }
    if (parent.isSyntheticBlock()) {
      // Add the new node inside the SCRIPT node instead of the
      // synthetic block that contains it.
      parent = parent.getFirstChild();
    }

    Node name = IR.name(THIS_VAR).srcref(parent);
    Node thisVar = IR.var(name, IR.thisNode().srcref(parent));
    thisVar.srcref(parent);
    parent.addChildToFront(thisVar);
    scope.declare(THIS_VAR, name, null, compiler.getInput(parent.getInputId()));
  }

  private static class UpdateThisNodes implements NodeTraversal.Callback {
    private boolean changed = false;

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (n.isThis()) {
        Node name = IR.name(THIS_VAR).srcref(n);
        parent.replaceChild(n, name);
        changed = true;
      }
    }

    @Override
    public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
      return !n.isFunction() || n.isArrowFunction();
    }
  }

  private void cannotConvert(Node n) {
    compiler.report(JSError.make(n, CANNOT_CONVERT));
  }

  /**
   * Warns the user that the given ES6 feature cannot be converted to ES3 because the transpilation
   * is not yet implemented. A call to this method is essentially a "TODO(tbreisacher): Implement
   * {@code feature}" comment.
   */
  private void cannotConvertYet(Node n, String feature) {
    compiler.report(JSError.make(n, CANNOT_CONVERT_YET, feature));
  }
}
/**
 * A pass for stripping a list of provided Javascript object types.
 *
 * <p>The stripping strategy is as follows: - Provide: 1) a list of types that should be stripped,
 * and 2) a list of suffixes of field/variable names that should be stripped. - Remove declarations
 * of variables that are initialized using static methods of strip types (e.g. var x =
 * goog.debug.Logger.getLogger(...);). - Remove all references to variables that are stripped. -
 * Remove all object literal keys with strip names. - Remove all assignments to 1) field names that
 * are strip names and 2) qualified names that begin with strip types. - Remove all statements
 * containing calls to static methods of strip types.
 */
class StripCode implements CompilerPass {

  // TODO(user): Try eliminating the need for a list of strip names by instead
  // recording which field names are assigned to debug types in each js input.
  private final AbstractCompiler compiler;
  private final Set<String> stripTypes;
  private final Set<String> stripNameSuffixes;
  private final Set<String> stripTypePrefixes;
  private final Set<String> stripNamePrefixes;
  private final Set<Scope.Var> varsToRemove;

  static final DiagnosticType STRIP_TYPE_INHERIT_ERROR =
      DiagnosticType.error(
          "JSC_STRIP_TYPE_INHERIT_ERROR", "Non-strip type {0} cannot inherit from strip type {1}");

  static final DiagnosticType STRIP_ASSIGNMENT_ERROR =
      DiagnosticType.error("JSC_STRIP_ASSIGNMENT_ERROR", "Unable to strip assignment to {0}");

  /**
   * Creates an instance.
   *
   * @param compiler The compiler
   */
  StripCode(
      AbstractCompiler compiler,
      Set<String> stripTypes,
      Set<String> stripNameSuffixes,
      Set<String> stripTypePrefixes,
      Set<String> stripNamePrefixes) {

    this.compiler = compiler;
    this.stripTypes = Sets.newHashSet(stripTypes);
    this.stripNameSuffixes = Sets.newHashSet(stripNameSuffixes);
    this.stripTypePrefixes = Sets.newHashSet(stripTypePrefixes);
    this.stripNamePrefixes = Sets.newHashSet(stripNamePrefixes);
    this.varsToRemove = Sets.newHashSet();
  }

  /** Enables stripping of goog.tweak functions. */
  public void enableTweakStripping() {
    stripTypes.add("goog.tweak");
  }

  public void process(Node externs, Node root) {
    NodeTraversal.traverse(compiler, root, new Strip());
  }

  // -------------------------------------------------------------------------

  /** A callback that strips debug code from a Javascript parse tree. */
  private class Strip extends AbstractPostOrderCallback {

    public void visit(NodeTraversal t, Node n, Node parent) {
      switch (n.getType()) {
        case Token.VAR:
          removeVarDeclarationsByNameOrRvalue(t, n, parent);
          break;

        case Token.NAME:
          maybeRemoveReferenceToRemovedVariable(t, n, parent);
          break;

        case Token.ASSIGN:
        case Token.ASSIGN_BITOR:
        case Token.ASSIGN_BITXOR:
        case Token.ASSIGN_BITAND:
        case Token.ASSIGN_LSH:
        case Token.ASSIGN_RSH:
        case Token.ASSIGN_URSH:
        case Token.ASSIGN_ADD:
        case Token.ASSIGN_SUB:
        case Token.ASSIGN_MUL:
        case Token.ASSIGN_DIV:
        case Token.ASSIGN_MOD:
          maybeEliminateAssignmentByLvalueName(t, n, parent);
          break;

        case Token.CALL:
        case Token.NEW:
          maybeRemoveCall(t, n, parent);
          break;

        case Token.OBJECTLIT:
          eliminateKeysWithStripNamesFromObjLit(t, n);
          break;

        case Token.EXPR_RESULT:
          maybeEliminateExpressionByName(t, n, parent);
          break;
      }
    }

    /**
     * Removes declarations of any variables whose names are strip names or whose whose rvalues are
     * static method calls on strip types. Builds a set of removed variables so that all references
     * to them can be removed.
     *
     * @param t The traversal
     * @param n A VAR node
     * @param parent {@code n}'s parent
     */
    void removeVarDeclarationsByNameOrRvalue(NodeTraversal t, Node n, Node parent) {
      for (Node nameNode = n.getFirstChild(); nameNode != null; nameNode = nameNode.getNext()) {
        String name = nameNode.getString();
        if (isStripName(name) || isCallWhoseReturnValueShouldBeStripped(nameNode.getFirstChild())) {
          // Remove the NAME.
          Scope scope = t.getScope();
          varsToRemove.add(scope.getVar(name));
          n.removeChild(nameNode);
          compiler.reportCodeChange();
        }
      }
      if (!n.hasChildren()) {
        // Must also remove the VAR.
        replaceWithEmpty(n, parent);
        compiler.reportCodeChange();
      }
    }

    /**
     * Removes a reference if it is a reference to a removed variable.
     *
     * @param t The traversal
     * @param n A NAME node
     * @param parent {@code n}'s parent
     */
    void maybeRemoveReferenceToRemovedVariable(NodeTraversal t, Node n, Node parent) {
      switch (parent.getType()) {
        case Token.VAR:
          // This is a variable decalaration, not a reference.
          break;

        case Token.GETPROP:
          // GETPROP
          //   NAME
          //   STRING (property name)
        case Token.GETELEM:
          // GETELEM
          //   NAME
          //   NUMBER|STRING|NAME|...
          if (parent.getFirstChild() == n && isReferenceToRemovedVar(t, n)) {
            replaceHighestNestedCallWithNull(parent, parent.getParent());
          }
          break;

        case Token.ASSIGN:
        case Token.ASSIGN_BITOR:
        case Token.ASSIGN_BITXOR:
        case Token.ASSIGN_BITAND:
        case Token.ASSIGN_LSH:
        case Token.ASSIGN_RSH:
        case Token.ASSIGN_URSH:
        case Token.ASSIGN_ADD:
        case Token.ASSIGN_SUB:
        case Token.ASSIGN_MUL:
        case Token.ASSIGN_DIV:
        case Token.ASSIGN_MOD:
          if (isReferenceToRemovedVar(t, n)) {
            if (parent.getFirstChild() == n) {
              Node gramps = parent.getParent();
              if (NodeUtil.isExpressionNode(gramps)) {
                // Remove the assignment.
                Node greatGramps = gramps.getParent();
                replaceWithEmpty(gramps, greatGramps);
                compiler.reportCodeChange();
              } else {
                // Substitute the rvalue for the assignment.
                Node rvalue = n.getNext();
                parent.removeChild(rvalue);
                gramps.replaceChild(parent, rvalue);
                compiler.reportCodeChange();
              }
            } else {
              // The var reference is the rvalue. Replace it with null.
              replaceWithNull(n, parent);
              compiler.reportCodeChange();
            }
          }
          break;

        default:
          if (isReferenceToRemovedVar(t, n)) {
            replaceWithNull(n, parent);
            compiler.reportCodeChange();
          }
          break;
      }
    }

    /**
     * Use a while loop to get up out of any nested calls. For example, if we have just detected
     * that we need to remove the a.b() call in a.b().c().d(), we'll have to remove all of the
     * calls, and it will take a few iterations through this loop to get up to d().
     */
    void replaceHighestNestedCallWithNull(Node node, Node parent) {
      Node ancestor = parent;
      Node ancestorChild = node;
      while (true) {
        if (ancestor.getFirstChild() != ancestorChild) {
          replaceWithNull(ancestorChild, ancestor);
          break;
        }
        if (NodeUtil.isExpressionNode(ancestor)) {
          // Remove the entire expression statement.
          Node ancParent = ancestor.getParent();
          replaceWithEmpty(ancestor, ancParent);
          break;
        }
        int type = ancestor.getType();
        if (type != Token.GETPROP && type != Token.GETELEM && type != Token.CALL) {
          replaceWithNull(ancestorChild, ancestor);
          break;
        }
        ancestorChild = ancestor;
        ancestor = ancestor.getParent();
      }
      compiler.reportCodeChange();
    }

    /**
     * Eliminates an assignment if the lvalue is: - A field name that's a strip name - A qualified
     * name that begins with a strip type
     *
     * @param t The traversal
     * @param n An ASSIGN node
     * @param parent {@code n}'s parent
     */
    void maybeEliminateAssignmentByLvalueName(NodeTraversal t, Node n, Node parent) {
      // ASSIGN
      //   lvalue
      //   rvalue
      Node lvalue = n.getFirstChild();
      if (nameEndsWithFieldNameToStrip(lvalue) || qualifiedNameBeginsWithStripType(lvalue)) {

        // Limit to EXPR_RESULT because it is not
        // safe to eliminate assignment in complex expressions,
        // e.g. in ((x = 7) + 8)
        if (NodeUtil.isExpressionNode(parent)) {
          Node gramps = parent.getParent();
          replaceWithEmpty(parent, gramps);
          compiler.reportCodeChange();
        } else {
          t.report(n, STRIP_ASSIGNMENT_ERROR, lvalue.getQualifiedName());
        }
      }
    }

    /**
     * Eliminates an expression if it refers to: - A field name that's a strip name - A qualified
     * name that begins with a strip type This gets rid of construct like: a.prototype.logger; (used
     * instead of a.prototype.logger = null;) This expression is not an assignment and so will not
     * be caught by maybeEliminateAssignmentByLvalueName.
     *
     * @param t The traversal
     * @param n An EXPR_RESULT node
     * @param parent {@code n}'s parent
     */
    void maybeEliminateExpressionByName(NodeTraversal t, Node n, Node parent) {
      // EXPR_RESULT
      //   expression
      Node expression = n.getFirstChild();
      if (nameEndsWithFieldNameToStrip(expression)
          || qualifiedNameBeginsWithStripType(expression)) {
        if (NodeUtil.isExpressionNode(parent)) {
          Node gramps = parent.getParent();
          replaceWithEmpty(parent, gramps);
        } else {
          replaceWithEmpty(n, parent);
        }
        compiler.reportCodeChange();
      }
    }

    /**
     * Removes a method call if {@link #isMethodOrCtorCallThatTriggersRemoval} indicates that it
     * should be removed.
     *
     * @param t The traversal
     * @param n A CALL node
     * @param parent {@code n}'s parent
     */
    void maybeRemoveCall(NodeTraversal t, Node n, Node parent) {
      // CALL/NEW
      //   function
      //   arguments
      if (isMethodOrCtorCallThatTriggersRemoval(t, n, parent)) {
        replaceHighestNestedCallWithNull(n, parent);
      }
    }

    /**
     * Eliminates any object literal keys in an object literal declaration that have strip names.
     *
     * @param t The traversal
     * @param n An OBJLIT node
     */
    void eliminateKeysWithStripNamesFromObjLit(NodeTraversal t, Node n) {
      // OBJLIT
      //   key1
      //     value1
      //   key2
      //   ...
      Node key = n.getFirstChild();
      while (key != null) {
        if (isStripName(key.getString())) {
          Node value = key.getFirstChild();
          Node next = key.getNext();
          n.removeChild(key);
          key = next;
          compiler.reportCodeChange();
        } else {
          key = key.getNext();
        }
      }
    }

    /**
     * Gets whether a node is a CALL node whose return value should be stripped. A call's return
     * value should be stripped if the function getting called is a static method in a class that
     * gets stripped. For example, if "goog.debug.Logger" is a strip name, then this function
     * returns true for a call such as "goog.debug.Logger.getLogger(...)". It may also simply be a
     * function that is getting stripped. For example, if "getLogger" is a strip name, but not
     * "goog.debug.Logger", this will still return true.
     *
     * @param n A node (typically a CALL node)
     * @return Whether the call's return value should be stripped
     */
    boolean isCallWhoseReturnValueShouldBeStripped(@Nullable Node n) {
      return n != null
          && (n.getType() == Token.CALL || n.getType() == Token.NEW)
          && n.hasChildren()
          && (qualifiedNameBeginsWithStripType(n.getFirstChild())
              || nameEndsWithFieldNameToStrip(n.getFirstChild()));
    }

    /**
     * Gets whether a qualified name begins with a strip name. The names "goog.debug",
     * "goog.debug.Logger", and "goog.debug.Logger.Level" are examples of strip names that would
     * result in this function returning true for a node representing the name
     * "goog.debug.Logger.Level".
     *
     * @param n A node (typically a NAME or GETPROP node)
     * @return Whether the name begins with a strip name
     */
    boolean qualifiedNameBeginsWithStripType(Node n) {
      String name = n.getQualifiedName();
      return qualifiedNameBeginsWithStripType(name);
    }

    /**
     * Gets whether a qualified name begins with a strip name. The names "goog.debug",
     * "goog.debug.Logger", and "goog.debug.Logger.Level" are examples of strip names that would
     * result in this function returning true for a node representing the name
     * "goog.debug.Logger.Level".
     *
     * @param name A qualified class name
     * @return Whether the name begins with a strip name
     */
    boolean qualifiedNameBeginsWithStripType(String name) {
      if (name != null) {
        for (String type : stripTypes) {
          if (name.equals(type) || name.startsWith(type + ".")) {
            return true;
          }
        }
        for (String type : stripTypePrefixes) {
          if (name.startsWith(type)) {
            return true;
          }
        }
      }
      return false;
    }

    /**
     * Determines whether a NAME node represents a reference to a variable that has been removed.
     *
     * @param t The traversal
     * @param n A NAME node
     * @return Whether the variable was removed
     */
    boolean isReferenceToRemovedVar(NodeTraversal t, Node n) {
      String name = n.getString();
      Scope scope = t.getScope();
      Scope.Var var = scope.getVar(name);
      return varsToRemove.contains(var);
    }

    /**
     * Gets whether a CALL node triggers statement removal, based on the name of the object whose
     * method is being called, or the name of the method. Checks whether the name begins with a
     * strip type, ends with a field name that's a strip name, or belongs to the set of global
     * class-defining functions (e.g. goog.inherits).
     *
     * @param t The traversal
     * @param n A CALL node
     * @return Whether the node triggers statement removal
     */
    boolean isMethodOrCtorCallThatTriggersRemoval(NodeTraversal t, Node n, Node parent) {
      // CALL/NEW
      //   GETPROP (function)         <-- we're interested in this, the function
      //     GETPROP (callee object)  <-- or the object on which it is called
      //       ...
      //       STRING (field name)
      //     STRING (method name)
      //   ... (arguments)

      Node function = n.getFirstChild();
      if (function == null || function.getType() != Token.GETPROP) {
        // We are only interested in calls on object references that are
        // properties. We don't need to eliminate method calls on variables
        // that are getting removed, since that's already done by the code
        // that removes all references to those variables.
        return false;
      }

      if (parent != null && parent.getType() == Token.NAME) {
        Node gramps = parent.getParent();
        if (gramps != null && gramps.getType() == Token.VAR) {
          // The call's return value is being used to initialize a newly
          // declared variable. We should leave the call intact for now.
          // That way, when the traversal reaches the variable declaration,
          // we'll recognize that the variable and all references to it need
          // to be eliminated.
          return false;
        }
      }

      Node callee = function.getFirstChild();
      return nameEndsWithFieldNameToStrip(callee)
          || nameEndsWithFieldNameToStrip(function)
          || qualifiedNameBeginsWithStripType(function)
          || actsOnStripType(t, n);
    }

    /**
     * Gets whether a name ends with a field name that should be stripped. For example, this
     * function would return true when passed "this.logger" or "a.b.c.myLogger" if "logger" is a
     * strip name.
     *
     * @param n A node (typically a GETPROP node)
     * @return Whether the name ends with a field name that should be stripped
     */
    boolean nameEndsWithFieldNameToStrip(@Nullable Node n) {
      if (n != null && n.getType() == Token.GETPROP) {
        Node propNode = n.getLastChild();
        return propNode != null
            && propNode.getType() == Token.STRING
            && isStripName(propNode.getString());
      }
      return false;
    }

    /**
     * Determines whether the given node helps to define a strip type. For example,
     * goog.inherits(stripType, Object) would be such a call.
     *
     * <p>Also reports an error if a non-strip type inherits from a strip type.
     *
     * @param t The current traversal
     * @param callNode The CALL node
     */
    private boolean actsOnStripType(NodeTraversal t, Node callNode) {
      SubclassRelationship classes =
          compiler.getCodingConvention().getClassesDefinedByCall(callNode);
      if (classes != null) {
        // It's okay to strip a type that inherits from a non-stripped type
        // e.g. goog.inherits(goog.debug.Logger, Object)
        if (qualifiedNameBeginsWithStripType(classes.subclassName)) {
          return true;
        }

        // report an error if a non-strip type inherits from a
        // strip type.
        if (qualifiedNameBeginsWithStripType(classes.superclassName)) {
          t.report(
              callNode, STRIP_TYPE_INHERIT_ERROR, classes.subclassName, classes.superclassName);
        }
      }

      return false;
    }

    /**
     * Gets whether a Javascript identifier is the name of a variable or property that should be
     * stripped.
     *
     * @param name A Javascript identifier
     * @return Whether {@code name} is a name that triggers removal
     */
    boolean isStripName(String name) {
      if (stripNameSuffixes.contains(name) || stripNamePrefixes.contains(name)) {
        return true;
      }

      if ((name.length() == 0) || Character.isUpperCase(name.charAt(0))) {
        return false;
      }

      String lcName = name.toLowerCase();
      for (String stripName : stripNamePrefixes) {
        if (lcName.startsWith(stripName.toLowerCase())) {
          return true;
        }
      }

      for (String stripName : stripNameSuffixes) {
        if (lcName.endsWith(stripName.toLowerCase())) {
          return true;
        }
      }

      return false;
    }

    /**
     * Replaces a node with a NULL node. This is useful where a value is expected.
     *
     * @param n A node
     * @param parent {@code n}'s parent
     */
    void replaceWithNull(Node n, Node parent) {
      parent.replaceChild(n, new Node(Token.NULL));
    }

    /**
     * Replaces a node with an EMPTY node. This is useful where a statement is expected.
     *
     * @param n A node
     * @param parent {@code n}'s parent
     */
    void replaceWithEmpty(Node n, Node parent) {
      NodeUtil.removeChild(parent, n);
    }
  }
}
/**
 * This pass walks the AST to create a Collection of 'new' nodes and 'goog.require' nodes. It
 * reconciles these Collections, creating a warning for each discrepancy.
 *
 * <p>The rules on when a warning is reported are:
 *
 * <ul>
 *   <li>Type is referenced in code -> goog.require is required (missingRequires check fails if it's
 *       not there)
 *   <li>Type is referenced in an @extends or @implements -> goog.require is required
 *       (missingRequires check fails if it's not there)
 *   <li>Type is referenced in other JsDoc (@type etc) -> goog.require is optional (don't warn,
 *       regardless of if it is there)
 *   <li>Type is not referenced at all -> goog.require is forbidden (extraRequires check fails if it
 *       is there)
 * </ul>
 */
class CheckRequiresForConstructors implements HotSwapCompilerPass, NodeTraversal.Callback {
  private final AbstractCompiler compiler;
  private final CodingConvention codingConvention;

  public static enum Mode {
    // Looking at a single file. Only a minimal set of externs are present.
    SINGLE_FILE,
    // Used during a normal compilation. The entire program + externs are available.
    FULL_COMPILE
  };

  private final Mode mode;

  private final Set<String> providedNames = new HashSet<>();
  private final Map<String, Node> requires = new HashMap<>();

  // Only used in single-file mode.
  private final Set<String> closurizedNamespaces = new HashSet<>();

  // Adding an entry to usages indicates that the name is used and should be required.
  private final Map<String, Node> usages = new HashMap<>();

  // Adding an entry to weakUsages indicates that the name is used, but in a way which may not
  // require a goog.require, such as in a @type annotation. If the only usages of a name are
  // in weakUsages, don't give a missingRequire warning, nor an extraRequire warning.
  private final Map<String, Node> weakUsages = new HashMap<>();

  static final DiagnosticType MISSING_REQUIRE_WARNING =
      DiagnosticType.disabled("JSC_MISSING_REQUIRE_WARNING", "missing require: ''{0}''");

  // Essentially the same as MISSING_REQUIRE_WARNING except that if the user calls foo.bar.baz()
  // then we don't know whether they should require it as goog.require('foo.bar.baz') or as
  // goog.require('foo.bar'). So, warn but don't provide a suggested fix.
  static final DiagnosticType MISSING_REQUIRE_CALL_WARNING =
      DiagnosticType.disabled(
          "JSC_MISSING_REQUIRE_CALL_WARNING", "No matching require found for ''{0}''");

  static final DiagnosticType EXTRA_REQUIRE_WARNING =
      DiagnosticType.disabled("JSC_EXTRA_REQUIRE_WARNING", "extra require: ''{0}''");

  static final DiagnosticType DUPLICATE_REQUIRE_WARNING =
      DiagnosticType.disabled("JSC_DUPLICATE_REQUIRE_WARNING", "''{0}'' required more than once.");

  private static final Set<String> DEFAULT_EXTRA_NAMESPACES =
      ImmutableSet.of("goog.testing.asserts", "goog.testing.jsunit");

  CheckRequiresForConstructors(AbstractCompiler compiler, Mode mode) {
    this.compiler = compiler;
    this.mode = mode;
    this.codingConvention = compiler.getCodingConvention();
  }

  /**
   * Uses Collections of new and goog.require nodes to create a compiler warning for each new class
   * name without a corresponding goog.require().
   */
  @Override
  public void process(Node externs, Node root) {
    NodeTraversal.traverseRootsEs6(compiler, this, externs, root);
  }

  @Override
  public void hotSwapScript(Node scriptRoot, Node originalRoot) {
    NodeTraversal.traverseEs6(compiler, scriptRoot, this);
  }

  // Return true if the name is a class name (starts with an uppercase
  // character). This also matches for all-caps constants, which eliminates
  // some false positives (e.g. goog.LOCALE.replace()).
  private static boolean isClassName(String name) {
    return name != null && name.length() > 1 && Character.isUpperCase(name.charAt(0));
  }

  // Return the shortest prefix of the className that refers to a class,
  // or null if no part refers to a class.
  private static String getOutermostClassName(String className) {
    for (String part : Splitter.on('.').split(className)) {
      if (isClassName(part)) {
        return className.substring(0, className.indexOf(part) + part.length());
      }
    }

    return null;
  }

  @Override
  public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
    return parent == null || !parent.isScript() || !t.getInput().isExtern();
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    maybeAddJsDocUsages(t, n);
    switch (n.getType()) {
      case Token.ASSIGN:
      case Token.VAR:
      case Token.LET:
      case Token.CONST:
        maybeAddProvidedName(n);
        break;
      case Token.FUNCTION:
        // Exclude function expressions.
        if (NodeUtil.isStatement(n)) {
          maybeAddProvidedName(n);
        }
        break;
      case Token.NAME:
        if (!NodeUtil.isLValue(n)) {
          visitQualifiedName(n);
        }
        break;
      case Token.GETPROP:
        visitQualifiedName(n);
        break;
      case Token.CALL:
        visitCallNode(t, n, parent);
        break;
      case Token.SCRIPT:
        visitScriptNode(t);
        reset();
        break;
      case Token.NEW:
        visitNewNode(t, n);
        break;
      case Token.CLASS:
        visitClassNode(t, n);
        break;
      case Token.IMPORT:
        visitImportNode(n);
        break;
    }
  }

  private void reset() {
    this.usages.clear();
    this.weakUsages.clear();
    this.requires.clear();
    this.closurizedNamespaces.clear();
    this.providedNames.clear();
  }

  private void visitScriptNode(NodeTraversal t) {
    if (mode == Mode.SINGLE_FILE && requires.isEmpty()) {
      // Likely a file that isn't using Closure at all.
      return;
    }

    Set<String> namespaces = new HashSet<>();

    // For every usage, check that there is a goog.require, and warn if not.
    for (Map.Entry<String, Node> entry : usages.entrySet()) {
      String namespace = entry.getKey();
      if (namespace.endsWith(".call") || namespace.endsWith(".apply")) {
        namespace = namespace.substring(0, namespace.lastIndexOf('.'));
      }
      if (namespace.startsWith("goog.global.")) {
        continue;
      }

      Node node = entry.getValue();
      JSDocInfo info = NodeUtil.getBestJSDocInfo(NodeUtil.getEnclosingStatement(node));
      if (info != null && info.getSuppressions().contains("missingRequire")) {
        continue;
      }

      String outermostClassName = getOutermostClassName(namespace);
      // The parent namespace is also checked as part of the requires so that classes
      // used by goog.module are still checked properly. This may cause missing requires
      // to be missed but in practice that should happen rarely.
      String nonNullClassName = outermostClassName != null ? outermostClassName : namespace;
      String parentNamespace = null;
      int separatorIndex = nonNullClassName.lastIndexOf('.');
      if (separatorIndex > 0) {
        parentNamespace = nonNullClassName.substring(0, separatorIndex);
      }
      boolean notProvidedByConstructors =
          !providedNames.contains(namespace)
              && !providedNames.contains(outermostClassName)
              && !providedNames.contains(parentNamespace);
      boolean notProvidedByRequires =
          !requires.containsKey(namespace)
              && !requires.containsKey(outermostClassName)
              && !requires.containsKey(parentNamespace);
      if (notProvidedByConstructors
          && notProvidedByRequires
          && !namespaces.contains(namespace)
          && !"goog".equals(parentNamespace)) {
        // TODO(mknichel): If the symbol is not explicitly provided, find the next best
        // symbol from the provides in the same file.
        String rootName = Splitter.on('.').split(namespace).iterator().next();
        if (mode != Mode.SINGLE_FILE || closurizedNamespaces.contains(rootName)) {
          if (node.isCall()) {
            compiler.report(t.makeError(node, MISSING_REQUIRE_CALL_WARNING, namespace));
          } else {
            compiler.report(t.makeError(node, MISSING_REQUIRE_WARNING, namespace));
          }
          namespaces.add(namespace);
        }
      }
    }

    // For every goog.require, check that there is a usage (in either usages or weakUsages)
    // and warn if there is not.
    for (Map.Entry<String, Node> entry : requires.entrySet()) {
      String require = entry.getKey();
      Node call = entry.getValue();
      Node parent = call.getParent();
      if (parent.isAssign()) {
        // var baz = goog.require('foo.bar.baz');
        // Assume that the var 'baz' is used somewhere, and don't warn.
        continue;
      }
      if (!usages.containsKey(require) && !weakUsages.containsKey(require)) {
        reportExtraRequireWarning(call, require);
      }
    }
  }

  private void reportExtraRequireWarning(Node call, String require) {
    if (DEFAULT_EXTRA_NAMESPACES.contains(require)) {
      return;
    }
    JSDocInfo jsDoc = call.getJSDocInfo();
    if (jsDoc != null && jsDoc.getSuppressions().contains("extraRequire")) {
      // There is a @suppress {extraRequire} on the call node. Even though the compiler generally
      // doesn't understand @suppress in that position, respect it in this case,
      // since lots of people put it there to suppress the closure-linter's extraRequire check.
      return;
    }
    compiler.report(JSError.make(call, EXTRA_REQUIRE_WARNING, require));
  }

  private void reportDuplicateRequireWarning(Node call, String require) {
    compiler.report(JSError.make(call, DUPLICATE_REQUIRE_WARNING, require));
  }

  private void visitRequire(String requiredName, Node node) {
    if (requires.containsKey(requiredName)) {
      reportDuplicateRequireWarning(node, requiredName);
    } else {
      requires.put(requiredName, node);
      if (mode == Mode.SINGLE_FILE) {
        String rootName = Splitter.on('.').split(requiredName).iterator().next();
        closurizedNamespaces.add(rootName);
      }
    }
  }

  private void visitImportNode(Node importNode) {
    Node defaultImport = importNode.getFirstChild();
    if (defaultImport.isName()) {
      visitRequire(defaultImport.getString(), importNode);
    }
    Node namedImports = defaultImport.getNext();
    if (namedImports.getType() == Token.IMPORT_SPECS) {
      for (Node importSpec : namedImports.children()) {
        visitRequire(importSpec.getLastChild().getString(), importNode);
      }
    }
  }

  private void visitCallNode(NodeTraversal t, Node call, Node parent) {
    String required = codingConvention.extractClassNameIfRequire(call, parent);
    if (required != null) {
      visitRequire(required, call);
      return;
    }
    String provided = codingConvention.extractClassNameIfProvide(call, parent);
    if (provided != null) {
      providedNames.add(provided);
      return;
    }

    if (codingConvention.isClassFactoryCall(call)) {
      if (parent.isName()) {
        providedNames.add(parent.getString());
      } else if (parent.isAssign()) {
        providedNames.add(parent.getFirstChild().getQualifiedName());
      }
    }

    Node callee = call.getFirstChild();
    if (callee.isName()) {
      weakUsages.put(callee.getString(), callee);
    } else if (callee.isQualifiedName()) {
      Node root = NodeUtil.getRootOfQualifiedName(callee);
      if (root.isName()) {
        Var var = t.getScope().getVar(root.getString());
        if (var == null || (!var.isExtern() && !var.isLocal())) {
          String name = getOutermostClassName(callee.getQualifiedName());
          if (name == null) {
            name = callee.getQualifiedName();
          }
          usages.put(name, call);
        }
      }
    }
  }

  private void visitQualifiedName(Node getprop) {
    // For "foo.bar.baz.qux" add weak usages for "foo.bar.baz.qux", "foo.bar.baz",
    // "foo.bar", and "foo" because those might all be goog.provide'd in different files,
    // so it doesn't make sense to require the user to goog.require all of them.
    for (; getprop != null; getprop = getprop.getFirstChild()) {
      weakUsages.put(getprop.getQualifiedName(), getprop);
    }
  }

  private void visitNewNode(NodeTraversal t, Node newNode) {
    Node qNameNode = newNode.getFirstChild();

    // Single names are likely external, but if this is running in single-file mode, they
    // will not be in the externs, so add a weak usage.
    if (mode == Mode.SINGLE_FILE && qNameNode.isName()) {
      weakUsages.put(qNameNode.getString(), qNameNode);
      return;
    }

    // If the ctor is something other than a qualified name, ignore it.
    if (!qNameNode.isQualifiedName()) {
      return;
    }

    // Grab the root ctor namespace.
    Node root = NodeUtil.getRootOfQualifiedName(qNameNode);

    // We only consider programmer-defined constructors that are
    // global variables, or are defined on global variables.
    if (!root.isName()) {
      return;
    }

    String name = root.getString();
    Var var = t.getScope().getVar(name);
    if (var != null && (var.isExtern() || var.getSourceFile() == newNode.getStaticSourceFile())) {
      return;
    }
    usages.put(qNameNode.getQualifiedName(), newNode);

    // for "new foo.bar.Baz.Qux" add weak usages for "foo.bar.Baz", "foo.bar", and "foo"
    // because those might be goog.provide'd from a different file than foo.bar.Baz.Qux,
    // so it doesn't make sense to require the user to goog.require all of them.
    for (; qNameNode != null; qNameNode = qNameNode.getFirstChild()) {
      weakUsages.put(qNameNode.getQualifiedName(), qNameNode);
    }
  }

  private void visitClassNode(NodeTraversal t, Node classNode) {
    String name = NodeUtil.getName(classNode);
    if (name != null) {
      providedNames.add(name);
    }

    Node extendClass = classNode.getSecondChild();

    // If the superclass is something other than a qualified name, ignore it.
    if (!extendClass.isQualifiedName()) {
      return;
    }

    // Single names are likely external, but if this is running in single-file mode, they
    // will not be in the externs, so add a weak usage.
    if (mode == Mode.SINGLE_FILE && extendClass.isName()) {
      weakUsages.put(extendClass.getString(), extendClass);
      return;
    }

    Node root = NodeUtil.getRootOfQualifiedName(extendClass);

    // It should always be a name. Extending this.something or
    // super.something is unlikely.
    // We only consider programmer-defined superclasses that are
    // global variables, or are defined on global variables.
    if (root.isName()) {
      String rootName = root.getString();
      Var var = t.getScope().getVar(rootName);
      if (var != null && (var.isLocal() || var.isExtern())) {
        // "require" not needed for these
      } else {
        usages.put(extendClass.getQualifiedName(), extendClass);
      }
    }
  }

  private void maybeAddProvidedName(Node n) {
    Node name = n.getFirstChild();
    if (name.isQualifiedName()) {
      providedNames.add(name.getQualifiedName());
    }
  }

  /**
   * If this returns true, check for @extends and @implements annotations on this node. Otherwise,
   * it's probably an alias for an existing class, so skip those annotations.
   *
   * @return Whether the given node declares a function. True for the following forms:
   *     <li>
   *         <pre>function foo() {}</pre>
   *     <li>
   *         <pre>var foo = function() {};</pre>
   *     <li>
   *         <pre>foo.bar = function() {};</pre>
   */
  private boolean declaresFunction(Node n) {
    if (n.isFunction()) {
      return true;
    }

    if (n.isAssign() && n.getLastChild().isFunction()) {
      return true;
    }

    if (NodeUtil.isNameDeclaration(n)
        && n.getFirstChild().hasChildren()
        && n.getFirstFirstChild().isFunction()) {
      return true;
    }

    return false;
  }

  private void maybeAddJsDocUsages(NodeTraversal t, Node n) {
    JSDocInfo info = n.getJSDocInfo();
    if (info == null) {
      return;
    }

    if (declaresFunction(n)) {
      for (JSTypeExpression expr : info.getImplementedInterfaces()) {
        maybeAddUsage(t, n, expr);
      }
      if (info.getBaseType() != null) {
        maybeAddUsage(t, n, info.getBaseType());
      }
      for (JSTypeExpression extendedInterface : info.getExtendedInterfaces()) {
        maybeAddUsage(t, n, extendedInterface);
      }
    }

    for (Node typeNode : info.getTypeNodes()) {
      maybeAddWeakUsage(t, n, typeNode);
    }
  }

  /**
   * Adds a weak usage for the given type expression (unless it references a variable that is
   * defined in the externs, in which case no goog.require() is needed). When a "weak usage" is
   * added, it means that a goog.require for that type is optional: No warning is given whether the
   * require is there or not.
   */
  private void maybeAddWeakUsage(NodeTraversal t, Node n, Node typeNode) {
    maybeAddUsage(t, n, typeNode, this.weakUsages, Predicates.<Node>alwaysTrue());
  }

  /**
   * Adds a usage for the given type expression (unless it references a variable that is defined in
   * the externs, in which case no goog.require() is needed). When a usage is added, it means that
   * there should be a goog.require for that type.
   */
  private void maybeAddUsage(NodeTraversal t, Node n, final JSTypeExpression expr) {
    // Just look at the root node, don't traverse.
    Predicate<Node> pred =
        new Predicate<Node>() {
          @Override
          public boolean apply(Node n) {
            return n == expr.getRoot();
          }
        };
    maybeAddUsage(t, n, expr.getRoot(), this.usages, pred);
  }

  private void maybeAddUsage(
      final NodeTraversal t,
      final Node n,
      Node rootTypeNode,
      final Map<String, Node> usagesMap,
      Predicate<Node> pred) {
    Visitor visitor =
        new Visitor() {
          @Override
          public void visit(Node typeNode) {
            if (typeNode.isString()) {
              String typeString = typeNode.getString();
              if (mode == Mode.SINGLE_FILE && !typeString.contains(".")) {
                // If using a single-name type, it's probably something like Error, which we
                // don't have externs for.
                weakUsages.put(typeString, n);
                return;
              }
              String rootName = Splitter.on('.').split(typeString).iterator().next();
              Var var = t.getScope().getVar(rootName);
              if (var == null || !var.isExtern()) {
                usagesMap.put(typeString, n);

                // Regardless of whether we're adding a weak or strong usage here, add weak usages
                // for the prefixes of the namespace, like we do for GETPROP nodes. Otherwise we get
                // an extra require warning for cases like:
                //
                //     goog.require('foo.bar.SomeService');
                //
                //     /** @constructor @extends {foo.bar.SomeService.Handler} */
                //     var MyHandler = function() {};
                Node getprop = NodeUtil.newQName(compiler, typeString);
                getprop.useSourceInfoIfMissingFromForTree(typeNode);
                visitQualifiedName(getprop);
              } else {
                // Even if the root namespace is in externs, add a weak usage because the full
                // namespace may still be goog.provided.
                weakUsages.put(typeString, n);
              }
            }
          }
        };

    NodeUtil.visitPreOrder(rootTypeNode, visitor, pred);
  }
}
/**
 * Checks for common errors, such as misplaced semicolons:
 *
 * <pre>
 * if (x); act_now();
 * </pre>
 *
 * or comparison against NaN:
 *
 * <pre>
 * if (x === NaN) act();
 * </pre>
 *
 * and generates warnings.
 *
 * @author [email protected] (John Lenz)
 */
final class CheckSuspiciousCode extends AbstractPostOrderCallback {

  static final DiagnosticType SUSPICIOUS_SEMICOLON =
      DiagnosticType.warning(
          "JSC_SUSPICIOUS_SEMICOLON",
          "If this if/for/while really shouldn''t have a body, use '{}'");

  static final DiagnosticType SUSPICIOUS_COMPARISON_WITH_NAN =
      DiagnosticType.warning(
          "JSC_SUSPICIOUS_NAN", "Comparison against NaN is always false. Did you mean isNaN()?");

  static final DiagnosticType SUSPICIOUS_IN_OPERATOR =
      DiagnosticType.warning(
          "JSC_SUSPICIOUS_IN",
          "Use of the \"in\" keyword on non-object types throws an exception.");

  static final DiagnosticType SUSPICIOUS_INSTANCEOF_LEFT_OPERAND =
      DiagnosticType.warning(
          "JSC_SUSPICIOUS_INSTANCEOF_LEFT",
          "\"instanceof\" with left non-object operand is always false.");

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    checkMissingSemicolon(t, n);
    checkNaN(t, n);
    checkInvalidIn(t, n);
    checkNonObjectInstanceOf(t, n);
  }

  private void checkMissingSemicolon(NodeTraversal t, Node n) {
    switch (n.getType()) {
      case Token.IF:
        Node trueCase = n.getSecondChild();
        reportIfWasEmpty(t, trueCase);
        Node elseCase = trueCase.getNext();
        if (elseCase != null) {
          reportIfWasEmpty(t, elseCase);
        }
        break;

      case Token.WHILE:
      case Token.FOR:
      case Token.FOR_OF:
        reportIfWasEmpty(t, NodeUtil.getLoopCodeBlock(n));
        break;
    }
  }

  private static void reportIfWasEmpty(NodeTraversal t, Node block) {
    Preconditions.checkState(block.isBlock());

    // A semicolon is distinguished from a block without children by
    // annotating it with EMPTY_BLOCK.  Blocks without children are
    // usually intentional, especially with loops.
    if (!block.hasChildren() && block.isAddedBlock()) {
      t.getCompiler().report(t.makeError(block, SUSPICIOUS_SEMICOLON));
    }
  }

  private void checkNaN(NodeTraversal t, Node n) {
    switch (n.getType()) {
      case Token.EQ:
      case Token.GE:
      case Token.GT:
      case Token.LE:
      case Token.LT:
      case Token.NE:
      case Token.SHEQ:
      case Token.SHNE:
        reportIfNaN(t, n.getFirstChild());
        reportIfNaN(t, n.getLastChild());
    }
  }

  private static void reportIfNaN(NodeTraversal t, Node n) {
    if (NodeUtil.isNaN(n)) {
      t.getCompiler().report(t.makeError(n.getParent(), SUSPICIOUS_COMPARISON_WITH_NAN));
    }
  }

  private void checkInvalidIn(NodeTraversal t, Node n) {
    if (n.getType() == Token.IN) {
      reportIfNonObject(t, n.getLastChild(), SUSPICIOUS_IN_OPERATOR);
    }
  }

  private void checkNonObjectInstanceOf(NodeTraversal t, Node n) {
    if (n.getType() == Token.INSTANCEOF) {
      reportIfNonObject(t, n.getFirstChild(), SUSPICIOUS_INSTANCEOF_LEFT_OPERAND);
    }
  }

  private static boolean reportIfNonObject(NodeTraversal t, Node n, DiagnosticType diagnosticType) {
    if (n.isAdd() || !NodeUtil.mayBeObect(n)) {
      t.report(n.getParent(), diagnosticType);
      return true;
    }
    return false;
  }
}
/**
 * DisambiguateProperties renames properties to disambiguate between unrelated fields with the same
 * name. Two properties are considered related if they share a definition on their prototype chains,
 * or if they are potentially referenced together via union types.
 *
 * <p>Renamimg only occurs if there are two or more distinct properties with the same name.
 *
 * <p>This pass allows other passes, such as inlining and code removal to take advantage of type
 * information implicitly.
 *
 * <pre>
 *   Foo.a;
 *   Bar.a;
 * </pre>
 *
 * <p>will become
 *
 * <pre>
 *   Foo.a$Foo;
 *   Bar.a$Bar;
 * </pre>
 */
class DisambiguateProperties<T> implements CompilerPass {
  private static final Logger logger = Logger.getLogger(DisambiguateProperties.class.getName());

  // TODO(user): add a flag to allow enabling of this once apps start
  // using it.
  static final DiagnosticType INVALIDATION =
      DiagnosticType.warning(
          "JSC_INVALIDATION",
          "Property disambiguator skipping all instances of property {0} "
              + "because of type {1} node {2}");
  private final boolean showInvalidationWarnings = false;

  private final AbstractCompiler compiler;
  private final TypeSystem<T> typeSystem;

  private class Property {
    /** The name of the property. */
    final String name;

    /** All types on which the field exists, grouped together if related. */
    private UnionFind<T> types;

    /**
     * A set of types for which renaming this field should be skipped. This list is first filled by
     * fields defined in the externs file.
     */
    Set<T> typesToSkip = Sets.newHashSet();

    /**
     * If true, do not rename any instance of this field, as it has been referenced from an unknown
     * type.
     */
    boolean skipRenaming;

    /** Set of nodes for this field that need renaming. */
    Set<Node> renameNodes = Sets.newHashSet();

    /**
     * Map from node to the highest type in the prototype chain containing the field for that node.
     * In the case of a union, the type is the highest type of one of the types in the union.
     */
    final Map<Node, T> rootTypes = Maps.newHashMap();

    Property(String name) {
      this.name = name;
    }

    /** Returns the types on which this field is referenced. */
    UnionFind<T> getTypes() {
      if (types == null) {
        types = new StandardUnionFind<T>();
      }
      return types;
    }

    /**
     * Record that this property is referenced from this type.
     *
     * @return true if the type was recorded for this property, else false, which would happen if
     *     the type was invalidating.
     */
    boolean addType(T type, T top, T relatedType) {
      checkState(!skipRenaming, "Attempt to record skipped property: %s", name);
      if (typeSystem.isInvalidatingType(top)) {
        invalidate();
        return false;
      } else {
        if (typeSystem.isTypeToSkip(top)) {
          addTypeToSkip(top);
        }

        if (relatedType == null) {
          getTypes().add(top);
        } else {
          getTypes().union(top, relatedType);
        }
        typeSystem.recordInterfaces(type, top, this);
        return true;
      }
    }

    /** Records the given type as one to skip for this property. */
    void addTypeToSkip(T type) {
      for (T skipType : typeSystem.getTypesToSkipForType(type)) {
        typesToSkip.add(skipType);
        getTypes().union(skipType, type);
      }
    }

    /** Invalidates any types related to invalid types. */
    void expandTypesToSkip() {
      // If we are not going to rename any properties, then we do not need to
      // update the list of invalid types, as they are all invalid.
      if (shouldRename()) {
        int count = 0;
        while (true) {
          // It should usually only take one time through this do-while.
          checkState(++count < 10, "Stuck in loop expanding types to skip.");

          // Make sure that the representative type for each type to skip is
          // marked as being skipped.
          Set<T> rootTypesToSkip = Sets.newHashSet();
          for (T subType : typesToSkip) {
            rootTypesToSkip.add(types.find(subType));
          }
          typesToSkip.addAll(rootTypesToSkip);

          Set<T> newTypesToSkip = Sets.newHashSet();
          Set<T> allTypes = types.elements();
          int originalTypesSize = allTypes.size();
          for (T subType : allTypes) {
            if (!typesToSkip.contains(subType) && typesToSkip.contains(types.find(subType))) {
              newTypesToSkip.add(subType);
            }
          }

          for (T newType : newTypesToSkip) {
            addTypeToSkip(newType);
          }

          // If there were not any new types added, we are done here.
          if (types.elements().size() == originalTypesSize) {
            break;
          }
        }
      }
    }

    /** Returns true if any instance of this property should be renamed. */
    boolean shouldRename() {
      return !skipRenaming && types != null && types.allEquivalenceClasses().size() > 1;
    }

    /**
     * Returns true if this property should be renamed on this type. expandTypesToSkip() should be
     * called before this, if anything has been added to the typesToSkip list.
     */
    boolean shouldRename(T type) {
      return !skipRenaming && !typesToSkip.contains(type);
    }

    /**
     * Invalidates a field from renaming. Used for field references on an object with unknown type.
     */
    boolean invalidate() {
      boolean changed = !skipRenaming;
      skipRenaming = true;
      types = null;
      return changed;
    }

    /**
     * Schedule the node to potentially be renamed.
     *
     * @param node the node to rename
     * @param type the highest type in the prototype chain for which the property is defined
     * @return True if type was accepted without invalidation or if the property was already
     *     invalidated. False if this property was invalidated this time.
     */
    boolean scheduleRenaming(Node node, T type) {
      if (!skipRenaming) {
        if (typeSystem.isInvalidatingType(type)) {
          invalidate();
          return false;
        }
        renameNodes.add(node);
        rootTypes.put(node, type);
      }
      return true;
    }
  }

  private Map<String, Property> properties = Maps.newHashMap();

  static DisambiguateProperties<JSType> forJSTypeSystem(AbstractCompiler compiler) {
    return new DisambiguateProperties<JSType>(compiler, new JSTypeSystem(compiler));
  }

  static DisambiguateProperties<ConcreteType> forConcreteTypeSystem(
      AbstractCompiler compiler, TightenTypes tt) {
    return new DisambiguateProperties<ConcreteType>(
        compiler, new ConcreteTypeSystem(tt, compiler.getCodingConvention()));
  }

  /**
   * This constructor should only be called by one of the helper functions above for either the
   * JSType system, or the concrete type system.
   */
  private DisambiguateProperties(AbstractCompiler compiler, TypeSystem<T> typeSystem) {
    this.compiler = compiler;
    this.typeSystem = typeSystem;
  }

  public void process(Node externs, Node root) {
    for (TypeMismatch mis : compiler.getTypeValidator().getMismatches()) {
      addInvalidatingType(mis.typeA);
      addInvalidatingType(mis.typeB);
    }

    StaticScope<T> scope = typeSystem.getRootScope();
    NodeTraversal.traverse(compiler, externs, new FindExternProperties());
    NodeTraversal.traverse(compiler, root, new FindRenameableProperties());
    renameProperties();
  }

  /** Invalidates the given type, so that no properties on it will be renamed. */
  private void addInvalidatingType(JSType type) {
    type = type.restrictByNotNullOrUndefined();
    if (type instanceof UnionType) {
      for (JSType alt : ((UnionType) type).getAlternates()) {
        addInvalidatingType(alt);
      }
      return;
    }

    typeSystem.addInvalidatingType(type);
    ObjectType objType = ObjectType.cast(type);
    if (objType != null && objType.getImplicitPrototype() != null) {
      typeSystem.addInvalidatingType(objType.getImplicitPrototype());
    }
  }

  /** Returns the property for the given name, creating it if necessary. */
  protected Property getProperty(String name) {
    if (!properties.containsKey(name)) {
      properties.put(name, new Property(name));
    }
    return properties.get(name);
  }

  /** Public for testing. */
  T getTypeWithProperty(String field, T type) {
    return typeSystem.getTypeWithProperty(field, type);
  }

  /** Tracks the current type system scope while traversing. */
  private abstract class AbstractScopingCallback implements ScopedCallback {
    protected final Stack<StaticScope<T>> scopes = new Stack<StaticScope<T>>();

    public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
      return true;
    }

    public void enterScope(NodeTraversal t) {
      if (t.inGlobalScope()) {
        scopes.push(typeSystem.getRootScope());
      } else {
        scopes.push(typeSystem.getFunctionScope(t.getScopeRoot()));
      }
    }

    public void exitScope(NodeTraversal t) {
      scopes.pop();
    }

    /** Returns the current scope at this point in the file. */
    protected StaticScope<T> getScope() {
      return scopes.peek();
    }
  }

  /**
   * Finds all properties defined in the externs file and sets them as ineligible for renaming from
   * the type on which they are defined.
   */
  private class FindExternProperties extends AbstractScopingCallback {
    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      // TODO(johnlenz): Support object-literal property definitions.
      if (n.getType() == Token.GETPROP) {
        String field = n.getLastChild().getString();
        T type = typeSystem.getType(getScope(), n.getFirstChild(), field);
        Property prop = getProperty(field);
        if (typeSystem.isInvalidatingType(type)) {
          prop.invalidate();
        } else {
          prop.addTypeToSkip(type);

          // If this is a prototype property, then we want to skip assignments
          // to the instance type as well.  These assignments are not usually
          // seen in the extern code itself, so we must handle them here.
          if ((type = typeSystem.getInstanceFromPrototype(type)) != null) {
            prop.getTypes().add(type);
            prop.typesToSkip.add(type);
          }
        }
      }
    }
  }

  /**
   * Traverses the tree, building a map from field names to Nodes for all fields that can be
   * renamed.
   */
  private class FindRenameableProperties extends AbstractScopingCallback {
    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (n.getType() == Token.GETPROP) {
        handleGetProp(t, n);
      } else if (n.getType() == Token.OBJECTLIT) {
        handleObjectLit(t, n);
      }
    }

    /** Processes a GETPROP node. */
    private void handleGetProp(NodeTraversal t, Node n) {
      String name = n.getLastChild().getString();
      T type = typeSystem.getType(getScope(), n.getFirstChild(), name);

      Property prop = getProperty(name);
      if (!prop.scheduleRenaming(n.getLastChild(), processProperty(t, prop, type, null))) {
        if (showInvalidationWarnings) {
          compiler.report(
              JSError.make(
                  t.getSourceName(),
                  n,
                  INVALIDATION,
                  name,
                  (type == null ? "null" : type.toString()),
                  n.toString()));
        }
      }
    }

    /** Processes a OBJECTLIT node. */
    private void handleObjectLit(NodeTraversal t, Node n) {
      Node child = n.getFirstChild();
      while (child != null) {
        // Maybe STRING, NUMBER, GET, SET
        if (child.getType() != Token.NUMBER) {
          // We should never see a mix of numbers and strings.
          String name = child.getString();
          T type = typeSystem.getType(getScope(), n, name);

          Property prop = getProperty(name);
          if (!prop.scheduleRenaming(child, processProperty(t, prop, type, null))) {
            if (showInvalidationWarnings) {
              compiler.report(
                  JSError.make(
                      t.getSourceName(),
                      child,
                      INVALIDATION,
                      name,
                      (type == null ? "null" : type.toString()),
                      n.toString()));
            }
          }
        }

        child = child.getNext();
      }
    }

    /**
     * Processes a property, adding it to the list of properties to rename.
     *
     * @return a representative type for the property reference, which will be the highest type on
     *     the prototype chain of the provided type. In the case of a union type, it will be the
     *     highest type on the prototype chain of one of the members of the union.
     */
    private T processProperty(NodeTraversal t, Property prop, T type, T relatedType) {
      type = typeSystem.restrictByNotNullOrUndefined(type);
      if (prop.skipRenaming || typeSystem.isInvalidatingType(type)) {
        return null;
      }

      Iterable<T> alternatives = typeSystem.getTypeAlternatives(type);
      if (alternatives != null) {
        T firstType = relatedType;
        for (T subType : alternatives) {
          T lastType = processProperty(t, prop, subType, firstType);
          if (lastType != null) {
            firstType = firstType == null ? lastType : firstType;
          }
        }
        return firstType;
      } else {
        T topType = typeSystem.getTypeWithProperty(prop.name, type);
        if (typeSystem.isInvalidatingType(topType)) {
          return null;
        }
        prop.addType(type, topType, relatedType);
        return topType;
      }
    }
  }

  /** Renames all properties with references on more than one type. */
  void renameProperties() {
    int propsRenamed = 0,
        propsSkipped = 0,
        instancesRenamed = 0,
        instancesSkipped = 0,
        singleTypeProps = 0;

    for (Property prop : properties.values()) {
      if (prop.shouldRename()) {
        Map<T, String> propNames = buildPropNames(prop.getTypes(), prop.name);

        ++propsRenamed;
        prop.expandTypesToSkip();
        UnionFind<T> types = prop.getTypes();
        for (Node node : prop.renameNodes) {
          T rootType = prop.rootTypes.get(node);
          if (prop.shouldRename(rootType)) {
            String newName = propNames.get(rootType);
            node.setString(newName);
            compiler.reportCodeChange();
            ++instancesRenamed;
          } else {
            ++instancesSkipped;
          }
        }
      } else {
        if (prop.skipRenaming) {
          ++propsSkipped;
        } else {
          ++singleTypeProps;
        }
      }
    }
    logger.info("Renamed " + instancesRenamed + " instances of " + propsRenamed + " properties.");
    logger.info(
        "Skipped renaming "
            + instancesSkipped
            + " invalidated "
            + "properties, "
            + propsSkipped
            + " instances of properties "
            + "that were skipped for specific types and "
            + singleTypeProps
            + " properties that were referenced from only one type.");
  }

  /**
   * Chooses a name to use for renaming in each equivalence class and maps each type in that class
   * to it.
   */
  private Map<T, String> buildPropNames(UnionFind<T> types, String name) {
    Map<T, String> names = Maps.newHashMap();
    for (Set<T> set : types.allEquivalenceClasses()) {
      checkState(!set.isEmpty());

      String typeName = null;
      for (T type : set) {
        if (typeName == null || type.toString().compareTo(typeName) < 0) {
          typeName = type.toString();
        }
      }

      String newName;
      if ("{...}".equals(typeName)) {
        newName = name;
      } else {
        newName = typeName.replaceAll("[^\\w$]", "_") + "$" + name;
      }

      for (T type : set) {
        names.put(type, newName);
      }
    }
    return names;
  }

  /** Returns a map from field name to types for which it will be renamed. */
  Multimap<String, Collection<T>> getRenamedTypesForTesting() {
    Multimap<String, Collection<T>> ret = HashMultimap.create();
    for (Map.Entry<String, Property> entry : properties.entrySet()) {
      Property prop = entry.getValue();
      if (!prop.skipRenaming) {
        for (Collection<T> c : prop.getTypes().allEquivalenceClasses()) {
          if (!c.isEmpty() && !prop.typesToSkip.contains(c.iterator().next())) {
            ret.put(entry.getKey(), c);
          }
        }
      }
    }
    return ret;
  }

  /** Interface for providing the type information needed by this pass. */
  private interface TypeSystem<T> {
    // TODO(user): add a getUniqueName(T type) method that is guaranteed
    // to be unique, performant and human-readable.

    /** Returns the top-most scope used by the type system (if any). */
    StaticScope<T> getRootScope();

    /** Returns the new scope started at the given function node. */
    StaticScope<T> getFunctionScope(Node node);

    /**
     * Returns the type of the given node.
     *
     * @param prop Only types with this property need to be returned. In general with type
     *     tightening, this will require no special processing, but in the case of an unknown
     *     JSType, we might need to add in the native types since we don't track them, but only if
     *     they have the given property.
     */
    T getType(StaticScope<T> scope, Node node, String prop);

    /**
     * Returns true if a field reference on this type will invalidiate all references to that field
     * as candidates for renaming. This is true if the type is unknown or all-inclusive, as
     * variables with such a type could be references to any object.
     */
    boolean isInvalidatingType(T type);

    /**
     * Informs the given type system that a type is invalidating due to a type mismatch found during
     * type checking.
     */
    void addInvalidatingType(JSType type);

    /**
     * Returns a set of types that should be skipped given the given type. This is necessary for
     * interfaces when using JSTypes, as all super interfaces must also be skipped.
     */
    ImmutableSet<T> getTypesToSkipForType(T type);

    /**
     * Determines whether the given type is one whose properties should not be considered for
     * renaming.
     */
    boolean isTypeToSkip(T type);

    /** Remove null and undefined from the options in the given type. */
    T restrictByNotNullOrUndefined(T type);

    /**
     * Returns the alternatives if this is a type that represents multiple types, and null if not.
     * Union and interface types can correspond to multiple other types.
     */
    Iterable<T> getTypeAlternatives(T type);

    /**
     * Returns the type in the chain from the given type that contains the given field or null if it
     * is not found anywhere.
     */
    T getTypeWithProperty(String field, T type);

    /**
     * Returns the type of the instance of which this is the prototype or null if this is not a
     * function prototype.
     */
    T getInstanceFromPrototype(T type);

    /**
     * Records that this property could be referenced from any interface that this type, or any type
     * in its superclass chain, implements.
     */
    void recordInterfaces(T type, T relatedType, DisambiguateProperties<T>.Property p);
  }

  /** Implementation of TypeSystem using JSTypes. */
  private static class JSTypeSystem implements TypeSystem<JSType> {
    private final Set<JSType> invalidatingTypes;
    private JSTypeRegistry registry;

    public JSTypeSystem(AbstractCompiler compiler) {
      registry = compiler.getTypeRegistry();
      invalidatingTypes =
          Sets.newHashSet(
              registry.getNativeType(JSTypeNative.ALL_TYPE),
              registry.getNativeType(JSTypeNative.NO_OBJECT_TYPE),
              registry.getNativeType(JSTypeNative.NO_TYPE),
              registry.getNativeType(JSTypeNative.FUNCTION_PROTOTYPE),
              registry.getNativeType(JSTypeNative.FUNCTION_INSTANCE_TYPE),
              registry.getNativeType(JSTypeNative.OBJECT_PROTOTYPE),
              registry.getNativeType(JSTypeNative.TOP_LEVEL_PROTOTYPE),
              registry.getNativeType(JSTypeNative.UNKNOWN_TYPE));
    }

    @Override
    public void addInvalidatingType(JSType type) {
      checkState(!type.isUnionType());
      invalidatingTypes.add(type);
    }

    @Override
    public StaticScope<JSType> getRootScope() {
      return null;
    }

    @Override
    public StaticScope<JSType> getFunctionScope(Node node) {
      return null;
    }

    @Override
    public JSType getType(StaticScope<JSType> scope, Node node, String prop) {
      if (node.getJSType() == null) {
        return registry.getNativeType(JSTypeNative.UNKNOWN_TYPE);
      }
      return node.getJSType();
    }

    @Override
    public boolean isInvalidatingType(JSType type) {
      if (type == null
          || invalidatingTypes.contains(type)
          || type.isUnknownType() /* unresolved types */) {
        return true;
      }

      ObjectType objType = ObjectType.cast(type);
      return objType != null && !objType.hasReferenceName();
    }

    @Override
    public ImmutableSet<JSType> getTypesToSkipForType(JSType type) {
      type = type.restrictByNotNullOrUndefined();
      if (type instanceof UnionType) {
        Set<JSType> types = Sets.newHashSet(type);
        for (JSType alt : ((UnionType) type).getAlternates()) {
          types.addAll(getTypesToSkipForTypeNonUnion(type));
        }
        return ImmutableSet.copyOf(types);
      }
      return ImmutableSet.copyOf(getTypesToSkipForTypeNonUnion(type));
    }

    private Set<JSType> getTypesToSkipForTypeNonUnion(JSType type) {
      Set<JSType> types = Sets.newHashSet();
      JSType skipType = type;
      while (skipType != null) {
        types.add(skipType);

        ObjectType objSkipType = skipType.toObjectType();
        if (objSkipType != null) {
          skipType = objSkipType.getImplicitPrototype();
        } else {
          break;
        }
      }
      return types;
    }

    @Override
    public boolean isTypeToSkip(JSType type) {
      return type.isEnumType() || (type.autoboxesTo() != null);
    }

    @Override
    public JSType restrictByNotNullOrUndefined(JSType type) {
      return type.restrictByNotNullOrUndefined();
    }

    @Override
    public Iterable<JSType> getTypeAlternatives(JSType type) {
      if (type.isUnionType()) {
        return ((UnionType) type).getAlternates();
      } else {
        ObjectType objType = type.toObjectType();
        if (objType != null
            && objType.getConstructor() != null
            && objType.getConstructor().isInterface()) {
          List<JSType> list = Lists.newArrayList();
          for (FunctionType impl : registry.getDirectImplementors(objType)) {
            list.add(impl.getInstanceType());
          }
          return list;
        } else {
          return null;
        }
      }
    }

    @Override
    public ObjectType getTypeWithProperty(String field, JSType type) {
      if (!(type instanceof ObjectType)) {
        if (type.autoboxesTo() != null) {
          type = type.autoboxesTo();
        } else {
          return null;
        }
      }

      // Ignore the prototype itself at all times.
      if ("prototype".equals(field)) {
        return null;
      }

      // We look up the prototype chain to find the highest place (if any) that
      // this appears.  This will make references to overriden properties look
      // like references to the initial property, so they are renamed alike.
      ObjectType foundType = null;
      ObjectType objType = ObjectType.cast(type);
      while (objType != null && objType.getImplicitPrototype() != objType) {
        if (objType.hasOwnProperty(field)) {
          foundType = objType;
        }
        objType = objType.getImplicitPrototype();
      }
      // If the property does not exist on the referenced type but the original
      // type is an object type, see if any subtype has the property.
      if (foundType == null) {
        ObjectType maybeType =
            ObjectType.cast(registry.getGreatestSubtypeWithProperty(type, field));
        // getGreatestSubtypeWithProperty does not guarantee that the property
        // is defined on the returned type, it just indicates that it might be,
        // so we have to double check.
        if (maybeType != null && maybeType.hasOwnProperty(field)) {
          foundType = maybeType;
        }
      }
      return foundType;
    }

    @Override
    public JSType getInstanceFromPrototype(JSType type) {
      if (type.isFunctionPrototypeType()) {
        FunctionPrototypeType prototype = (FunctionPrototypeType) type;
        FunctionType owner = prototype.getOwnerFunction();
        if (owner.isConstructor() || owner.isInterface()) {
          return ((FunctionPrototypeType) type).getOwnerFunction().getInstanceType();
        }
      }
      return null;
    }

    @Override
    public void recordInterfaces(
        JSType type, JSType relatedType, DisambiguateProperties<JSType>.Property p) {
      ObjectType objType = ObjectType.cast(type);
      if (objType != null) {
        FunctionType constructor;
        if (objType instanceof FunctionType) {
          constructor = (FunctionType) objType;
        } else if (objType instanceof FunctionPrototypeType) {
          constructor = ((FunctionPrototypeType) objType).getOwnerFunction();
        } else {
          constructor = objType.getConstructor();
        }
        while (constructor != null) {
          for (ObjectType itype : constructor.getImplementedInterfaces()) {
            JSType top = getTypeWithProperty(p.name, itype);
            if (top != null) {
              p.addType(itype, top, relatedType);
            } else {
              recordInterfaces(itype, relatedType, p);
            }

            // If this interface invalidated this property, return now.
            if (p.skipRenaming) return;
          }
          if (constructor.isInterface() || constructor.isConstructor()) {
            constructor = constructor.getSuperClassConstructor();
          } else {
            constructor = null;
          }
        }
      }
    }
  }

  /** Implementation of TypeSystem using concrete types. */
  private static class ConcreteTypeSystem implements TypeSystem<ConcreteType> {
    private final TightenTypes tt;
    private int nextUniqueId;
    private CodingConvention codingConvention;
    private final Set<JSType> invalidatingTypes = Sets.newHashSet();

    // An array of native types that are not tracked by type tightening, and
    // thus need to be added in if an unknown type is encountered.
    private static final JSTypeNative[] nativeTypes =
        new JSTypeNative[] {
          JSTypeNative.BOOLEAN_OBJECT_TYPE,
          JSTypeNative.NUMBER_OBJECT_TYPE,
          JSTypeNative.STRING_OBJECT_TYPE
        };

    public ConcreteTypeSystem(TightenTypes tt, CodingConvention convention) {
      this.tt = tt;
      this.codingConvention = convention;
    }

    @Override
    public void addInvalidatingType(JSType type) {
      checkState(!type.isUnionType());
      invalidatingTypes.add(type);
    }

    @Override
    public StaticScope<ConcreteType> getRootScope() {
      return tt.getTopScope();
    }

    @Override
    public StaticScope<ConcreteType> getFunctionScope(Node decl) {
      ConcreteFunctionType func = tt.getConcreteFunction(decl);
      return (func != null) ? func.getScope() : (StaticScope<ConcreteType>) null;
    }

    @Override
    public ConcreteType getType(StaticScope<ConcreteType> scope, Node node, String prop) {
      if (scope != null) {
        ConcreteType c = tt.inferConcreteType((TightenTypes.ConcreteScope) scope, node);
        return maybeAddAutoboxes(c, node, prop);
      } else {
        return null;
      }
    }

    /**
     * Add concrete types for autoboxing types if necessary. The concrete type system does not track
     * native types, like string, so add them if they are present in the JSType for the node.
     */
    private ConcreteType maybeAddAutoboxes(ConcreteType cType, Node node, String prop) {
      JSType jsType = node.getJSType();
      if (jsType == null) {
        return cType;
      } else if (jsType.isUnknownType()) {
        for (JSTypeNative nativeType : nativeTypes) {
          ConcreteType concrete =
              tt.getConcreteInstance(tt.getTypeRegistry().getNativeObjectType(nativeType));
          if (concrete != null && !concrete.getPropertyType(prop).isNone()) {
            cType = cType.unionWith(concrete);
          }
        }
        return cType;
      }

      return maybeAddAutoboxes(cType, jsType, prop);
    }

    private ConcreteType maybeAddAutoboxes(ConcreteType cType, JSType jsType, String prop) {
      jsType = jsType.restrictByNotNullOrUndefined();
      if (jsType instanceof UnionType) {
        for (JSType alt : ((UnionType) jsType).getAlternates()) {
          return maybeAddAutoboxes(cType, alt, prop);
        }
      }

      if (jsType.autoboxesTo() != null) {
        JSType autoboxed = jsType.autoboxesTo();
        return cType.unionWith(tt.getConcreteInstance((ObjectType) autoboxed));
      } else if (jsType.unboxesTo() != null) {
        return cType.unionWith(tt.getConcreteInstance((ObjectType) jsType));
      }

      return cType;
    }

    @Override
    public boolean isInvalidatingType(ConcreteType type) {
      // We will disallow types on functions so that 'prototype' is not renamed.
      // TODO(user): Support properties on functions as well.
      return (type == null)
          || type.isAll()
          || type.isFunction()
          || (type.isInstance() && invalidatingTypes.contains(type.toInstance().instanceType));
    }

    @Override
    public ImmutableSet<ConcreteType> getTypesToSkipForType(ConcreteType type) {
      return ImmutableSet.of(type);
    }

    @Override
    public boolean isTypeToSkip(ConcreteType type) {
      // Skip anonymous object literals and enum types.
      return type.isInstance()
          && !(type.toInstance().isFunctionPrototype()
              || type.toInstance().instanceType.isInstanceType());
    }

    @Override
    public ConcreteType restrictByNotNullOrUndefined(ConcreteType type) {
      // These are not represented in concrete types.
      return type;
    }

    @Override
    public Iterable<ConcreteType> getTypeAlternatives(ConcreteType type) {
      if (type.isUnion()) {
        return ((ConcreteUnionType) type).getAlternatives();
      } else {
        return null;
      }
    }

    @Override
    public ConcreteType getTypeWithProperty(String field, ConcreteType type) {
      if (type.isInstance()) {
        ConcreteInstanceType instanceType = (ConcreteInstanceType) type;
        return instanceType.getInstanceTypeWithProperty(field);
      } else if (type.isFunction()) {
        if ("prototype".equals(field) || codingConvention.isSuperClassReference(field)) {
          return type;
        }
      } else if (type.isNone()) {
        // If the receiver is none, then this code is never reached.  We will
        // return a new fake type to ensure that this access is renamed
        // differently from any other, so it can be easily removed.
        return new ConcreteUniqueType(++nextUniqueId);
      } else if (type.isUnion()) {
        // If only one has the property, return that.
        for (ConcreteType t : ((ConcreteUnionType) type).getAlternatives()) {
          ConcreteType ret = getTypeWithProperty(field, t);
          if (ret != null) {
            return ret;
          }
        }
      }
      return null;
    }

    @Override
    public ConcreteType getInstanceFromPrototype(ConcreteType type) {
      if (type.isInstance()) {
        ConcreteInstanceType instanceType = (ConcreteInstanceType) type;
        if (instanceType.isFunctionPrototype()) {
          return instanceType.getConstructorType().getInstanceType();
        }
      }
      return null;
    }

    @Override
    public void recordInterfaces(
        ConcreteType type,
        ConcreteType relatedType,
        DisambiguateProperties<ConcreteType>.Property p) {
      // No need to record interfaces when using concrete types.
    }
  }
}
/**
 * NodeTraversal allows an iteration through the nodes in the parse tree, and facilitates the
 * optimizations on the parse tree.
 */
public class NodeTraversal {
  private final AbstractCompiler compiler;
  private final Callback callback;

  /** Contains the current node */
  private Node curNode;

  public static final DiagnosticType NODE_TRAVERSAL_ERROR =
      DiagnosticType.error("JSC_NODE_TRAVERSAL_ERROR", "{0}");

  /**
   * Stack containing the Scopes that have been created. The Scope objects are lazily created; so
   * the {@code scopeRoots} stack contains the Nodes for all Scopes that have not been created yet.
   */
  private final Deque<Scope> scopes = new ArrayDeque<>();

  /**
   * A stack of scope roots. All scopes that have not been created are represented in this Deque.
   */
  private final Deque<Node> scopeRoots = new ArrayDeque<>();

  /**
   * A stack of scope roots that are valid cfg roots. All cfg roots that have not been created are
   * represented in this Deque.
   */
  private final Deque<Node> cfgRoots = new ArrayDeque<>();

  /**
   * Stack of control flow graphs (CFG). There is one CFG per scope. CFGs are lazily populated:
   * elements are {@code null} until requested by {@link #getControlFlowGraph()}. Note that {@link
   * ArrayDeque} does not allow {@code null} elements, so {@link LinkedList} is used instead.
   */
  Deque<ControlFlowGraph<Node>> cfgs = new LinkedList<>();

  /** The current source file name */
  private String sourceName;

  /** The current input */
  private InputId inputId;

  /** The scope creator */
  private final ScopeCreator scopeCreator;

  private final boolean useBlockScope;

  /** Possible callback for scope entry and exist * */
  private ScopedCallback scopeCallback;

  /** Callback for passes that iterate over a list of functions */
  public interface FunctionCallback {
    void visit(AbstractCompiler compiler, Node fnRoot);
  }

  /** Callback for tree-based traversals */
  public interface Callback {
    /**
     * Visits a node in pre order (before visiting its children) and decides whether this node's
     * children should be traversed. If children are traversed, they will be visited by {@link
     * #visit(NodeTraversal, Node, Node)} in postorder.
     *
     * <p>Implementations can have side effects (e.g. modifying the parse tree).
     *
     * @return whether the children of this node should be visited
     */
    boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent);

    /**
     * Visits a node in postorder (after its children have been visited). A node is visited only if
     * all its parents should be traversed ({@link #shouldTraverse(NodeTraversal, Node, Node)}).
     *
     * <p>Implementations can have side effects (e.g. modifying the parse tree).
     */
    void visit(NodeTraversal t, Node n, Node parent);
  }

  /** Callback that also knows about scope changes */
  public interface ScopedCallback extends Callback {

    /**
     * Called immediately after entering a new scope. The new scope can be accessed through
     * t.getScope()
     */
    void enterScope(NodeTraversal t);

    /**
     * Called immediately before exiting a scope. The ending scope can be accessed through
     * t.getScope()
     */
    void exitScope(NodeTraversal t);
  }

  /** Abstract callback to visit all nodes in postorder. */
  public abstract static class AbstractPostOrderCallback implements Callback {
    @Override
    public final boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
      return true;
    }
  }

  /** Abstract callback to visit all nodes in preorder. */
  public abstract static class AbstractPreOrderCallback implements Callback {
    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {}
  }

  /** Abstract scoped callback to visit all nodes in postorder. */
  public abstract static class AbstractScopedCallback implements ScopedCallback {
    @Override
    public final boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
      return true;
    }

    @Override
    public void enterScope(NodeTraversal t) {}

    @Override
    public void exitScope(NodeTraversal t) {}
  }

  /** Abstract callback to visit all nodes but not traverse into function bodies. */
  public abstract static class AbstractShallowCallback implements Callback {
    @Override
    public final boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
      // We do want to traverse the name of a named function, but we don't
      // want to traverse the arguments or body.
      return parent == null || !parent.isFunction() || n == parent.getFirstChild();
    }
  }

  /**
   * Abstract callback to visit all structure and statement nodes but doesn't traverse into
   * functions or expressions.
   */
  public abstract static class AbstractShallowStatementCallback implements Callback {
    @Override
    public final boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
      return parent == null
          || NodeUtil.isControlStructure(parent)
          || NodeUtil.isStatementBlock(parent);
    }
  }

  /** Abstract callback to visit a pruned set of nodes. */
  public abstract static class AbstractNodeTypePruningCallback implements Callback {
    private final Set<Integer> nodeTypes;
    private final boolean include;

    /**
     * Creates an abstract pruned callback.
     *
     * @param nodeTypes the nodes to include in the traversal
     */
    public AbstractNodeTypePruningCallback(Set<Integer> nodeTypes) {
      this(nodeTypes, true);
    }

    /**
     * Creates an abstract pruned callback.
     *
     * @param nodeTypes the nodes to include/exclude in the traversal
     * @param include whether to include or exclude the nodes in the traversal
     */
    public AbstractNodeTypePruningCallback(Set<Integer> nodeTypes, boolean include) {
      this.nodeTypes = nodeTypes;
      this.include = include;
    }

    @Override
    public boolean shouldTraverse(NodeTraversal nodeTraversal, Node n, Node parent) {
      return include == nodeTypes.contains(n.getType());
    }
  }

  /** Creates a node traversal using the specified callback interface. */
  public NodeTraversal(AbstractCompiler compiler, Callback cb) {
    this(
        compiler,
        cb,
        compiler.getLanguageMode().isEs6OrHigher()
            ? new Es6SyntacticScopeCreator(compiler)
            : SyntacticScopeCreator.makeUntyped(compiler));
  }

  /** Creates a node traversal using the specified callback interface and the scope creator. */
  public NodeTraversal(AbstractCompiler compiler, Callback cb, ScopeCreator scopeCreator) {
    this.callback = cb;
    if (cb instanceof ScopedCallback) {
      this.scopeCallback = (ScopedCallback) cb;
    }
    this.compiler = compiler;
    this.inputId = null;
    this.sourceName = "";
    this.scopeCreator = scopeCreator;
    this.useBlockScope = scopeCreator.hasBlockScope();
  }

  private void throwUnexpectedException(Exception unexpectedException) {
    // If there's an unexpected exception, try to get the
    // line number of the code that caused it.
    String message = unexpectedException.getMessage();

    // TODO(user): It is possible to get more information if curNode or
    // its parent is missing. We still have the scope stack in which it is still
    // very useful to find out at least which function caused the exception.
    if (inputId != null) {
      message =
          unexpectedException.getMessage()
              + "\n"
              + formatNodeContext("Node", curNode)
              + (curNode == null ? "" : formatNodeContext("Parent", curNode.getParent()));
    }
    compiler.throwInternalError(message, unexpectedException);
  }

  private String formatNodeContext(String label, Node n) {
    if (n == null) {
      return "  " + label + ": NULL";
    }
    return "  " + label + "(" + n.toString(false, false, false) + "): " + formatNodePosition(n);
  }

  /** Traverses a parse tree recursively. */
  public void traverse(Node root) {
    try {
      inputId = NodeUtil.getInputId(root);
      sourceName = "";
      curNode = root;
      pushScope(root);
      // null parent ensures that the shallow callbacks will traverse root
      traverseBranch(root, null);
      popScope();
    } catch (Exception unexpectedException) {
      throwUnexpectedException(unexpectedException);
    }
  }

  void traverseRoots(Node externs, Node root) {
    try {
      Node scopeRoot = externs.getParent();
      Preconditions.checkState(scopeRoot != null);

      inputId = NodeUtil.getInputId(scopeRoot);
      sourceName = "";
      curNode = scopeRoot;
      pushScope(scopeRoot);

      traverseBranch(externs, scopeRoot);
      Preconditions.checkState(root.getParent() == scopeRoot);
      traverseBranch(root, scopeRoot);

      popScope();
    } catch (Exception unexpectedException) {
      throwUnexpectedException(unexpectedException);
    }
  }

  private static final String MISSING_SOURCE = "[source unknown]";

  private String formatNodePosition(Node n) {
    String sourceFileName = getBestSourceFileName(n);
    if (sourceFileName == null) {
      return MISSING_SOURCE + "\n";
    }

    int lineNumber = n.getLineno();
    int columnNumber = n.getCharno();
    String src = compiler.getSourceLine(sourceFileName, lineNumber);
    if (src == null) {
      src = MISSING_SOURCE;
    }
    return sourceFileName + ":" + lineNumber + ":" + columnNumber + "\n" + src + "\n";
  }

  /**
   * Traverses a parse tree recursively with a scope, starting with the given root. This should only
   * be used in the global scope. Otherwise, use {@link #traverseAtScope}.
   */
  void traverseWithScope(Node root, Scope s) {
    Preconditions.checkState(s.isGlobal());
    try {
      inputId = null;
      sourceName = "";
      curNode = root;
      pushScope(s);
      traverseBranch(root, null);
      popScope();
    } catch (Exception unexpectedException) {
      throwUnexpectedException(unexpectedException);
    }
  }

  /** Traverses a parse tree recursively with a scope, starting at that scope's root. */
  void traverseAtScope(Scope s) {
    Node n = s.getRootNode();
    if (n.isFunction()) {
      // We need to do some extra magic to make sure that the scope doesn't
      // get re-created when we dive into the function.
      if (inputId == null) {
        inputId = NodeUtil.getInputId(n);
      }
      sourceName = getSourceName(n);
      curNode = n;
      pushScope(s);

      Node args = n.getFirstChild().getNext();
      Node body = args.getNext();
      traverseBranch(args, n);
      traverseBranch(body, n);

      popScope();
    } else if (n.isBlock()) {
      if (inputId == null) {
        inputId = NodeUtil.getInputId(n);
      }
      sourceName = getSourceName(n);
      curNode = n;
      pushScope(s);
      traverseBranch(n, n.getParent());

      popScope();
    } else {
      Preconditions.checkState(s.isGlobal(), "Expected global scope. Got:", s);
      traverseWithScope(n, s);
    }
  }

  /**
   * Traverse a function out-of-band of normal traversal.
   *
   * @param node The function node.
   * @param scope The scope the function is contained in. Does not fire enter/exit callback events
   *     for this scope.
   */
  public void traverseFunctionOutOfBand(Node node, Scope scope) {
    Preconditions.checkNotNull(scope);
    Preconditions.checkState(node.isFunction());
    Preconditions.checkState(scope.getRootNode() != null);
    if (inputId == null) {
      inputId = NodeUtil.getInputId(node);
    }
    curNode = node.getParent();
    pushScope(scope, true /* quietly */);
    traverseBranch(node, curNode);
    popScope(true /* quietly */);
  }

  /**
   * Traverses an inner node recursively with a refined scope. An inner node may be any node with a
   * non {@code null} parent (i.e. all nodes except the root).
   *
   * @param node the node to traverse
   * @param parent the node's parent, it may not be {@code null}
   * @param refinedScope the refined scope of the scope currently at the top of the scope stack or
   *     in trivial cases that very scope or {@code null}
   */
  protected void traverseInnerNode(Node node, Node parent, Scope refinedScope) {
    Preconditions.checkNotNull(parent);
    if (inputId == null) {
      inputId = NodeUtil.getInputId(node);
    }
    if (refinedScope != null && getScope() != refinedScope) {
      curNode = node;
      pushScope(refinedScope);
      traverseBranch(node, parent);
      popScope();
    } else {
      traverseBranch(node, parent);
    }
  }

  public AbstractCompiler getCompiler() {
    return compiler;
  }

  /**
   * Gets the current line number, or zero if it cannot be determined. The line number is retrieved
   * lazily as a running time optimization.
   */
  public int getLineNumber() {
    Node cur = curNode;
    while (cur != null) {
      int line = cur.getLineno();
      if (line >= 0) {
        return line;
      }
      cur = cur.getParent();
    }
    return 0;
  }

  /**
   * Gets the current char number, or zero if it cannot be determined. The line number is retrieved
   * lazily as a running time optimization.
   */
  public int getCharno() {
    Node cur = curNode;
    while (cur != null) {
      int line = cur.getCharno();
      if (line >= 0) {
        return line;
      }
      cur = cur.getParent();
    }
    return 0;
  }

  /**
   * Gets the current input source name.
   *
   * @return A string that may be empty, but not null
   */
  public String getSourceName() {
    return sourceName;
  }

  /** Gets the current input source. */
  public CompilerInput getInput() {
    return compiler.getInput(inputId);
  }

  /** Gets the current input module. */
  public JSModule getModule() {
    CompilerInput input = getInput();
    return input == null ? null : input.getModule();
  }

  /** Returns the node currently being traversed. */
  public Node getCurrentNode() {
    return curNode;
  }

  /**
   * Traversal for passes that work only on changed functions. Suppose a loopable pass P1 uses this
   * traversal. Then, if a function doesn't change between two runs of P1, it won't look at the
   * function the second time. (We're assuming that P1 runs to a fixpoint, o/w we may miss
   * optimizations.)
   *
   * <p>Most changes are reported with calls to Compiler.reportCodeChange(), which doesn't know
   * which scope changed. We keep track of the current scope by calling Compiler.setScope inside
   * pushScope and popScope. The automatic tracking can be wrong in rare cases when a pass changes
   * scope w/out causing a call to pushScope or popScope. It's very hard to find the places where
   * this happens unless a bug is triggered. Passes that do cross-scope modifications call
   * Compiler.reportChangeToEnclosingScope(Node n).
   */
  public static void traverseChangedFunctions(
      AbstractCompiler compiler, FunctionCallback callback) {
    final AbstractCompiler comp = compiler;
    final FunctionCallback cb = callback;
    final Node jsRoot = comp.getJsRoot();
    NodeTraversal t =
        new NodeTraversal(
            comp,
            new AbstractPreOrderCallback() {
              @Override
              public final boolean shouldTraverse(NodeTraversal t, Node n, Node p) {
                if ((n == jsRoot || n.isFunction()) && comp.hasScopeChanged(n)) {
                  cb.visit(comp, n);
                }
                return true;
              }
            });
    t.traverse(jsRoot);
  }

  /** Traverses a node recursively. */
  public static void traverse(AbstractCompiler compiler, Node root, Callback cb) {
    NodeTraversal t = new NodeTraversal(compiler, cb);
    t.traverse(root);
  }

  public static void traverseTyped(AbstractCompiler compiler, Node root, Callback cb) {
    NodeTraversal t = new NodeTraversal(compiler, cb, SyntacticScopeCreator.makeTyped(compiler));
    t.traverse(root);
  }

  public static void traverseRoots(
      AbstractCompiler compiler, Callback cb, Node externs, Node root) {
    NodeTraversal t = new NodeTraversal(compiler, cb);
    t.traverseRoots(externs, root);
  }

  static void traverseRootsTyped(AbstractCompiler compiler, Callback cb, Node externs, Node root) {
    NodeTraversal t = new NodeTraversal(compiler, cb, SyntacticScopeCreator.makeTyped(compiler));
    t.traverseRoots(externs, root);
  }

  /** Traverses a branch. */
  private void traverseBranch(Node n, Node parent) {
    int type = n.getType();
    if (type == Token.SCRIPT) {
      inputId = n.getInputId();
      sourceName = getSourceName(n);
    }

    curNode = n;
    if (!callback.shouldTraverse(this, n, parent)) {
      return;
    }

    if (type == Token.FUNCTION) {
      traverseFunction(n, parent);
    } else if (useBlockScope && NodeUtil.createsBlockScope(n)) {
      traverseBlockScope(n);
    } else {
      for (Node child = n.getFirstChild(); child != null; ) {
        // child could be replaced, in which case our child node
        // would no longer point to the true next
        Node next = child.getNext();
        traverseBranch(child, n);
        child = next;
      }
    }

    curNode = n;
    callback.visit(this, n, parent);
  }

  /** Traverses a function. */
  private void traverseFunction(Node n, Node parent) {
    Preconditions.checkState(n.getChildCount() == 3);
    Preconditions.checkState(n.isFunction());

    final Node fnName = n.getFirstChild();
    boolean isFunctionExpression = (parent != null) && NodeUtil.isFunctionExpression(n);

    if (!isFunctionExpression) {
      // Functions declarations are in the scope containing the declaration.
      traverseBranch(fnName, n);
    }

    curNode = n;
    pushScope(n);

    if (isFunctionExpression) {
      // Function expression names are only accessible within the function
      // scope.
      traverseBranch(fnName, n);
    }

    final Node args = fnName.getNext();
    final Node body = args.getNext();

    // Args
    traverseBranch(args, n);

    // Body
    // ES6 "arrow" function may not have a block as a body.
    traverseBranch(body, n);

    popScope();
  }

  /** Traverses a non-function block. */
  private void traverseBlockScope(Node n) {
    pushScope(n);
    for (Node child : n.children()) {
      traverseBranch(child, n);
    }
    popScope();
  }

  /** Examines the functions stack for the last instance of a function node. */
  public Node getEnclosingFunction() {
    Node root = getCfgRoot();
    return root.isFunction() ? root : null;
  }

  /** Creates a new scope (e.g. when entering a function). */
  private void pushScope(Node node) {
    Preconditions.checkState(curNode != null);
    compiler.setScope(node);
    scopeRoots.push(node);
    if (NodeUtil.isValidCfgRoot(node)) {
      cfgRoots.push(node);
      cfgs.push(null);
    }
    if (scopeCallback != null) {
      scopeCallback.enterScope(this);
    }
  }

  /** Creates a new scope (e.g. when entering a function). */
  private void pushScope(Scope s) {
    pushScope(s, false);
  }

  /**
   * Creates a new scope (e.g. when entering a function).
   *
   * @param quietly Don't fire an enterScope callback.
   */
  private void pushScope(Scope s, boolean quietly) {
    Preconditions.checkState(curNode != null);
    compiler.setScope(s.getRootNode());
    scopes.push(s);
    if (NodeUtil.isValidCfgRoot(s.getRootNode())) {
      cfgs.push(null);
    }
    if (!quietly && scopeCallback != null) {
      scopeCallback.enterScope(this);
    }
  }

  private void popScope() {
    popScope(false);
  }

  /**
   * Pops back to the previous scope (e.g. when leaving a function).
   *
   * @param quietly Don't fire the exitScope callback.
   */
  private void popScope(boolean quietly) {
    if (!quietly && scopeCallback != null) {
      scopeCallback.exitScope(this);
    }
    Node scopeRoot;
    if (scopeRoots.isEmpty()) {
      scopeRoot = scopes.pop().getRootNode();
    } else {
      scopeRoot = scopeRoots.pop();
    }
    if (NodeUtil.isValidCfgRoot(scopeRoot)) {
      cfgs.pop();
      if (!cfgRoots.isEmpty()) {
        Preconditions.checkState(cfgRoots.pop() == scopeRoot);
      }
    }
    if (hasScope()) {
      compiler.setScope(getScopeRoot());
    }
  }

  /** Gets the current scope. */
  public Scope getScope() {
    Scope scope = scopes.isEmpty() ? null : scopes.peek();
    if (scopeRoots.isEmpty()) {
      return scope;
    }

    Iterator<Node> it = scopeRoots.descendingIterator();
    while (it.hasNext()) {
      scope = scopeCreator.createScope(it.next(), scope);
      scopes.push(scope);
    }
    scopeRoots.clear();
    cfgRoots.clear();
    // No need to call compiler.setScope; the top scopeRoot is now the top scope
    return scope;
  }

  public TypedScope getTypedScope() {
    Scope s = getScope();
    Preconditions.checkState(s instanceof TypedScope, "getTypedScope called for untyped traversal");
    return (TypedScope) s;
  }

  /** Gets the control flow graph for the current JS scope. */
  public ControlFlowGraph<Node> getControlFlowGraph() {
    if (cfgs.peek() == null) {
      ControlFlowAnalysis cfa = new ControlFlowAnalysis(compiler, false, true);
      cfa.process(null, getCfgRoot());
      cfgs.pop();
      cfgs.push(cfa.getCfg());
    }
    return cfgs.peek();
  }

  /** Returns the current scope's root. */
  public Node getScopeRoot() {
    if (scopeRoots.isEmpty()) {
      return scopes.peek().getRootNode();
    } else {
      return scopeRoots.peek();
    }
  }

  private Node getCfgRoot() {
    if (cfgRoots.isEmpty()) {
      Scope currScope = scopes.peek();
      while (currScope.isBlockScope()) {
        currScope = currScope.getParent();
      }
      return currScope.getRootNode();
    } else {
      return cfgRoots.peek();
    }
  }

  /** Determines whether the traversal is currently in the global scope. */
  boolean inGlobalScope() {
    return getScopeDepth() <= 1;
  }

  // Not dual of inGlobalScope, because of block scoping.
  // They both return false in an inner block at top level.
  boolean inFunction() {
    return getCfgRoot().isFunction();
  }

  int getScopeDepth() {
    return scopes.size() + scopeRoots.size();
  }

  public boolean hasScope() {
    return !(scopes.isEmpty() && scopeRoots.isEmpty());
  }

  /** Reports a diagnostic (error or warning) */
  public void report(Node n, DiagnosticType diagnosticType, String... arguments) {
    JSError error = JSError.make(n, diagnosticType, arguments);
    compiler.report(error);
  }

  private static String getSourceName(Node n) {
    String name = n.getSourceFileName();
    return name == null ? "" : name;
  }

  InputId getInputId() {
    return inputId;
  }

  /**
   * Creates a JSError during NodeTraversal.
   *
   * @param n Determines the line and char position within the source file name
   * @param type The DiagnosticType
   * @param arguments Arguments to be incorporated into the message
   */
  public JSError makeError(Node n, CheckLevel level, DiagnosticType type, String... arguments) {
    return JSError.make(n, level, type, arguments);
  }

  /**
   * Creates a JSError during NodeTraversal.
   *
   * @param n Determines the line and char position within the source file name
   * @param type The DiagnosticType
   * @param arguments Arguments to be incorporated into the message
   */
  public JSError makeError(Node n, DiagnosticType type, String... arguments) {
    return JSError.make(n, type, arguments);
  }

  private String getBestSourceFileName(Node n) {
    return n == null ? sourceName : n.getSourceFileName();
  }
}
/** A compiler pass to run the type inference analysis. */
class TypeInferencePass implements CompilerPass {

  static final DiagnosticType DATAFLOW_ERROR =
      DiagnosticType.warning("JSC_INTERNAL_ERROR_DATAFLOW", "non-monotonic data-flow analysis");

  private final AbstractCompiler compiler;
  private final ReverseAbstractInterpreter reverseInterpreter;
  private final Scope topScope;
  private final MemoizedScopeCreator scopeCreator;
  private final Map<String, AssertionFunctionSpec> assertionFunctionsMap;

  TypeInferencePass(
      AbstractCompiler compiler,
      ReverseAbstractInterpreter reverseInterpreter,
      Scope topScope,
      MemoizedScopeCreator scopeCreator) {
    this.compiler = compiler;
    this.reverseInterpreter = reverseInterpreter;
    this.topScope = topScope;
    this.scopeCreator = scopeCreator;

    assertionFunctionsMap = Maps.newHashMap();
    for (AssertionFunctionSpec assertionFunction :
        compiler.getCodingConvention().getAssertionFunctions()) {
      assertionFunctionsMap.put(assertionFunction.getFunctionName(), assertionFunction);
    }
  }

  /**
   * Main entry point for type inference when running over the whole tree.
   *
   * @param externsRoot The root of the externs parse tree.
   * @param jsRoot The root of the input parse tree to be checked.
   */
  @Override
  public void process(Node externsRoot, Node jsRoot) {
    Node externsAndJs = jsRoot.getParent();
    Preconditions.checkState(externsAndJs != null);
    Preconditions.checkState(externsRoot == null || externsAndJs.hasChild(externsRoot));

    inferAllScopes(externsAndJs);
  }

  /** Entry point for type inference when running over part of the tree. */
  void inferAllScopes(Node node) {
    // Type analysis happens in two major phases.
    // 1) Finding all the symbols.
    // 2) Propagating all the inferred types.
    //
    // The order of this analysis is non-obvious. In a complete inference
    // system, we may need to backtrack arbitrarily far. But the compile-time
    // costs would be unacceptable.
    //
    // We do one pass where we do typed scope creation for all scopes
    // in pre-order.
    //
    // Then we do a second pass where we do all type inference
    // (type propagation) in pre-order.
    //
    // We use a memoized scope creator so that we never create a scope
    // more than once.
    //
    // This will allow us to handle cases like:
    // var ns = {};
    // (function() { /** JSDoc */ ns.method = function() {}; })();
    // ns.method();
    // In this code, we need to build the symbol table for the inner scope in
    // order to propagate the type of ns.method in the outer scope.
    (new NodeTraversal(compiler, new FirstScopeBuildingCallback(), scopeCreator))
        .traverseWithScope(node, topScope);

    for (Scope s : scopeCreator.getAllMemoizedScopes()) {
      s.resolveTypes();
    }

    (new NodeTraversal(compiler, new SecondScopeBuildingCallback(), scopeCreator))
        .traverseWithScope(node, topScope);
  }

  void inferScope(Node n, Scope scope) {
    TypeInference typeInference =
        new TypeInference(
            compiler, computeCfg(n), reverseInterpreter, scope, assertionFunctionsMap);
    try {
      typeInference.analyze();

      // Resolve any new type names found during the inference.
      compiler.getTypeRegistry().resolveTypesInScope(scope);

    } catch (DataFlowAnalysis.MaxIterationsExceededException e) {
      compiler.report(JSError.make(n, DATAFLOW_ERROR));
    }
  }

  private static class FirstScopeBuildingCallback extends AbstractScopedCallback {
    @Override
    public void enterScope(NodeTraversal t) {
      t.getScope();
    }

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      // Do nothing
    }
  }

  private class SecondScopeBuildingCallback extends AbstractScopedCallback {
    @Override
    public void enterScope(NodeTraversal t) {
      // Only infer the entry root, rather than the scope root.
      // This ensures that incremental compilation only touches the root
      // that's been swapped out.
      inferScope(t.getCurrentNode(), t.getScope());
    }

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      // Do nothing
    }
  }

  private ControlFlowGraph<Node> computeCfg(Node n) {
    ControlFlowAnalysis cfa = new ControlFlowAnalysis(compiler, false, false);
    cfa.process(null, n);
    return cfa.getCfg();
  }
}
/**
 * Named groups of DiagnosticTypes exposed by Compiler.
 *
 * @author [email protected] (Nick Santos)
 */
public class DiagnosticGroups {
  static final DiagnosticType UNUSED = DiagnosticType.warning("JSC_UNUSED", "{0}");

  public static final Set<String> wildcardExcludedGroups = ImmutableSet.of("reportUnknownTypes");

  public DiagnosticGroups() {}

  private static final Map<String, DiagnosticGroup> groupsByName = new HashMap<>();

  static DiagnosticGroup registerDeprecatedGroup(String name) {
    return registerGroup(name, new DiagnosticGroup(name, UNUSED));
  }

  static DiagnosticGroup registerGroup(String name, DiagnosticGroup group) {
    groupsByName.put(name, group);
    return group;
  }

  static DiagnosticGroup registerGroup(String name, DiagnosticType... types) {
    DiagnosticGroup group = new DiagnosticGroup(name, types);
    groupsByName.put(name, group);
    return group;
  }

  static DiagnosticGroup registerGroup(String name, DiagnosticGroup... groups) {
    DiagnosticGroup group = new DiagnosticGroup(name, groups);
    groupsByName.put(name, group);
    return group;
  }

  /** Get the registered diagnostic groups, indexed by name. */
  protected Map<String, DiagnosticGroup> getRegisteredGroups() {
    return ImmutableMap.copyOf(groupsByName);
  }

  /** Find the diagnostic group registered under the given name. */
  public DiagnosticGroup forName(String name) {
    return groupsByName.get(name);
  }

  // A bit of a hack to display the available groups on the command-line.
  // New groups should be added to this list if they are public and should
  // be listed on the command-line as an available option.
  //
  // If a group is suppressible on a per-file basis, it should be added
  // to parser/ParserConfig.properties
  static final String DIAGNOSTIC_GROUP_NAMES =
      "accessControls, ambiguousFunctionDecl, checkEventfulObjectDisposal, "
          + "checkRegExp, checkTypes, checkVars, "
          + "conformanceViolations, const, constantProperty, deprecated, "
          + "deprecatedAnnotations, duplicateMessage, es3, "
          + "es5Strict, externsValidation, fileoverviewTags, globalThis, "
          + "inferredConstCheck, internetExplorerChecks, invalidCasts, "
          + "misplacedTypeAnnotation, missingGetCssName, missingProperties, "
          + "missingProvide, missingRequire, missingReturn, msgDescriptions"
          + "newCheckTypes, nonStandardJsDocs, reportUnknownTypes, suspiciousCode, "
          + "strictModuleDepCheck, typeInvalidation, "
          + "undefinedNames, undefinedVars, unknownDefines, unnecessaryCasts, uselessCode, "
          + "useOfGoogBase, visibility";

  public static final DiagnosticGroup GLOBAL_THIS =
      DiagnosticGroups.registerGroup("globalThis", CheckGlobalThis.GLOBAL_THIS);

  public static final DiagnosticGroup DEPRECATED =
      DiagnosticGroups.registerGroup(
          "deprecated",
          CheckAccessControls.DEPRECATED_NAME,
          CheckAccessControls.DEPRECATED_NAME_REASON,
          CheckAccessControls.DEPRECATED_PROP,
          CheckAccessControls.DEPRECATED_PROP_REASON,
          CheckAccessControls.DEPRECATED_CLASS,
          CheckAccessControls.DEPRECATED_CLASS_REASON);

  public static final DiagnosticGroup VISIBILITY =
      DiagnosticGroups.registerGroup(
          "visibility",
          CheckAccessControls.BAD_PRIVATE_GLOBAL_ACCESS,
          CheckAccessControls.BAD_PRIVATE_PROPERTY_ACCESS,
          CheckAccessControls.BAD_PACKAGE_PROPERTY_ACCESS,
          CheckAccessControls.BAD_PROTECTED_PROPERTY_ACCESS,
          CheckAccessControls.EXTEND_FINAL_CLASS,
          CheckAccessControls.PRIVATE_OVERRIDE,
          CheckAccessControls.VISIBILITY_MISMATCH,
          CheckAccessControls.CONVENTION_MISMATCH);

  public static final DiagnosticGroup ACCESS_CONTROLS =
      DiagnosticGroups.registerGroup("accessControls", DEPRECATED, VISIBILITY);

  public static final DiagnosticGroup NON_STANDARD_JSDOC =
      DiagnosticGroups.registerGroup(
          "nonStandardJsDocs",
          RhinoErrorReporter.BAD_JSDOC_ANNOTATION,
          RhinoErrorReporter.INVALID_PARAM,
          RhinoErrorReporter.JSDOC_IN_BLOCK_COMMENT);

  public static final DiagnosticGroup INVALID_CASTS =
      DiagnosticGroups.registerGroup("invalidCasts", TypeValidator.INVALID_CAST);

  public static final DiagnosticGroup UNNECESSARY_CASTS =
      DiagnosticGroups.registerGroup("unnecessaryCasts", TypeValidator.UNNECESSARY_CAST);

  public static final DiagnosticGroup INFERRED_CONST_CHECKS =
      DiagnosticGroups.registerGroup(
          "inferredConstCheck", TypedScopeCreator.CANNOT_INFER_CONST_TYPE);

  public static final DiagnosticGroup FILEOVERVIEW_JSDOC =
      DiagnosticGroups.registerDeprecatedGroup("fileoverviewTags");

  public static final DiagnosticGroup STRICT_MODULE_DEP_CHECK =
      DiagnosticGroups.registerGroup(
          "strictModuleDepCheck",
          VarCheck.STRICT_MODULE_DEP_ERROR,
          CheckGlobalNames.STRICT_MODULE_DEP_QNAME);

  public static final DiagnosticGroup VIOLATED_MODULE_DEP =
      DiagnosticGroups.registerGroup("violatedModuleDep", VarCheck.VIOLATED_MODULE_DEP_ERROR);

  public static final DiagnosticGroup EXTERNS_VALIDATION =
      DiagnosticGroups.registerGroup(
          "externsValidation",
          VarCheck.NAME_REFERENCE_IN_EXTERNS_ERROR,
          VarCheck.UNDEFINED_EXTERN_VAR_ERROR);

  public static final DiagnosticGroup AMBIGUOUS_FUNCTION_DECL =
      DiagnosticGroups.registerGroup(
          "ambiguousFunctionDecl",
          VariableReferenceCheck.AMBIGUOUS_FUNCTION_DECL,
          StrictModeCheck.BAD_FUNCTION_DECLARATION);

  public static final DiagnosticGroup UNKNOWN_DEFINES =
      DiagnosticGroups.registerGroup("unknownDefines", ProcessDefines.UNKNOWN_DEFINE_WARNING);

  public static final DiagnosticGroup TWEAKS =
      DiagnosticGroups.registerGroup(
          "tweakValidation",
          ProcessTweaks.INVALID_TWEAK_DEFAULT_VALUE_WARNING,
          ProcessTweaks.TWEAK_WRONG_GETTER_TYPE_WARNING,
          ProcessTweaks.UNKNOWN_TWEAK_WARNING);

  public static final DiagnosticGroup MISSING_PROPERTIES =
      DiagnosticGroups.registerGroup(
          "missingProperties",
          TypeCheck.INEXISTENT_PROPERTY,
          TypeCheck.INEXISTENT_PROPERTY_WITH_SUGGESTION,
          TypeCheck.POSSIBLE_INEXISTENT_PROPERTY);

  public static final DiagnosticGroup MISSING_RETURN =
      DiagnosticGroups.registerGroup("missingReturn", CheckMissingReturn.MISSING_RETURN_STATEMENT);

  public static final DiagnosticGroup INTERNET_EXPLORER_CHECKS =
      DiagnosticGroups.registerGroup("internetExplorerChecks", RhinoErrorReporter.TRAILING_COMMA);

  public static final DiagnosticGroup UNDEFINED_VARIABLES =
      DiagnosticGroups.registerGroup("undefinedVars", VarCheck.UNDEFINED_VAR_ERROR);

  public static final DiagnosticGroup UNDEFINED_NAMES =
      DiagnosticGroups.registerGroup("undefinedNames", CheckGlobalNames.UNDEFINED_NAME_WARNING);

  public static final DiagnosticGroup DEBUGGER_STATEMENT_PRESENT =
      DiagnosticGroups.registerGroup(
          "checkDebuggerStatement", CheckDebuggerStatement.DEBUGGER_STATEMENT_PRESENT);

  public static final DiagnosticGroup CHECK_REGEXP =
      DiagnosticGroups.registerGroup(
          "checkRegExp", CheckRegExp.REGEXP_REFERENCE, CheckRegExp.MALFORMED_REGEXP);

  public static final DiagnosticGroup CHECK_TYPES =
      DiagnosticGroups.registerGroup(
          "checkTypes", TypeValidator.ALL_DIAGNOSTICS, TypeCheck.ALL_DIAGNOSTICS);

  // Part of the new type inference (under development)
  public static final DiagnosticGroup NEW_CHECK_TYPES =
      DiagnosticGroups.registerGroup(
          "newCheckTypes", GlobalTypeInfo.ALL_DIAGNOSTICS, NewTypeInference.ALL_DIAGNOSTICS);

  public static final DiagnosticGroup NEW_CHECK_TYPES_ALL_CHECKS =
      DiagnosticGroups.registerGroup(
          "newCheckTypesAllChecks",
          JSTypeCreatorFromJSDoc.CONFLICTING_SHAPE_TYPE,
          NewTypeInference.NULLABLE_DEREFERENCE);

  static {
    // Warnings that are absent in closure library
    DiagnosticGroups.registerGroup(
        "newCheckTypesClosureClean",
        //           JSTypeCreatorFromJSDoc.BAD_JSDOC_ANNOTATION,
        JSTypeCreatorFromJSDoc.CONFLICTING_EXTENDED_TYPE,
        JSTypeCreatorFromJSDoc.CONFLICTING_IMPLEMENTED_TYPE,
        JSTypeCreatorFromJSDoc.DICT_IMPLEMENTS_INTERF,
        JSTypeCreatorFromJSDoc.EXTENDS_NON_OBJECT,
        JSTypeCreatorFromJSDoc.EXTENDS_NOT_ON_CTOR_OR_INTERF,
        JSTypeCreatorFromJSDoc.IMPLEMENTS_WITHOUT_CONSTRUCTOR,
        JSTypeCreatorFromJSDoc.INHERITANCE_CYCLE,
        //          JSTypeCreatorFromJSDoc.UNION_IS_UNINHABITABLE,
        GlobalTypeInfo.ANONYMOUS_NOMINAL_TYPE,
        GlobalTypeInfo.CANNOT_INIT_TYPEDEF,
        GlobalTypeInfo.CANNOT_OVERRIDE_FINAL_METHOD,
        GlobalTypeInfo.CONST_WITHOUT_INITIALIZER,
        //           GlobalTypeInfo.COULD_NOT_INFER_CONST_TYPE,
        GlobalTypeInfo.CTOR_IN_DIFFERENT_SCOPE,
        GlobalTypeInfo.DUPLICATE_JSDOC,
        GlobalTypeInfo.DUPLICATE_PROP_IN_ENUM,
        GlobalTypeInfo.EXPECTED_CONSTRUCTOR,
        GlobalTypeInfo.EXPECTED_INTERFACE,
        GlobalTypeInfo.INEXISTENT_PARAM,
        //           GlobalTypeInfo.INVALID_PROP_OVERRIDE,
        GlobalTypeInfo.LENDS_ON_BAD_TYPE,
        GlobalTypeInfo.MALFORMED_ENUM,
        GlobalTypeInfo.MISPLACED_CONST_ANNOTATION,
        //           GlobalTypeInfo.REDECLARED_PROPERTY,
        GlobalTypeInfo.STRUCTDICT_WITHOUT_CTOR,
        GlobalTypeInfo.UNDECLARED_NAMESPACE,
        //           GlobalTypeInfo.UNRECOGNIZED_TYPE_NAME,
        TypeCheck.CONFLICTING_EXTENDED_TYPE,
        TypeCheck.ENUM_NOT_CONSTANT,
        TypeCheck.INCOMPATIBLE_EXTENDED_PROPERTY_TYPE,
        TypeCheck.MULTIPLE_VAR_DEF,
        TypeCheck.UNKNOWN_OVERRIDE,
        TypeValidator.INTERFACE_METHOD_NOT_IMPLEMENTED,
        NewTypeInference.ASSERT_FALSE,
        NewTypeInference.CANNOT_BIND_CTOR,
        NewTypeInference.CONST_REASSIGNED,
        NewTypeInference.CROSS_SCOPE_GOTCHA,
        //           NewTypeInference.FAILED_TO_UNIFY,
        //           NewTypeInference.FORIN_EXPECTS_OBJECT,
        NewTypeInference.FORIN_EXPECTS_STRING_KEY,
        //           NewTypeInference.GOOG_BIND_EXPECTS_FUNCTION,
        //           NewTypeInference.INVALID_ARGUMENT_TYPE,
        //           NewTypeInference.INVALID_CAST,
        NewTypeInference.INVALID_INFERRED_RETURN_TYPE,
        //           NewTypeInference.INVALID_OBJLIT_PROPERTY_TYPE,
        //           NewTypeInference.INVALID_OPERAND_TYPE,
        //           NewTypeInference.INVALID_THIS_TYPE_IN_BIND,
        //           NewTypeInference.MISTYPED_ASSIGN_RHS,
        //           NewTypeInference.NON_NUMERIC_ARRAY_INDEX,
        //           NewTypeInference.NOT_A_CONSTRUCTOR,
        //           NewTypeInference.NOT_UNIQUE_INSTANTIATION,
        //           NewTypeInference.POSSIBLY_INEXISTENT_PROPERTY,
        //           NewTypeInference.PROPERTY_ACCESS_ON_NONOBJECT,
        //           NewTypeInference.RETURN_NONDECLARED_TYPE,
        NewTypeInference.UNKNOWN_ASSERTION_TYPE,
        //           CheckGlobalThis.GLOBAL_THIS,
        //           CheckMissingReturn.MISSING_RETURN_STATEMENT,
        TypeCheck.CONSTRUCTOR_NOT_CALLABLE,
        TypeCheck.ILLEGAL_OBJLIT_KEY,
        //           TypeCheck.ILLEGAL_PROPERTY_CREATION,
        TypeCheck.IN_USED_WITH_STRUCT,
        //           TypeCheck.INEXISTENT_PROPERTY,
        TypeCheck.NOT_CALLABLE,
        //           TypeCheck.WRONG_ARGUMENT_COUNT,
        //           TypeValidator.ILLEGAL_PROPERTY_ACCESS,
        TypeValidator.UNKNOWN_TYPEOF_VALUE);
  }

  public static final DiagnosticGroup CHECK_EVENTFUL_OBJECT_DISPOSAL =
      DiagnosticGroups.registerGroup(
          "checkEventfulObjectDisposal",
          CheckEventfulObjectDisposal.EVENTFUL_OBJECT_NOT_DISPOSED,
          CheckEventfulObjectDisposal.EVENTFUL_OBJECT_PURELY_LOCAL,
          CheckEventfulObjectDisposal.OVERWRITE_PRIVATE_EVENTFUL_OBJECT,
          CheckEventfulObjectDisposal.UNLISTEN_WITH_ANONBOUND);

  public static final DiagnosticGroup REPORT_UNKNOWN_TYPES =
      DiagnosticGroups.registerGroup("reportUnknownTypes", TypeCheck.UNKNOWN_EXPR_TYPE);

  public static final DiagnosticGroup CHECK_STRUCT_DICT_INHERITANCE =
      DiagnosticGroups.registerDeprecatedGroup("checkStructDictInheritance");

  public static final DiagnosticGroup CHECK_VARIABLES =
      DiagnosticGroups.registerGroup(
          "checkVars",
          VarCheck.UNDEFINED_VAR_ERROR,
          VarCheck.VAR_MULTIPLY_DECLARED_ERROR,
          VariableReferenceCheck.EARLY_REFERENCE,
          VariableReferenceCheck.REDECLARED_VARIABLE);

  public static final DiagnosticGroup CHECK_USELESS_CODE =
      DiagnosticGroups.registerGroup(
          "uselessCode",
          CheckSideEffects.USELESS_CODE_ERROR,
          CheckUnreachableCode.UNREACHABLE_CODE);

  public static final DiagnosticGroup CONST =
      DiagnosticGroups.registerGroup(
          "const",
          CheckAccessControls.CONST_PROPERTY_DELETED,
          CheckAccessControls.CONST_PROPERTY_REASSIGNED_VALUE,
          ConstCheck.CONST_REASSIGNED_VALUE_ERROR,
          NewTypeInference.CONST_REASSIGNED,
          NewTypeInference.CONST_PROPERTY_REASSIGNED);

  public static final DiagnosticGroup CONSTANT_PROPERTY =
      DiagnosticGroups.registerGroup(
          "constantProperty",
          CheckAccessControls.CONST_PROPERTY_DELETED,
          CheckAccessControls.CONST_PROPERTY_REASSIGNED_VALUE,
          NewTypeInference.CONST_PROPERTY_REASSIGNED);

  public static final DiagnosticGroup TYPE_INVALIDATION =
      DiagnosticGroups.registerGroup(
          "typeInvalidation",
          DisambiguateProperties.Warnings.INVALIDATION,
          DisambiguateProperties.Warnings.INVALIDATION_ON_TYPE);

  public static final DiagnosticGroup DUPLICATE_VARS =
      DiagnosticGroups.registerGroup(
          "duplicate",
          VarCheck.VAR_MULTIPLY_DECLARED_ERROR,
          TypeValidator.DUP_VAR_DECLARATION,
          TypeValidator.DUP_VAR_DECLARATION_TYPE_MISMATCH,
          VariableReferenceCheck.REDECLARED_VARIABLE,
          GlobalTypeInfo.REDECLARED_PROPERTY);

  public static final DiagnosticGroup ES3 =
      DiagnosticGroups.registerGroup(
          "es3", RhinoErrorReporter.INVALID_ES3_PROP_NAME, RhinoErrorReporter.TRAILING_COMMA);

  static final DiagnosticGroup ES5_STRICT_UNCOMMON =
      DiagnosticGroups.registerGroup(
          "es5StrictUncommon",
          RhinoErrorReporter.INVALID_OCTAL_LITERAL,
          StrictModeCheck.USE_OF_WITH,
          StrictModeCheck.UNKNOWN_VARIABLE,
          StrictModeCheck.EVAL_DECLARATION,
          StrictModeCheck.EVAL_ASSIGNMENT,
          StrictModeCheck.ARGUMENTS_DECLARATION,
          StrictModeCheck.ARGUMENTS_ASSIGNMENT,
          StrictModeCheck.DELETE_VARIABLE,
          StrictModeCheck.DUPLICATE_OBJECT_KEY,
          StrictModeCheck.BAD_FUNCTION_DECLARATION);

  static final DiagnosticGroup ES5_STRICT_REFLECTION =
      DiagnosticGroups.registerGroup(
          "es5StrictReflection",
          StrictModeCheck.ARGUMENTS_CALLEE_FORBIDDEN,
          StrictModeCheck.ARGUMENTS_CALLER_FORBIDDEN,
          StrictModeCheck.FUNCTION_CALLER_FORBIDDEN,
          StrictModeCheck.FUNCTION_ARGUMENTS_PROP_FORBIDDEN);

  public static final DiagnosticGroup ES5_STRICT =
      DiagnosticGroups.registerGroup("es5Strict", ES5_STRICT_UNCOMMON, ES5_STRICT_REFLECTION);

  public static final DiagnosticGroup MISSING_PROVIDE =
      DiagnosticGroups.registerGroup("missingProvide", CheckProvides.MISSING_PROVIDE_WARNING);

  public static final DiagnosticGroup MISSING_REQUIRE =
      DiagnosticGroups.registerGroup(
          "missingRequire", CheckRequiresForConstructors.MISSING_REQUIRE_WARNING);

  public static final DiagnosticGroup EXTRA_REQUIRE =
      DiagnosticGroups.registerGroup(
          "extraRequire", CheckRequiresForConstructors.EXTRA_REQUIRE_WARNING);

  public static final DiagnosticGroup MISSING_GETCSSNAME =
      DiagnosticGroups.registerGroup(
          "missingGetCssName", CheckMissingGetCssName.MISSING_GETCSSNAME);

  public static final DiagnosticGroup DUPLICATE_MESSAGE =
      DiagnosticGroups.registerGroup("duplicateMessage", JsMessageVisitor.MESSAGE_DUPLICATE_KEY);

  public static final DiagnosticGroup MESSAGE_DESCRIPTIONS =
      DiagnosticGroups.registerGroup(
          "msgDescriptions", JsMessageVisitor.MESSAGE_HAS_NO_DESCRIPTION);

  public static final DiagnosticGroup MISPLACED_TYPE_ANNOTATION =
      DiagnosticGroups.registerGroup(
          "misplacedTypeAnnotation",
          CheckJSDoc.DISALLOWED_MEMBER_JSDOC,
          CheckJSDoc.MISPLACED_ANNOTATION,
          CheckJSDoc.MISPLACED_MSG_ANNOTATION);

  public static final DiagnosticGroup SUSPICIOUS_CODE =
      DiagnosticGroups.registerGroup(
          "suspiciousCode",
          CheckSuspiciousCode.SUSPICIOUS_SEMICOLON,
          CheckSuspiciousCode.SUSPICIOUS_COMPARISON_WITH_NAN,
          CheckSuspiciousCode.SUSPICIOUS_IN_OPERATOR,
          CheckSuspiciousCode.SUSPICIOUS_INSTANCEOF_LEFT_OPERAND);

  public static final DiagnosticGroup DEPRECATED_ANNOTATIONS =
      DiagnosticGroups.registerGroup("deprecatedAnnotations", CheckJSDoc.ANNOTATION_DEPRECATED);

  // These checks are not intended to be enabled as errors. It is
  // recommended that you think of them as "linter" warnings that
  // provide optional suggestions.
  public static final DiagnosticGroup LINT_CHECKS =
      DiagnosticGroups.registerGroup(
          "lintChecks", // undocumented
          CheckEmptyStatements.USELESS_EMPTY_STATEMENT,
          CheckEnums.DUPLICATE_ENUM_VALUE,
          // TODO(tbreisacher): Consider moving the CheckInterfaces warnings into the
          // checkTypes DiagnosticGroup
          CheckInterfaces.INTERFACE_FUNCTION_NOT_EMPTY,
          CheckInterfaces.INTERFACE_SHOULD_NOT_TAKE_ARGS,
          CheckJSDocStyle.MISSING_PARAM_JSDOC,
          CheckJSDocStyle.MUST_BE_PRIVATE,
          CheckJSDocStyle.OPTIONAL_PARAM_NOT_MARKED_OPTIONAL,
          CheckJSDocStyle.OPTIONAL_TYPE_NOT_USING_OPTIONAL_NAME,
          CheckNullableReturn.NULLABLE_RETURN,
          CheckNullableReturn.NULLABLE_RETURN_WITH_NAME,
          CheckForInOverArray.FOR_IN_OVER_ARRAY,
          CheckPrototypeProperties.ILLEGAL_PROTOTYPE_MEMBER,
          ImplicitNullabilityCheck.IMPLICITLY_NULLABLE_JSDOC,
          RhinoErrorReporter.JSDOC_MISSING_BRACES_WARNING,
          RhinoErrorReporter.JSDOC_MISSING_TYPE_WARNING,
          RhinoErrorReporter.TOO_MANY_TEMPLATE_PARAMS);

  public static final DiagnosticGroup USE_OF_GOOG_BASE =
      DiagnosticGroups.registerGroup("useOfGoogBase", ProcessClosurePrimitives.USE_OF_GOOG_BASE);

  public static final DiagnosticGroup CLOSURE_DEP_METHOD_USAGE_CHECKS =
      DiagnosticGroups.registerGroup(
          "closureDepMethodUsageChecks", ProcessClosurePrimitives.INVALID_CLOSURE_CALL_ERROR);

  // This group exists so that generated code can suppress these
  // warnings. Not for general use. These diagnostics will most likely
  // be moved to the suspiciousCode group.
  static {
    DiagnosticGroups.registerGroup(
        "transitionalSuspiciousCodeWarnings",
        PeepholeFoldConstants.INDEX_OUT_OF_BOUNDS_ERROR,
        PeepholeFoldConstants.NEGATING_A_NON_NUMBER_ERROR,
        PeepholeFoldConstants.BITWISE_OPERAND_OUT_OF_RANGE,
        PeepholeFoldConstants.SHIFT_AMOUNT_OUT_OF_BOUNDS,
        PeepholeFoldConstants.FRACTIONAL_BITWISE_OPERAND);
  }

  public static final DiagnosticGroup CONFORMANCE_VIOLATIONS =
      DiagnosticGroups.registerGroup(
          "conformanceViolations",
          CheckConformance.CONFORMANCE_VIOLATION,
          CheckConformance.CONFORMANCE_POSSIBLE_VIOLATION);

  static {
    // For internal use only, so there is no constant for it.
    DiagnosticGroups.registerGroup(
        "invalidProvide", ProcessClosurePrimitives.INVALID_PROVIDE_ERROR);

    DiagnosticGroups.registerGroup("es6Typed", RhinoErrorReporter.MISPLACED_TYPE_SYNTAX);
  }

  /** Adds warning levels by name. */
  void setWarningLevel(CompilerOptions options, String name, CheckLevel level) {
    DiagnosticGroup group = forName(name);
    Preconditions.checkNotNull(group, "No warning class for name: %s", name);
    options.setWarningLevel(group, level);
  }
}
/**
 * A builder for FunctionTypes, because FunctionTypes are so ridiculously complex. All methods
 * return {@code this} for ease of use.
 *
 * <p>Right now, this mostly uses JSDocInfo to infer type information about functions. In the long
 * term, developers should extend it to use other signals by overloading the various "inferXXX"
 * methods. For example, we might want to use {@code goog.inherits} calls as a signal for
 * inheritance, or {@code return} statements as a signal for return type.
 *
 * <p>NOTE(nicksantos): Organizationally, this feels like it should be in Rhino. But it depends on
 * some coding convention stuff that's really part of JSCompiler.
 *
 * @author [email protected] (Nick Santos)
 * @author [email protected] (Pascal-Louis Perez)
 */
final class FunctionTypeBuilder {

  private final String fnName;
  private final AbstractCompiler compiler;
  private final CodingConvention codingConvention;
  private final JSTypeRegistry typeRegistry;
  private final Node errorRoot;
  private final String sourceName;
  private final Scope scope;

  private JSType returnType = null;
  private boolean returnTypeInferred = false;
  private List<ObjectType> implementedInterfaces = null;
  private ObjectType baseType = null;
  private ObjectType thisType = null;
  private boolean isConstructor = false;
  private boolean isInterface = false;
  private Node parametersNode = null;
  private Node sourceNode = null;
  private String templateTypeName = null;

  static final DiagnosticType EXTENDS_WITHOUT_TYPEDEF =
      DiagnosticType.warning(
          "JSC_EXTENDS_WITHOUT_TYPEDEF",
          "@extends used without @constructor or @interface for {0}");

  static final DiagnosticType EXTENDS_NON_OBJECT =
      DiagnosticType.warning("JSC_EXTENDS_NON_OBJECT", "{0} @extends non-object type {1}");

  static final DiagnosticType RESOLVED_TAG_EMPTY =
      DiagnosticType.warning("JSC_RESOLVED_TAG_EMPTY", "Could not resolve type in {0} tag of {1}");

  static final DiagnosticType IMPLEMENTS_WITHOUT_CONSTRUCTOR =
      DiagnosticType.warning(
          "JSC_IMPLEMENTS_WITHOUT_CONSTRUCTOR",
          "@implements used without @constructor or @interface for {0}");

  static final DiagnosticType VAR_ARGS_MUST_BE_LAST =
      DiagnosticType.warning("JSC_VAR_ARGS_MUST_BE_LAST", "variable length argument must be last");

  static final DiagnosticType OPTIONAL_ARG_AT_END =
      DiagnosticType.warning("JSC_OPTIONAL_ARG_AT_END", "optional arguments must be at the end");

  static final DiagnosticType INEXISTANT_PARAM =
      DiagnosticType.warning(
          "JSC_INEXISTANT_PARAM", "parameter {0} does not appear in {1}''s parameter list");

  static final DiagnosticType TYPE_REDEFINITION =
      DiagnosticType.warning(
          "JSC_TYPE_REDEFINITION",
          "attempted re-definition of type {0}\n" + "found   : {1}\n" + "expected: {2}");

  static final DiagnosticType TEMPLATE_TYPE_DUPLICATED =
      DiagnosticType.error(
          "JSC_TEMPLATE_TYPE_DUPLICATED", "Only one parameter type must be the template type");

  static final DiagnosticType TEMPLATE_TYPE_EXPECTED =
      DiagnosticType.error(
          "JSC_TEMPLATE_TYPE_EXPECTED", "The template type must be a parameter type");

  static final DiagnosticType THIS_TYPE_NON_OBJECT =
      DiagnosticType.warning(
          "JSC_THIS_TYPE_NON_OBJECT",
          "@this type of a function must be an object\n" + "Actual type: {0}");

  private class ExtendedTypeValidator implements Predicate<JSType> {
    @Override
    public boolean apply(JSType type) {
      ObjectType objectType = ObjectType.cast(type);
      if (objectType == null) {
        reportWarning(EXTENDS_NON_OBJECT, fnName, type.toString());
      } else if (objectType.isUnknownType()
          &&
          // If this has a supertype that hasn't been resolved yet,
          // then we can assume this type will be ok once the super
          // type resolves.
          (objectType.getImplicitPrototype() == null
              || objectType.getImplicitPrototype().isResolved())) {
        reportWarning(RESOLVED_TAG_EMPTY, "@extends", fnName);
      } else {
        return true;
      }
      return false;
    }
  }

  private class ImplementedTypeValidator implements Predicate<JSType> {
    @Override
    public boolean apply(JSType type) {
      ObjectType objectType = ObjectType.cast(type);
      if (objectType == null) {
        reportError(BAD_IMPLEMENTED_TYPE, fnName);
      } else if (objectType.isUnknownType()
          &&
          // If this has a supertype that hasn't been resolved yet,
          // then we can assume this type will be ok once the super
          // type resolves.
          (objectType.getImplicitPrototype() == null
              || objectType.getImplicitPrototype().isResolved())) {
        reportWarning(RESOLVED_TAG_EMPTY, "@implements", fnName);
      } else {
        return true;
      }
      return false;
    }
  }

  private class ThisTypeValidator implements Predicate<JSType> {
    @Override
    public boolean apply(JSType type) {
      // TODO(user): Doing an instanceof check here is too
      // restrictive as (Date,Error) is, for instance, an object type
      // even though its implementation is a UnionType. Would need to
      // create interfaces JSType, ObjectType, FunctionType etc and have
      // separate implementation instead of the class hierarchy, so that
      // union types can also be object types, etc.
      if (!type.restrictByNotNullOrUndefined().isSubtype(typeRegistry.getNativeType(OBJECT_TYPE))) {
        reportWarning(THIS_TYPE_NON_OBJECT, type.toString());
        return false;
      }
      return true;
    }
  }

  /**
   * @param fnName The function name.
   * @param compiler The compiler.
   * @param errorRoot The node to associate with any warning generated by this builder.
   * @param sourceName A source name for associating any warnings that we have to emit.
   * @param scope The syntactic scope.
   */
  FunctionTypeBuilder(
      String fnName, AbstractCompiler compiler, Node errorRoot, String sourceName, Scope scope) {
    Preconditions.checkNotNull(errorRoot);

    this.fnName = fnName == null ? "" : fnName;
    this.codingConvention = compiler.getCodingConvention();
    this.typeRegistry = compiler.getTypeRegistry();
    this.errorRoot = errorRoot;
    this.sourceName = sourceName;
    this.compiler = compiler;
    this.scope = scope;
  }

  /** Sets the FUNCTION node of this function. */
  FunctionTypeBuilder setSourceNode(@Nullable Node sourceNode) {
    this.sourceNode = sourceNode;
    return this;
  }

  /**
   * Infer the parameter and return types of a function from the parameter and return types of the
   * function it is overriding.
   *
   * @param oldType The function being overridden. Does nothing if this is null.
   * @param paramsParent The LP node of the function that we're assigning to. If null, that just
   *     means we're not initializing this to a function literal.
   */
  FunctionTypeBuilder inferFromOverriddenFunction(
      @Nullable FunctionType oldType, @Nullable Node paramsParent) {
    if (oldType == null) {
      return this;
    }

    returnType = oldType.getReturnType();
    returnTypeInferred = oldType.isReturnTypeInferred();
    if (paramsParent == null) {
      // Not a function literal.
      parametersNode = oldType.getParametersNode();
      if (parametersNode == null) {
        parametersNode = new FunctionParamBuilder(typeRegistry).build();
      }
    } else {
      // We're overriding with a function literal. Apply type information
      // to each parameter of the literal.
      FunctionParamBuilder paramBuilder = new FunctionParamBuilder(typeRegistry);
      Iterator<Node> oldParams = oldType.getParameters().iterator();
      boolean warnedAboutArgList = false;
      boolean oldParamsListHitOptArgs = false;
      for (Node currentParam = paramsParent.getFirstChild();
          currentParam != null;
          currentParam = currentParam.getNext()) {
        if (oldParams.hasNext()) {
          Node oldParam = oldParams.next();
          Node newParam = paramBuilder.newParameterFromNode(oldParam);

          oldParamsListHitOptArgs =
              oldParamsListHitOptArgs || oldParam.isVarArgs() || oldParam.isOptionalArg();

          // The subclass method might right its var_args as individual
          // arguments.
          if (currentParam.getNext() != null && newParam.isVarArgs()) {
            newParam.setVarArgs(false);
            newParam.setOptionalArg(true);
          }
        } else {
          warnedAboutArgList |=
              addParameter(
                  paramBuilder,
                  typeRegistry.getNativeType(UNKNOWN_TYPE),
                  warnedAboutArgList,
                  codingConvention.isOptionalParameter(currentParam) || oldParamsListHitOptArgs,
                  codingConvention.isVarArgsParameter(currentParam));
        }
      }
      parametersNode = paramBuilder.build();
    }
    return this;
  }

  /** Infer the return type from JSDocInfo. */
  FunctionTypeBuilder inferReturnType(@Nullable JSDocInfo info) {
    if (info != null && info.hasReturnType()) {
      returnType = info.getReturnType().evaluate(scope, typeRegistry);
      returnTypeInferred = false;
    }

    if (templateTypeName != null
        && returnType != null
        && returnType.restrictByNotNullOrUndefined().isTemplateType()) {
      reportError(TEMPLATE_TYPE_EXPECTED, fnName);
    }
    return this;
  }

  /**
   * If we haven't found a return value yet, try to look at the "return" statements in the function.
   */
  FunctionTypeBuilder inferReturnStatementsAsLastResort(@Nullable Node functionBlock) {
    if (functionBlock == null || compiler.getInput(sourceName).isExtern()) {
      return this;
    }
    Preconditions.checkArgument(functionBlock.getType() == Token.BLOCK);
    if (returnType == null) {
      boolean hasNonEmptyReturns = false;
      List<Node> worklist = Lists.newArrayList(functionBlock);
      while (!worklist.isEmpty()) {
        Node current = worklist.remove(worklist.size() - 1);
        int cType = current.getType();
        if (cType == Token.RETURN && current.getFirstChild() != null || cType == Token.THROW) {
          hasNonEmptyReturns = true;
          break;
        } else if (NodeUtil.isStatementBlock(current) || NodeUtil.isControlStructure(current)) {
          for (Node child = current.getFirstChild(); child != null; child = child.getNext()) {
            worklist.add(child);
          }
        }
      }

      if (!hasNonEmptyReturns) {
        returnType = typeRegistry.getNativeType(VOID_TYPE);
        returnTypeInferred = true;
      }
    }
    return this;
  }

  /**
   * Infer the role of the function (whether it's a constructor or interface) and what it inherits
   * from in JSDocInfo.
   */
  FunctionTypeBuilder inferInheritance(@Nullable JSDocInfo info) {
    if (info != null) {
      isConstructor = info.isConstructor();
      isInterface = info.isInterface();

      // base type
      if (info.hasBaseType()) {
        if (isConstructor || isInterface) {
          JSType maybeBaseType = info.getBaseType().evaluate(scope, typeRegistry);
          if (maybeBaseType != null && maybeBaseType.setValidator(new ExtendedTypeValidator())) {
            baseType = (ObjectType) maybeBaseType;
          }
        } else {
          reportWarning(EXTENDS_WITHOUT_TYPEDEF, fnName);
        }
      }

      // implemented interfaces
      if (isConstructor || isInterface) {
        implementedInterfaces = Lists.newArrayList();
        for (JSTypeExpression t : info.getImplementedInterfaces()) {
          JSType maybeInterType = t.evaluate(scope, typeRegistry);
          if (maybeInterType != null
              && maybeInterType.setValidator(new ImplementedTypeValidator())) {
            implementedInterfaces.add((ObjectType) maybeInterType);
          }
        }
        if (baseType != null) {
          JSType maybeFunctionType = baseType.getConstructor();
          if (maybeFunctionType instanceof FunctionType) {
            FunctionType functionType = baseType.getConstructor();
            Iterables.addAll(implementedInterfaces, functionType.getImplementedInterfaces());
          }
        }
      } else if (info.getImplementedInterfaceCount() > 0) {
        reportWarning(IMPLEMENTS_WITHOUT_CONSTRUCTOR, fnName);
      }
    }

    return this;
  }

  /**
   * Infers the type of {@code this}.
   *
   * @param type The type of this.
   */
  FunctionTypeBuilder inferThisType(JSDocInfo info, JSType type) {
    ObjectType objType = ObjectType.cast(type);
    if (objType != null && (info == null || !info.hasType())) {
      thisType = objType;
    }
    return this;
  }

  /**
   * Infers the type of {@code this}.
   *
   * @param info The JSDocInfo for this function.
   * @param owner The node for the object whose prototype "owns" this function. For example, {@code
   *     A} in the expression {@code A.prototype.foo}. May be null to indicate that this is not a
   *     prototype property.
   */
  FunctionTypeBuilder inferThisType(JSDocInfo info, @Nullable Node owner) {
    ObjectType maybeThisType = null;
    if (info != null && info.hasThisType()) {
      maybeThisType = ObjectType.cast(info.getThisType().evaluate(scope, typeRegistry));
    }
    if (maybeThisType != null) {
      thisType = maybeThisType;
      thisType.setValidator(new ThisTypeValidator());
    } else if (owner != null && (info == null || !info.hasType())) {
      // If the function is of the form:
      // x.prototype.y = function() {}
      // then we can assume "x" is the @this type. On the other hand,
      // if it's of the form:
      // /** @type {Function} */ x.prototype.y;
      // then we should not give it a @this type.
      String ownerTypeName = owner.getQualifiedName();
      ObjectType ownerType =
          ObjectType.cast(
              typeRegistry.getForgivingType(
                  scope, ownerTypeName, sourceName, owner.getLineno(), owner.getCharno()));
      if (ownerType != null) {
        thisType = ownerType;
      }
    }

    return this;
  }

  /** Infer the parameter types from the doc info alone. */
  FunctionTypeBuilder inferParameterTypes(JSDocInfo info) {
    // Create a fake args parent.
    Node lp = new Node(Token.LP);
    for (String name : info.getParameterNames()) {
      lp.addChildToBack(Node.newString(Token.NAME, name));
    }

    return inferParameterTypes(lp, info);
  }

  /** Infer the parameter types from the list of argument names and the doc info. */
  FunctionTypeBuilder inferParameterTypes(@Nullable Node argsParent, @Nullable JSDocInfo info) {
    if (argsParent == null) {
      if (info == null) {
        return this;
      } else {
        return inferParameterTypes(info);
      }
    }

    // arguments
    Node oldParameterType = null;
    if (parametersNode != null) {
      oldParameterType = parametersNode.getFirstChild();
    }

    FunctionParamBuilder builder = new FunctionParamBuilder(typeRegistry);
    boolean warnedAboutArgList = false;
    Set<String> allJsDocParams =
        (info == null) ? Sets.<String>newHashSet() : Sets.newHashSet(info.getParameterNames());
    boolean foundTemplateType = false;
    for (Node arg : argsParent.children()) {
      String argumentName = arg.getString();
      allJsDocParams.remove(argumentName);

      // type from JSDocInfo
      JSType parameterType = null;
      boolean isOptionalParam = isOptionalParameter(arg, info);
      boolean isVarArgs = isVarArgsParameter(arg, info);
      if (info != null && info.hasParameterType(argumentName)) {
        parameterType = info.getParameterType(argumentName).evaluate(scope, typeRegistry);
      } else if (oldParameterType != null && oldParameterType.getJSType() != null) {
        parameterType = oldParameterType.getJSType();
        isOptionalParam = oldParameterType.isOptionalArg();
        isVarArgs = oldParameterType.isVarArgs();
      } else {
        parameterType = typeRegistry.getNativeType(UNKNOWN_TYPE);
      }

      if (templateTypeName != null
          && parameterType.restrictByNotNullOrUndefined().isTemplateType()) {
        if (foundTemplateType) {
          reportError(TEMPLATE_TYPE_DUPLICATED, fnName);
        }
        foundTemplateType = true;
      }
      warnedAboutArgList |=
          addParameter(builder, parameterType, warnedAboutArgList, isOptionalParam, isVarArgs);

      if (oldParameterType != null) {
        oldParameterType = oldParameterType.getNext();
      }
    }

    if (templateTypeName != null && !foundTemplateType) {
      reportError(TEMPLATE_TYPE_EXPECTED, fnName);
    }

    for (String inexistentName : allJsDocParams) {
      reportWarning(INEXISTANT_PARAM, inexistentName, fnName);
    }

    parametersNode = builder.build();
    return this;
  }

  /** @return Whether the given param is an optional param. */
  private boolean isOptionalParameter(Node param, @Nullable JSDocInfo info) {
    if (codingConvention.isOptionalParameter(param)) {
      return true;
    }

    String paramName = param.getString();
    return info != null
        && info.hasParameterType(paramName)
        && info.getParameterType(paramName).isOptionalArg();
  }

  /**
   * Determine whether this is a var args parameter.
   *
   * @return Whether the given param is a var args param.
   */
  private boolean isVarArgsParameter(Node param, @Nullable JSDocInfo info) {
    if (codingConvention.isVarArgsParameter(param)) {
      return true;
    }

    String paramName = param.getString();
    return info != null
        && info.hasParameterType(paramName)
        && info.getParameterType(paramName).isVarArgs();
  }

  /** Infer the template type from the doc info. */
  FunctionTypeBuilder inferTemplateTypeName(@Nullable JSDocInfo info) {
    if (info != null) {
      templateTypeName = info.getTemplateTypeName();
      typeRegistry.setTemplateTypeName(templateTypeName);
    }
    return this;
  }

  /**
   * Add a parameter to the param list.
   *
   * @param builder A builder.
   * @param paramType The parameter type.
   * @param warnedAboutArgList Whether we've already warned about arg ordering issues (like if
   *     optional args appeared before required ones).
   * @param isOptional Is this an optional parameter?
   * @param isVarArgs Is this a var args parameter?
   * @return Whether a warning was emitted.
   */
  private boolean addParameter(
      FunctionParamBuilder builder,
      JSType paramType,
      boolean warnedAboutArgList,
      boolean isOptional,
      boolean isVarArgs) {
    boolean emittedWarning = false;
    if (isOptional) {
      // Remembering that an optional parameter has been encountered
      // so that if a non optional param is encountered later, an
      // error can be reported.
      if (!builder.addOptionalParams(paramType) && !warnedAboutArgList) {
        reportWarning(VAR_ARGS_MUST_BE_LAST);
        emittedWarning = true;
      }
    } else if (isVarArgs) {
      if (!builder.addVarArgs(paramType) && !warnedAboutArgList) {
        reportWarning(VAR_ARGS_MUST_BE_LAST);
        emittedWarning = true;
      }
    } else {
      if (!builder.addRequiredParams(paramType) && !warnedAboutArgList) {
        // An optional parameter was seen and this argument is not an optional
        // or var arg so it is an error.
        if (builder.hasVarArgs()) {
          reportWarning(VAR_ARGS_MUST_BE_LAST);
        } else {
          reportWarning(OPTIONAL_ARG_AT_END);
        }
        emittedWarning = true;
      }
    }
    return emittedWarning;
  }

  /** Builds the function type, and puts it in the registry. */
  FunctionType buildAndRegister() {
    if (returnType == null) {
      returnType = typeRegistry.getNativeType(UNKNOWN_TYPE);
    }

    if (parametersNode == null) {
      throw new IllegalStateException("All Function types must have params and a return type");
    }

    FunctionType fnType;
    if (isConstructor) {
      fnType = getOrCreateConstructor();
    } else if (isInterface) {
      fnType = typeRegistry.createInterfaceType(fnName, sourceNode);
      if (getScopeDeclaredIn().isGlobal() && !fnName.isEmpty()) {
        typeRegistry.declareType(fnName, fnType.getInstanceType());
      }
      maybeSetBaseType(fnType);
    } else {
      fnType =
          new FunctionBuilder(typeRegistry)
              .withName(fnName)
              .withSourceNode(sourceNode)
              .withParamsNode(parametersNode)
              .withReturnType(returnType, returnTypeInferred)
              .withTypeOfThis(thisType)
              .withTemplateName(templateTypeName)
              .build();
      maybeSetBaseType(fnType);
    }

    if (implementedInterfaces != null) {
      fnType.setImplementedInterfaces(implementedInterfaces);
    }

    typeRegistry.clearTemplateTypeName();

    return fnType;
  }

  private void maybeSetBaseType(FunctionType fnType) {
    if (baseType != null) {
      fnType.setPrototypeBasedOn(baseType);
    }
  }

  /**
   * Returns a constructor function either by returning it from the registry if it exists or
   * creating and registering a new type. If there is already a type, then warn if the existing type
   * is different than the one we are creating, though still return the existing function if
   * possible. The primary purpose of this is that registering a constructor will fail for all
   * built-in types that are initialized in {@link JSTypeRegistry}. We a) want to make sure that the
   * type information specified in the externs file matches what is in the registry and b) annotate
   * the externs with the {@link JSType} from the registry so that there are not two separate JSType
   * objects for one type.
   */
  private FunctionType getOrCreateConstructor() {
    FunctionType fnType =
        typeRegistry.createConstructorType(fnName, sourceNode, parametersNode, returnType);
    JSType existingType = typeRegistry.getType(fnName);

    if (existingType != null) {
      boolean isInstanceObject = existingType instanceof InstanceObjectType;
      if (isInstanceObject || fnName.equals("Function")) {
        FunctionType existingFn =
            isInstanceObject
                ? ((InstanceObjectType) existingType).getConstructor()
                : typeRegistry.getNativeFunctionType(FUNCTION_FUNCTION_TYPE);

        if (existingFn.getSource() == null) {
          existingFn.setSource(sourceNode);
        }

        if (!existingFn.hasEqualCallType(fnType)) {
          reportWarning(TYPE_REDEFINITION, fnName, fnType.toString(), existingFn.toString());
        }

        return existingFn;
      } else {
        // We fall through and return the created type, even though it will fail
        // to register. We have no choice as we have to return a function. We
        // issue an error elsewhere though, so the user should fix it.
      }
    }

    maybeSetBaseType(fnType);

    if (getScopeDeclaredIn().isGlobal() && !fnName.isEmpty()) {
      typeRegistry.declareType(fnName, fnType.getInstanceType());
    }
    return fnType;
  }

  private void reportWarning(DiagnosticType warning, String... args) {
    compiler.report(JSError.make(sourceName, errorRoot, warning, args));
  }

  private void reportError(DiagnosticType error, String... args) {
    compiler.report(JSError.make(sourceName, errorRoot, error, args));
  }

  /** Determines whether the given jsdoc info declares a function type. */
  static boolean isFunctionTypeDeclaration(JSDocInfo info) {
    return info.getParameterCount() > 0
        || info.hasReturnType()
        || info.hasThisType()
        || info.isConstructor()
        || info.isInterface();
  }

  /**
   * The scope that we should declare this function in, if it needs to be declared in a scope.
   * Notice that TypedScopeCreator takes care of most scope-declaring.
   */
  private Scope getScopeDeclaredIn() {
    int dotIndex = fnName.indexOf(".");
    if (dotIndex != -1) {
      String rootVarName = fnName.substring(0, dotIndex);
      Var rootVar = scope.getVar(rootVarName);
      if (rootVar != null) {
        return rootVar.getScope();
      }
    }
    return scope;
  }
}
/**
 * Records all of the symbols and properties that should be exported.
 *
 * <p>Currently applies to: - function foo() {} - var foo = function() {} - foo.bar = function() {}
 * - var FOO = ...; - foo.BAR = ...;
 *
 * <p>FOO = BAR = 5; and var FOO = BAR = 5; are not supported because the annotation is ambiguous to
 * whether it applies to all the variables or only the first one.
 */
class FindExportableNodes extends AbstractPostOrderCallback {

  static final DiagnosticType NON_GLOBAL_ERROR =
      DiagnosticType.error(
          "JSC_NON_GLOBAL_ERROR",
          "@export only applies to symbols/properties defined in the " + "global scope.");

  static final DiagnosticType EXPORT_ANNOTATION_NOT_ALLOWED =
      DiagnosticType.error(
          "JSC_EXPORT_ANNOTATION_NOT_ALLOWED", "@export is not supported on this expression.");

  private final AbstractCompiler compiler;

  /**
   * It's convenient to be able to iterate over exports in the order in which they are encountered.
   */
  private final LinkedHashMap<String, GenerateNodeContext> exports = new LinkedHashMap<>();

  private final boolean allowLocalExports;

  FindExportableNodes(AbstractCompiler compiler, boolean allowLocalExports) {
    this.compiler = compiler;
    this.allowLocalExports = allowLocalExports;
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    JSDocInfo docInfo = n.getJSDocInfo();
    if (docInfo != null && docInfo.isExport()) {

      if (parent.isAssign() && (n.isFunction() || n.isClass())) {
        JSDocInfo parentInfo = parent.getJSDocInfo();
        if (parentInfo != null && parentInfo.isExport()) {
          // ScopedAliases produces export annotations on both the function/class
          // node and assign node, we only want to visit the assign node.
          return;
        }
      }

      String export = null;
      GenerateNodeContext context = null;

      switch (n.getType()) {
        case Token.FUNCTION:
        case Token.CLASS:
          if (parent.isScript()) {
            export = NodeUtil.getName(n);
            context = new GenerateNodeContext(n, Mode.EXPORT);
          }
          break;

        case Token.MEMBER_FUNCTION_DEF:
          export = n.getString();
          context = new GenerateNodeContext(n, Mode.EXPORT);
          break;

        case Token.ASSIGN:
          Node grandparent = parent.getParent();
          if (parent.isExprResult() && !n.getLastChild().isAssign()) {
            if (grandparent != null
                && grandparent.isScript()
                && n.getFirstChild().isQualifiedName()) {
              export = n.getFirstChild().getQualifiedName();
              context = new GenerateNodeContext(n, Mode.EXPORT);
            } else if (allowLocalExports && n.getFirstChild().isGetProp()) {
              Node target = n.getFirstChild();
              export = target.getLastChild().getString();
              context = new GenerateNodeContext(n, Mode.EXTERN);
            }
          }
          break;

        case Token.VAR:
        case Token.LET:
        case Token.CONST:
          if (parent.isScript()) {
            if (n.getFirstChild().hasChildren() && !n.getFirstChild().getFirstChild().isAssign()) {
              export = n.getFirstChild().getString();
              context = new GenerateNodeContext(n, Mode.EXPORT);
            }
          }
          break;

        case Token.GETPROP:
          if (allowLocalExports && parent.isExprResult()) {
            export = n.getLastChild().getString();
            context = new GenerateNodeContext(n, Mode.EXTERN);
          }
          break;

        case Token.STRING_KEY:
        case Token.GETTER_DEF:
        case Token.SETTER_DEF:
          if (allowLocalExports) {
            export = n.getString();
            context = new GenerateNodeContext(n, Mode.EXTERN);
          }
          break;
      }

      if (export != null) {
        exports.put(export, context);
      } else {
        // Don't produce extra warnings for functions values of object literals
        if (!n.isFunction() || !NodeUtil.isObjectLitKey(parent)) {
          if (allowLocalExports) {
            compiler.report(t.makeError(n, EXPORT_ANNOTATION_NOT_ALLOWED));
          } else {
            compiler.report(t.makeError(n, NON_GLOBAL_ERROR));
          }
        }
      }
    }
  }

  LinkedHashMap<String, GenerateNodeContext> getExports() {
    return exports;
  }

  static enum Mode {
    EXPORT,
    EXTERN
  }

  /** Context holding the node references required for generating the export calls. */
  static class GenerateNodeContext {
    private final Node node;
    private final Mode mode;

    GenerateNodeContext(Node node, Mode mode) {
      this.node = node;
      this.mode = mode;
    }

    Node getNode() {
      return node;
    }

    public Mode getMode() {
      return mode;
    }
  }
}
Example #15
0
/**
 * A compiler pass that verifies the structure of the AST conforms to a number of invariants.
 * Because this can add a lot of overhead, we only run this in development mode.
 */
class SanityCheck implements CompilerPass {

  static final DiagnosticType CANNOT_PARSE_GENERATED_CODE =
      DiagnosticType.error(
          "JSC_CANNOT_PARSE_GENERATED_CODE",
          "Internal compiler error. Cannot parse generated code: {0}");

  static final DiagnosticType GENERATED_BAD_CODE =
      DiagnosticType.error(
          "JSC_GENERATED_BAD_CODE",
          "Internal compiler error. Generated bad code."
              + "----------------------------------------\n"
              + "Expected:\n{0}\n"
              + "----------------------------------------\n"
              + "Actual:\n{1}");

  private final AbstractCompiler compiler;

  SanityCheck(AbstractCompiler compiler) {
    this.compiler = compiler;
  }

  public void process(Node externs, Node root) {
    sanityCheckNormalization(externs, root);
    sanityCheckCodeGeneration(root);
  }

  /**
   * Sanity checks code generation by performing it once, parsing the result, then generating code
   * from the second parse tree to verify that it matches the code generated from the first parse
   * tree.
   *
   * @return The regenerated parse tree. Null on error.
   */
  private Node sanityCheckCodeGeneration(Node root) {
    if (compiler.hasHaltingErrors()) {
      // Don't even bother checking code generation if we already know the
      // the code is bad.
      return null;
    }

    String source = compiler.toSource(root);
    Node root2 = compiler.parseSyntheticCode(source);
    if (compiler.hasHaltingErrors()) {
      compiler.report(
          JSError.make(
              CANNOT_PARSE_GENERATED_CODE, Strings.truncateAtMaxLength(source, 100, true)));

      // Throw an exception, so that the infrastructure will tell us
      // which pass violated the sanity check.
      throw new IllegalStateException("Sanity Check failed");
    }

    String source2 = compiler.toSource(root2);
    if (!source.equals(source2)) {
      compiler.report(
          JSError.make(
              GENERATED_BAD_CODE,
              Strings.truncateAtMaxLength(source, 1000, true),
              Strings.truncateAtMaxLength(source2, 1000, true)));

      // Throw an exception, so that the infrastructure will tell us
      // which pass violated the sanity check.
      throw new IllegalStateException("Sanity Check failed");
    }

    return root2;
  }

  /** Sanity checks the AST. This is by verifing the normalization passes do nothing. */
  private void sanityCheckNormalization(Node externs, Node root) {
    // Verify nothing has inappropriately denormalize the AST.
    CodeChangeHandler handler = new CodeChangeHandler.ForbiddenChange();
    compiler.addChangeHandler(handler);

    // TODO(johnlenz): Change these normalization checks Preconditions and
    // Exceptions into Errors so that it is easier to find the root cause
    // when there are cascading issues.
    new PrepareAst(compiler, true).process(null, root);
    if (compiler.isNormalized()) {
      (new Normalize(compiler, true)).process(externs, root);

      boolean checkUserDeclarations = true;
      CompilerPass pass = new Normalize.VerifyConstants(compiler, checkUserDeclarations);
      pass.process(externs, root);
    }

    compiler.removeChangeHandler(handler);
  }
}
/** Insures '@constructor X' has a 'goog.provide("X")' . */
class CheckProvides implements HotSwapCompilerPass {
  private final AbstractCompiler compiler;
  private final CheckLevel checkLevel;
  private final CodingConvention codingConvention;

  static final DiagnosticType MISSING_PROVIDE_WARNING =
      DiagnosticType.disabled("JSC_MISSING_PROVIDE", "missing goog.provide(''{0}'')");

  CheckProvides(AbstractCompiler compiler, CheckLevel checkLevel) {
    this.compiler = compiler;
    this.checkLevel = checkLevel;
    this.codingConvention = compiler.getCodingConvention();
  }

  @Override
  public void process(Node externs, Node root) {
    hotSwapScript(root);
  }

  @Override
  public void hotSwapScript(Node scriptRoot) {
    CheckProvidesCallback callback = new CheckProvidesCallback(codingConvention);
    new NodeTraversal(compiler, callback).traverse(scriptRoot);
  }

  private class CheckProvidesCallback extends AbstractShallowCallback {
    private final Map<String, Node> provides = Maps.newHashMap();
    private final Map<String, Node> ctors = Maps.newHashMap();
    private final CodingConvention convention;

    CheckProvidesCallback(CodingConvention convention) {
      this.convention = convention;
    }

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      switch (n.getType()) {
        case Token.CALL:
          String providedClassName = codingConvention.extractClassNameIfProvide(n, parent);
          if (providedClassName != null) {
            provides.put(providedClassName, n);
          }
          break;
        case Token.FUNCTION:
          visitFunctionNode(n, parent);
          break;
        case Token.SCRIPT:
          visitScriptNode(t, n);
      }
    }

    private void visitFunctionNode(Node n, Node parent) {
      Node name = null;
      JSDocInfo info = parent.getJSDocInfo();
      if (info != null && info.isConstructor()) {
        name = parent.getFirstChild();
      } else {
        // look to the child, maybe it's a named function
        info = n.getJSDocInfo();
        if (info != null && info.isConstructor()) {
          name = n.getFirstChild();
        }
      }
      if (name != null && name.isQualifiedName()) {
        String qualifiedName = name.getQualifiedName();
        if (!this.convention.isPrivate(qualifiedName)) {
          Visibility visibility = info.getVisibility();
          if (!visibility.equals(JSDocInfo.Visibility.PRIVATE)) {
            ctors.put(qualifiedName, name);
          }
        }
      }
    }

    private void visitScriptNode(NodeTraversal t, Node n) {
      for (String ctorName : ctors.keySet()) {
        if (!provides.containsKey(ctorName)) {
          compiler.report(
              t.makeError(ctors.get(ctorName), checkLevel, MISSING_PROVIDE_WARNING, ctorName));
        }
      }
      provides.clear();
      ctors.clear();
    }
  }
}
/** A compiler pass to run the type inference analysis. */
class TypeInferencePass implements CompilerPass {

  static final DiagnosticType DATAFLOW_ERROR =
      DiagnosticType.warning("JSC_INTERNAL_ERROR_DATAFLOW", "non-monotonic data-flow analysis");

  private final AbstractCompiler compiler;
  private final ReverseAbstractInterpreter reverseInterpreter;
  private Scope topScope;
  private ScopeCreator scopeCreator;
  private final Map<String, AssertionFunctionSpec> assertionFunctionsMap;

  /**
   * Local variables that are declared in an outer scope, but are assigned in an inner scope. We
   * cannot do type inference on these vars.
   */
  private final Multimap<Scope, Var> escapedLocalVars = HashMultimap.create();

  TypeInferencePass(
      AbstractCompiler compiler,
      ReverseAbstractInterpreter reverseInterpreter,
      Scope topScope,
      ScopeCreator scopeCreator) {
    this.compiler = compiler;
    this.reverseInterpreter = reverseInterpreter;
    this.topScope = topScope;
    this.scopeCreator = scopeCreator;

    assertionFunctionsMap = Maps.newHashMap();
    for (AssertionFunctionSpec assertionFucntion :
        compiler.getCodingConvention().getAssertionFunctions()) {
      assertionFunctionsMap.put(assertionFucntion.getFunctionName(), assertionFucntion);
    }
  }

  /**
   * Main entry point for type inference when running over the whole tree.
   *
   * @param externsRoot The root of the externs parse tree.
   * @param jsRoot The root of the input parse tree to be checked.
   */
  public void process(Node externsRoot, Node jsRoot) {
    Node externsAndJs = jsRoot.getParent();
    Preconditions.checkState(externsAndJs != null);
    Preconditions.checkState(externsRoot == null || externsAndJs.hasChild(externsRoot));

    inferTypes(externsAndJs);
  }

  /** Entry point for type inference when running over part of the tree. */
  void inferTypes(Node node) {
    NodeTraversal inferTypes =
        new NodeTraversal(compiler, new TypeInferringCallback(), scopeCreator);
    inferTypes.traverseWithScope(node, topScope);
  }

  private Collection<Var> getUnflowableVars(Scope scope) {
    List<Var> vars = Lists.newArrayList();
    for (Scope current = scope; current.isLocal(); current = current.getParent()) {
      vars.addAll(escapedLocalVars.get(current));
    }
    return vars;
  }

  void inferTypes(NodeTraversal t, Node n, Scope scope) {
    TypeInference typeInference =
        new TypeInference(
            compiler,
            computeCfg(n),
            reverseInterpreter,
            scope,
            assertionFunctionsMap,
            getUnflowableVars(scope));
    try {
      typeInference.analyze();
      escapedLocalVars.putAll(typeInference.getAssignedOuterLocalVars());

      // Resolve any new type names found during the inference.
      compiler.getTypeRegistry().resolveTypesInScope(scope);

    } catch (DataFlowAnalysis.MaxIterationsExceededException e) {
      compiler.report(t.makeError(n, DATAFLOW_ERROR));
    }
  }

  private class TypeInferringCallback implements ScopedCallback {
    public void enterScope(NodeTraversal t) {
      Scope scope = t.getScope();
      Node node = t.getCurrentNode();
      if (scope.isGlobal()) {
        inferTypes(t, node, scope);
      }
    }

    public void exitScope(NodeTraversal t) {
      Scope scope = t.getScope();
      Node node = t.getCurrentNode();
      if (scope.isLocal()) {
        inferTypes(t, node, scope);
      }
    }

    public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
      return true;
    }

    public void visit(NodeTraversal t, Node n, Node parent) {
      // Do nothing
    }
  }

  private ControlFlowGraph<Node> computeCfg(Node n) {
    ControlFlowAnalysis cfa = new ControlFlowAnalysis(compiler, false);
    cfa.process(null, n);
    return cfa.getCfg();
  }
}
/**
 * Converts ES6 code to valid ES5 code. This class does most of the transpilation, and
 * https://github.com/google/closure-compiler/wiki/ECMAScript6 lists which ES6 features are
 * supported. Other classes that start with "Es6" do other parts of the transpilation.
 *
 * <p>In most cases, the output is valid as ES3 (hence the class name) but in some cases, if the
 * output language is set to ES5, we rely on ES5 features such as getters, setters, and
 * Object.defineProperties.
 *
 * @author [email protected] (Tyler Breisacher)
 */
public final class Es6ToEs3Converter implements NodeTraversal.Callback, HotSwapCompilerPass {
  private final AbstractCompiler compiler;

  static final DiagnosticType CANNOT_CONVERT =
      DiagnosticType.error("JSC_CANNOT_CONVERT", "This code cannot be converted from ES6. {0}");

  // TODO(tbreisacher): Remove this once we have implemented transpilation for all the features
  // we intend to support.
  static final DiagnosticType CANNOT_CONVERT_YET =
      DiagnosticType.error(
          "JSC_CANNOT_CONVERT_YET", "ES6 transpilation of ''{0}'' is not yet implemented.");

  static final DiagnosticType DYNAMIC_EXTENDS_TYPE =
      DiagnosticType.error(
          "JSC_DYNAMIC_EXTENDS_TYPE", "The class in an extends clause must be a qualified name.");

  static final DiagnosticType CLASS_REASSIGNMENT =
      DiagnosticType.error(
          "CLASS_REASSIGNMENT", "Class names defined inside a function cannot be reassigned.");

  static final DiagnosticType CONFLICTING_GETTER_SETTER_TYPE =
      DiagnosticType.error(
          "CONFLICTING_GETTER_SETTER_TYPE",
          "The types of the getter and setter for property ''{0}'' do not match.");

  static final DiagnosticType BAD_REST_PARAMETER_ANNOTATION =
      DiagnosticType.warning(
          "BAD_REST_PARAMETER_ANNOTATION",
          "Missing \"...\" in type annotation for rest parameter.");

  // The name of the index variable for populating the rest parameter array.
  private static final String REST_INDEX = "$jscomp$restIndex";

  // The name of the placeholder for the rest parameters.
  private static final String REST_PARAMS = "$jscomp$restParams";

  private static final String FRESH_SPREAD_VAR = "$jscomp$spread$args";

  private static final String FRESH_COMP_PROP_VAR = "$jscomp$compprop";

  private static final String ITER_BASE = "$jscomp$iter$";

  private static final String ITER_RESULT = "$jscomp$key$";

  // These functions are defined in js/es6_runtime.js
  static final String INHERITS = "$jscomp.inherits";
  static final String MAKE_ITER = "$jscomp.makeIterator";

  public Es6ToEs3Converter(AbstractCompiler compiler) {
    this.compiler = compiler;
  }

  @Override
  public void process(Node externs, Node root) {
    NodeTraversal.traverseEs6(compiler, externs, this);
    NodeTraversal.traverseEs6(compiler, root, this);
  }

  @Override
  public void hotSwapScript(Node scriptRoot, Node originalRoot) {
    NodeTraversal.traverseEs6(compiler, scriptRoot, this);
  }

  /**
   * Some nodes must be visited pre-order in order to rewrite the references to {@code this}
   * correctly. Everything else is translated post-order in {@link #visit}.
   */
  @Override
  public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
    switch (n.getType()) {
      case Token.REST:
        visitRestParam(n, parent);
        break;
      case Token.GETTER_DEF:
      case Token.SETTER_DEF:
        if (compiler.getOptions().getLanguageOut() == LanguageMode.ECMASCRIPT3) {
          cannotConvert(n, "ES5 getters/setters (consider using --language_out=ES5)");
          return false;
        }
        break;
    }
    return true;
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    switch (n.getType()) {
      case Token.OBJECTLIT:
        for (Node child : n.children()) {
          if (child.isComputedProp()) {
            visitObjectWithComputedProperty(n, parent);
            break;
          }
        }
        break;
      case Token.MEMBER_FUNCTION_DEF:
        if (parent.isObjectLit()) {
          visitMemberDefInObjectLit(n, parent);
        }
        break;
      case Token.FOR_OF:
        visitForOf(n, parent);
        break;
      case Token.STRING_KEY:
        visitStringKey(n);
        break;
      case Token.CLASS:
        for (Node member = n.getLastChild().getFirstChild();
            member != null;
            member = member.getNext()) {
          if (member.getBooleanProp(Node.COMPUTED_PROP_GETTER)
              || member.getBooleanProp(Node.COMPUTED_PROP_SETTER)) {
            cannotConvert(member, "computed getter or setter in class definition");
            return;
          }
        }
        visitClass(n, parent);
        break;
      case Token.ARRAYLIT:
      case Token.NEW:
      case Token.CALL:
        for (Node child : n.children()) {
          if (child.isSpread()) {
            visitArrayLitOrCallWithSpread(n, parent);
            break;
          }
        }
        break;
      case Token.TAGGED_TEMPLATELIT:
        Es6TemplateLiterals.visitTaggedTemplateLiteral(t, n);
        break;
      case Token.TEMPLATELIT:
        if (!parent.isTaggedTemplateLit()) {
          Es6TemplateLiterals.visitTemplateLiteral(t, n);
        }
        break;
    }
  }

  /**
   * Converts a member definition in an object literal to an ES3 key/value pair. Member definitions
   * in classes are handled in {@link #visitClass}.
   */
  private void visitMemberDefInObjectLit(Node n, Node parent) {
    String name = n.getString();
    Node stringKey = IR.stringKey(name, n.getFirstChild().detachFromParent());
    parent.replaceChild(n, stringKey);
    compiler.reportCodeChange();
  }

  /** Converts extended object literal {a} to {a:a}. */
  private void visitStringKey(Node n) {
    if (!n.hasChildren()) {
      Node name = IR.name(n.getString());
      name.useSourceInfoIfMissingFrom(n);
      n.addChildToBack(name);
      compiler.reportCodeChange();
    }
  }

  private void visitForOf(Node node, Node parent) {
    Node variable = node.removeFirstChild();
    Node iterable = node.removeFirstChild();
    Node body = node.removeFirstChild();

    Node iterName = IR.name(ITER_BASE + compiler.getUniqueNameIdSupplier().get());
    Node getNext = IR.call(IR.getprop(iterName.cloneTree(), IR.string("next")));
    String variableName;
    int declType;
    if (variable.isName()) {
      declType = Token.NAME;
      variableName = variable.getQualifiedName();
    } else {
      Preconditions.checkState(
          NodeUtil.isNameDeclaration(variable), "Expected var, let, or const. Got %s", variable);
      declType = variable.getType();
      variableName = variable.getFirstChild().getQualifiedName();
    }
    Node iterResult = IR.name(ITER_RESULT + variableName);

    Node makeIter = IR.call(NodeUtil.newQName(compiler, MAKE_ITER), iterable);
    compiler.needsEs6Runtime = true;

    Node init = IR.var(iterName.cloneTree(), makeIter);
    Node initIterResult = iterResult.cloneTree();
    initIterResult.addChildToFront(getNext.cloneTree());
    init.addChildToBack(initIterResult);

    Node cond = IR.not(IR.getprop(iterResult.cloneTree(), IR.string("done")));
    Node incr = IR.assign(iterResult.cloneTree(), getNext.cloneTree());

    Node declarationOrAssign;
    if (declType == Token.NAME) {
      declarationOrAssign =
          IR.exprResult(
              IR.assign(
                  IR.name(variableName), IR.getprop(iterResult.cloneTree(), IR.string("value"))));
    } else {
      declarationOrAssign = new Node(declType, IR.name(variableName));
      declarationOrAssign
          .getFirstChild()
          .addChildToBack(IR.getprop(iterResult.cloneTree(), IR.string("value")));
    }
    body.addChildToFront(declarationOrAssign);

    Node newFor = IR.forNode(init, cond, incr, body);
    newFor.useSourceInfoIfMissingFromForTree(node);
    parent.replaceChild(node, newFor);
    compiler.reportCodeChange();
  }

  private void checkClassReassignment(Node clazz) {
    Node name = NodeUtil.getClassNameNode(clazz);
    Node enclosingFunction = NodeUtil.getEnclosingFunction(clazz);
    if (enclosingFunction == null) {
      return;
    }
    CheckClassAssignments checkAssigns = new CheckClassAssignments(name);
    NodeTraversal.traverseEs6(compiler, enclosingFunction, checkAssigns);
  }

  /** Processes a rest parameter */
  private void visitRestParam(Node restParam, Node paramList) {
    Node functionBody = paramList.getLastSibling();

    restParam.setType(Token.NAME);
    restParam.setVarArgs(true);

    // Make sure rest parameters are typechecked
    JSTypeExpression type = null;
    JSDocInfo info = restParam.getJSDocInfo();
    String paramName = restParam.getString();
    if (info != null) {
      type = info.getType();
    } else {
      JSDocInfo functionInfo = paramList.getParent().getJSDocInfo();
      if (functionInfo != null) {
        type = functionInfo.getParameterType(paramName);
      }
    }
    if (type != null && type.getRoot().getType() != Token.ELLIPSIS) {
      compiler.report(JSError.make(restParam, BAD_REST_PARAMETER_ANNOTATION));
    }

    if (!functionBody.hasChildren()) {
      // If function has no body, we are done!
      compiler.reportCodeChange();
      return;
    }

    Node newBlock = IR.block().useSourceInfoFrom(functionBody);
    Node name = IR.name(paramName);
    Node let = IR.let(name, IR.name(REST_PARAMS)).useSourceInfoIfMissingFromForTree(functionBody);
    newBlock.addChildToFront(let);

    for (Node child : functionBody.children()) {
      newBlock.addChildToBack(child.detachFromParent());
    }

    if (type != null) {
      Node arrayType = IR.string("Array");
      Node typeNode = type.getRoot();
      Node memberType =
          typeNode.getType() == Token.ELLIPSIS
              ? typeNode.getFirstChild().cloneNode()
              : typeNode.cloneNode();
      arrayType.addChildToFront(
          new Node(Token.BLOCK, memberType).useSourceInfoIfMissingFrom(typeNode));
      JSDocInfoBuilder builder = new JSDocInfoBuilder(false);
      builder.recordType(
          new JSTypeExpression(new Node(Token.BANG, arrayType), restParam.getSourceFileName()));
      name.setJSDocInfo(builder.build());
    }

    int restIndex = paramList.getIndexOfChild(restParam);
    Node newArr = IR.var(IR.name(REST_PARAMS), IR.arraylit());
    functionBody.addChildToFront(newArr.useSourceInfoIfMissingFromForTree(restParam));
    Node init = IR.var(IR.name(REST_INDEX), IR.number(restIndex));
    Node cond = IR.lt(IR.name(REST_INDEX), IR.getprop(IR.name("arguments"), IR.string("length")));
    Node incr = IR.inc(IR.name(REST_INDEX), false);
    Node body =
        IR.block(
            IR.exprResult(
                IR.assign(
                    IR.getelem(
                        IR.name(REST_PARAMS), IR.sub(IR.name(REST_INDEX), IR.number(restIndex))),
                    IR.getelem(IR.name("arguments"), IR.name(REST_INDEX)))));
    functionBody.addChildAfter(
        IR.forNode(init, cond, incr, body).useSourceInfoIfMissingFromForTree(restParam), newArr);
    functionBody.addChildToBack(newBlock);
    compiler.reportCodeChange();

    // For now, we are running transpilation before type-checking, so we'll
    // need to make sure changes don't invalidate the JSDoc annotations.
    // Therefore we keep the parameter list the same length and only initialize
    // the values if they are set to undefined.
  }

  /**
   * Processes array literals or calls containing spreads. Eg.: [1, 2, ...x, 4, 5] => [1,
   * 2].concat(x, [4, 5]); Eg.: f(...arr) => f.apply(null, arr) Eg.: new F(...args) => new
   * Function.prototype.bind.apply(F, [].concat(args))
   */
  private void visitArrayLitOrCallWithSpread(Node node, Node parent) {
    Preconditions.checkArgument(node.isCall() || node.isArrayLit() || node.isNew());
    List<Node> groups = new ArrayList<>();
    Node currGroup = null;
    Node callee = node.isArrayLit() ? null : node.removeFirstChild();
    Node currElement = node.removeFirstChild();
    while (currElement != null) {
      if (currElement.isSpread()) {
        if (currGroup != null) {
          groups.add(currGroup);
          currGroup = null;
        }
        groups.add(currElement.removeFirstChild());
      } else {
        if (currGroup == null) {
          currGroup = IR.arraylit();
        }
        currGroup.addChildToBack(currElement);
      }
      currElement = node.removeFirstChild();
    }
    if (currGroup != null) {
      groups.add(currGroup);
    }
    Node result = null;
    Node joinedGroups =
        IR.call(
            IR.getprop(IR.arraylit(), IR.string("concat")),
            groups.toArray(new Node[groups.size()]));
    if (node.isArrayLit()) {
      result = joinedGroups;
    } else if (node.isCall()) {
      if (NodeUtil.mayHaveSideEffects(callee) && callee.isGetProp()) {
        Node statement = node;
        while (!NodeUtil.isStatement(statement)) {
          statement = statement.getParent();
        }
        Node freshVar = IR.name(FRESH_SPREAD_VAR + compiler.getUniqueNameIdSupplier().get());
        Node n = IR.var(freshVar.cloneTree());
        n.useSourceInfoIfMissingFromForTree(statement);
        statement.getParent().addChildBefore(n, statement);
        callee.addChildToFront(IR.assign(freshVar.cloneTree(), callee.removeFirstChild()));
        result = IR.call(IR.getprop(callee, IR.string("apply")), freshVar, joinedGroups);
      } else {
        Node context = callee.isGetProp() ? callee.getFirstChild().cloneTree() : IR.nullNode();
        result = IR.call(IR.getprop(callee, IR.string("apply")), context, joinedGroups);
      }
    } else {
      Node bindApply = NodeUtil.newQName(compiler, "Function.prototype.bind.apply");
      result = IR.newNode(bindApply, callee, joinedGroups);
    }
    result.useSourceInfoIfMissingFromForTree(node);
    parent.replaceChild(node, result);
    compiler.reportCodeChange();
  }

  private void visitObjectWithComputedProperty(Node obj, Node parent) {
    Preconditions.checkArgument(obj.isObjectLit());
    List<Node> props = new ArrayList<>();
    Node currElement = obj.getFirstChild();

    while (currElement != null) {
      if (currElement.getBooleanProp(Node.COMPUTED_PROP_GETTER)
          || currElement.getBooleanProp(Node.COMPUTED_PROP_SETTER)) {
        cannotConvertYet(currElement, "computed getter/setter");
        return;
      } else if (currElement.isGetterDef() || currElement.isSetterDef()) {
        currElement = currElement.getNext();
      } else {
        Node nextNode = currElement.getNext();
        obj.removeChild(currElement);
        props.add(currElement);
        currElement = nextNode;
      }
    }

    String objName = FRESH_COMP_PROP_VAR + compiler.getUniqueNameIdSupplier().get();

    props = Lists.reverse(props);
    Node result = IR.name(objName);
    for (Node propdef : props) {
      if (propdef.isComputedProp()) {
        Node propertyExpression = propdef.removeFirstChild();
        Node value = propdef.removeFirstChild();
        result =
            IR.comma(IR.assign(IR.getelem(IR.name(objName), propertyExpression), value), result);
      } else {
        if (!propdef.hasChildren()) {
          Node name = IR.name(propdef.getString()).useSourceInfoIfMissingFrom(propdef);
          propdef.addChildToBack(name);
        }
        Node val = propdef.removeFirstChild();
        propdef.setType(Token.STRING);
        int type = propdef.isQuotedString() ? Token.GETELEM : Token.GETPROP;
        Node access = new Node(type, IR.name(objName), propdef);
        result = IR.comma(IR.assign(access, val), result);
      }
    }

    Node statement = obj;
    while (!NodeUtil.isStatement(statement)) {
      statement = statement.getParent();
    }

    result.useSourceInfoIfMissingFromForTree(obj);
    parent.replaceChild(obj, result);

    Node var = IR.var(IR.name(objName), obj);
    var.useSourceInfoIfMissingFromForTree(statement);
    statement.getParent().addChildBefore(var, statement);
    compiler.reportCodeChange();
  }

  /**
   * Classes are processed in 3 phases:
   *
   * <ol>
   *   <li>The class name is extracted.
   *   <li>Class members are processed and rewritten.
   *   <li>The constructor is built.
   * </ol>
   */
  private void visitClass(Node classNode, Node parent) {
    checkClassReassignment(classNode);
    // Collect Metadata
    ClassDeclarationMetadata metadata = ClassDeclarationMetadata.create(classNode, parent);

    if (metadata == null || metadata.fullClassName == null) {
      cannotConvert(
          parent,
          "Can only convert classes that are declarations or the right hand"
              + " side of a simple assignment.");
      return;
    }
    if (metadata.hasSuperClass() && !metadata.superClassNameNode.isQualifiedName()) {
      compiler.report(JSError.make(metadata.superClassNameNode, DYNAMIC_EXTENDS_TYPE));
      return;
    }

    boolean useUnique = NodeUtil.isStatement(classNode) && !NodeUtil.isInFunction(classNode);
    String uniqueFullClassName =
        useUnique ? getUniqueClassName(metadata.fullClassName) : metadata.fullClassName;
    Node classNameAccess = NodeUtil.newQName(compiler, uniqueFullClassName);
    Node prototypeAccess = NodeUtil.newPropertyAccess(compiler, classNameAccess, "prototype");

    Preconditions.checkState(
        NodeUtil.isStatement(metadata.insertionPoint),
        "insertion point must be a statement: %s",
        metadata.insertionPoint);

    Node constructor = null;
    JSDocInfo ctorJSDocInfo = null;
    // Process all members of the class
    Node classMembers = classNode.getLastChild();
    Map<String, JSDocInfo> prototypeMembersToDeclare = new LinkedHashMap<>();
    Map<String, JSDocInfo> classMembersToDeclare = new LinkedHashMap<>();
    for (Node member : classMembers.children()) {
      if (member.isEmpty()) {
        continue;
      }
      Preconditions.checkState(
          member.isMemberFunctionDef()
              || member.isGetterDef()
              || member.isSetterDef()
              || (member.isComputedProp() && !member.getBooleanProp(Node.COMPUTED_PROP_VARIABLE)),
          "Member variables should have been transpiled earlier: ",
          member);

      if (member.isGetterDef() || member.isSetterDef()) {
        JSTypeExpression typeExpr = getTypeFromGetterOrSetter(member).clone();
        addToDefinePropertiesObject(metadata, member);

        Map<String, JSDocInfo> membersToDeclare =
            member.isStaticMember() ? classMembersToDeclare : prototypeMembersToDeclare;
        JSDocInfo existingJSDoc = membersToDeclare.get(member.getString());
        JSTypeExpression existingType = existingJSDoc == null ? null : existingJSDoc.getType();
        if (existingType != null && !existingType.equals(typeExpr)) {
          compiler.report(JSError.make(member, CONFLICTING_GETTER_SETTER_TYPE, member.getString()));
        } else {
          JSDocInfoBuilder jsDoc = new JSDocInfoBuilder(false);
          jsDoc.recordType(typeExpr);
          if (member.getJSDocInfo() != null && member.getJSDocInfo().isExport()) {
            jsDoc.recordExport();
          }
          if (member.isStaticMember()) {
            jsDoc.recordNoCollapse();
          }
          membersToDeclare.put(member.getString(), jsDoc.build());
        }
      } else if (member.isMemberFunctionDef() && member.getString().equals("constructor")) {
        ctorJSDocInfo = member.getJSDocInfo();
        constructor = member.getFirstChild().detachFromParent();
        if (!metadata.anonymous) {
          // Turns class Foo { constructor: function() {} } into function Foo() {},
          // i.e. attaches the name the ctor function.
          constructor.replaceChild(constructor.getFirstChild(), metadata.classNameNode.cloneNode());
        }
      } else {
        Node qualifiedMemberAccess =
            getQualifiedMemberAccess(compiler, member, classNameAccess, prototypeAccess);
        Node method = member.getLastChild().detachFromParent();

        Node assign = IR.assign(qualifiedMemberAccess, method);
        assign.useSourceInfoIfMissingFromForTree(member);

        JSDocInfo info = member.getJSDocInfo();
        if (member.isStaticMember() && NodeUtil.referencesThis(assign.getLastChild())) {
          JSDocInfoBuilder memberDoc = JSDocInfoBuilder.maybeCopyFrom(info);
          memberDoc.recordThisType(
              new JSTypeExpression(
                  new Node(Token.BANG, new Node(Token.QMARK)), member.getSourceFileName()));
          info = memberDoc.build();
        }
        if (info != null) {
          assign.setJSDocInfo(info);
        }

        Node newNode = NodeUtil.newExpr(assign);
        metadata.insertNodeAndAdvance(newNode);
      }
    }

    // Add declarations for properties that were defined with a getter and/or setter,
    // so that the typechecker knows those properties exist on the class.
    // This is a temporary solution. Eventually, the type checker should understand
    // Object.defineProperties calls directly.
    for (Map.Entry<String, JSDocInfo> entry : prototypeMembersToDeclare.entrySet()) {
      String declaredMember = entry.getKey();
      Node declaration = IR.getprop(prototypeAccess.cloneTree(), IR.string(declaredMember));
      declaration.setJSDocInfo(entry.getValue());
      metadata.insertNodeAndAdvance(
          IR.exprResult(declaration).useSourceInfoIfMissingFromForTree(classNode));
    }
    for (Map.Entry<String, JSDocInfo> entry : classMembersToDeclare.entrySet()) {
      String declaredMember = entry.getKey();
      Node declaration = IR.getprop(classNameAccess.cloneTree(), IR.string(declaredMember));
      declaration.setJSDocInfo(entry.getValue());
      metadata.insertNodeAndAdvance(
          IR.exprResult(declaration).useSourceInfoIfMissingFromForTree(classNode));
    }

    if (metadata.definePropertiesObjForPrototype.hasChildren()) {
      Node definePropsCall =
          IR.exprResult(
              IR.call(
                  NodeUtil.newQName(compiler, "Object.defineProperties"),
                  prototypeAccess.cloneTree(),
                  metadata.definePropertiesObjForPrototype));
      definePropsCall.useSourceInfoIfMissingFromForTree(classNode);
      metadata.insertNodeAndAdvance(definePropsCall);
    }

    if (metadata.definePropertiesObjForClass.hasChildren()) {
      Node definePropsCall =
          IR.exprResult(
              IR.call(
                  NodeUtil.newQName(compiler, "Object.defineProperties"),
                  classNameAccess.cloneTree(),
                  metadata.definePropertiesObjForClass));
      definePropsCall.useSourceInfoIfMissingFromForTree(classNode);
      metadata.insertNodeAndAdvance(definePropsCall);
    }

    Preconditions.checkNotNull(constructor);

    JSDocInfo classJSDoc = NodeUtil.getBestJSDocInfo(classNode);
    JSDocInfoBuilder newInfo = JSDocInfoBuilder.maybeCopyFrom(classJSDoc);

    newInfo.recordConstructor();

    if (metadata.hasSuperClass()) {
      String superClassString = metadata.superClassNameNode.getQualifiedName();
      if (newInfo.isInterfaceRecorded()) {
        newInfo.recordExtendedInterface(
            new JSTypeExpression(
                new Node(Token.BANG, IR.string(superClassString)),
                metadata.superClassNameNode.getSourceFileName()));
      } else {
        Node inherits =
            IR.call(
                NodeUtil.newQName(compiler, INHERITS),
                NodeUtil.newQName(compiler, metadata.fullClassName),
                NodeUtil.newQName(compiler, superClassString));
        Node inheritsCall = IR.exprResult(inherits);
        compiler.needsEs6Runtime = true;

        inheritsCall.useSourceInfoIfMissingFromForTree(classNode);
        Node enclosingStatement = NodeUtil.getEnclosingStatement(classNode);
        enclosingStatement.getParent().addChildAfter(inheritsCall, enclosingStatement);
        newInfo.recordBaseType(
            new JSTypeExpression(
                new Node(Token.BANG, IR.string(superClassString)),
                metadata.superClassNameNode.getSourceFileName()));
      }
    }

    // Classes are @struct by default.
    if (!newInfo.isUnrestrictedRecorded()
        && !newInfo.isDictRecorded()
        && !newInfo.isStructRecorded()) {
      newInfo.recordStruct();
    }

    if (ctorJSDocInfo != null) {
      newInfo.recordSuppressions(ctorJSDocInfo.getSuppressions());
      for (String param : ctorJSDocInfo.getParameterNames()) {
        newInfo.recordParameter(param, ctorJSDocInfo.getParameterType(param));
      }
      newInfo.mergePropertyBitfieldFrom(ctorJSDocInfo);
    }

    if (NodeUtil.isStatement(classNode)) {
      constructor.getFirstChild().setString("");
      Node ctorVar = IR.let(metadata.classNameNode.cloneNode(), constructor);
      ctorVar.useSourceInfoIfMissingFromForTree(classNode);
      parent.replaceChild(classNode, ctorVar);
    } else {
      parent.replaceChild(classNode, constructor);
    }

    if (NodeUtil.isStatement(constructor)) {
      constructor.setJSDocInfo(newInfo.build());
    } else if (parent.isName()) {
      // The constructor function is the RHS of a var statement.
      // Add the JSDoc to the VAR node.
      Node var = parent.getParent();
      var.setJSDocInfo(newInfo.build());
    } else if (constructor.getParent().isName()) {
      // Is a newly created VAR node.
      Node var = constructor.getParent().getParent();
      var.setJSDocInfo(newInfo.build());
    } else if (parent.isAssign()) {
      // The constructor function is the RHS of an assignment.
      // Add the JSDoc to the ASSIGN node.
      parent.setJSDocInfo(newInfo.build());
    } else {
      throw new IllegalStateException("Unexpected parent node " + parent);
    }

    compiler.reportCodeChange();
  }

  /** @param node A getter or setter node. */
  private JSTypeExpression getTypeFromGetterOrSetter(Node node) {
    JSDocInfo info = node.getJSDocInfo();

    if (info != null) {
      if (node.isGetterDef() && info.getReturnType() != null) {
        return info.getReturnType();
      } else {
        Set<String> paramNames = info.getParameterNames();
        if (paramNames.size() == 1) {
          return info.getParameterType(Iterables.getOnlyElement(info.getParameterNames()));
        }
      }
    }

    return new JSTypeExpression(new Node(Token.QMARK), node.getSourceFileName());
  }

  private void addToDefinePropertiesObject(ClassDeclarationMetadata metadata, Node member) {
    Node obj =
        member.isStaticMember()
            ? metadata.definePropertiesObjForClass
            : metadata.definePropertiesObjForPrototype;
    Node prop = NodeUtil.getFirstPropMatchingKey(obj, member.getString());
    if (prop == null) {
      prop =
          IR.objectlit(
              IR.stringKey("configurable", IR.trueNode()),
              IR.stringKey("enumerable", IR.trueNode()));
      obj.addChildToBack(IR.stringKey(member.getString(), prop));
    }

    Node function = member.getLastChild();
    JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(NodeUtil.getBestJSDocInfo(function));

    info.recordThisType(
        new JSTypeExpression(
            new Node(Token.BANG, IR.string(metadata.fullClassName)), member.getSourceFileName()));
    Node stringKey =
        IR.stringKey(member.isGetterDef() ? "get" : "set", function.detachFromParent());
    stringKey.setJSDocInfo(info.build());
    prop.addChildToBack(stringKey);
    prop.useSourceInfoIfMissingFromForTree(member);
  }

  /**
   * Constructs a Node that represents an access to the given class member, qualified by either the
   * static or the instance access context, depending on whether the member is static.
   *
   * <p><b>WARNING:</b> {@code member} may be modified/destroyed by this method, do not use it
   * afterwards.
   */
  static Node getQualifiedMemberAccess(
      AbstractCompiler compiler, Node member, Node staticAccess, Node instanceAccess) {
    Node context = member.isStaticMember() ? staticAccess : instanceAccess;
    context = context.cloneTree();
    if (member.isComputedProp()) {
      return IR.getelem(context, member.removeFirstChild());
    } else {
      return NodeUtil.newPropertyAccess(compiler, context, member.getString());
    }
  }

  private static String getUniqueClassName(String qualifiedName) {
    return qualifiedName;
  }

  private class CheckClassAssignments extends NodeTraversal.AbstractPostOrderCallback {
    private Node className;

    public CheckClassAssignments(Node className) {
      this.className = className;
    }

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (!n.isAssign() || n.getFirstChild() == className) {
        return;
      }
      if (className.matchesQualifiedName(n.getFirstChild())) {
        compiler.report(JSError.make(n, CLASS_REASSIGNMENT));
      }
    }
  }

  private void cannotConvert(Node n, String message) {
    compiler.report(JSError.make(n, CANNOT_CONVERT, message));
  }

  /**
   * Warns the user that the given ES6 feature cannot be converted to ES3 because the transpilation
   * is not yet implemented. A call to this method is essentially a "TODO(tbreisacher): Implement
   * {@code feature}" comment.
   */
  private void cannotConvertYet(Node n, String feature) {
    compiler.report(JSError.make(n, CANNOT_CONVERT_YET, feature));
  }

  /**
   * Represents static metadata on a class declaration expression - i.e. the qualified name that a
   * class declares (directly or by assignment), whether it's anonymous, and where transpiled code
   * should be inserted (i.e. which object will hold the prototype after transpilation).
   */
  static class ClassDeclarationMetadata {
    /** A statement node. Transpiled methods etc of the class are inserted after this node. */
    private Node insertionPoint;

    /**
     * An object literal node that will be used in a call to Object.defineProperties, to add getters
     * and setters to the prototype.
     */
    private final Node definePropertiesObjForPrototype;

    /**
     * An object literal node that will be used in a call to Object.defineProperties, to add getters
     * and setters to the class.
     */
    private final Node definePropertiesObjForClass;

    /**
     * The fully qualified name of the class, which will be used in the output. May come from the
     * class itself or the LHS of an assignment.
     */
    final String fullClassName;
    /** Whether the constructor function in the output should be anonymous. */
    final boolean anonymous;

    final Node classNameNode;
    final Node superClassNameNode;

    private ClassDeclarationMetadata(
        Node insertionPoint,
        String fullClassName,
        boolean anonymous,
        Node classNameNode,
        Node superClassNameNode) {
      this.insertionPoint = insertionPoint;
      this.definePropertiesObjForClass = IR.objectlit();
      this.definePropertiesObjForPrototype = IR.objectlit();
      this.fullClassName = fullClassName;
      this.anonymous = anonymous;
      this.classNameNode = classNameNode;
      this.superClassNameNode = superClassNameNode;
    }

    static ClassDeclarationMetadata create(Node classNode, Node parent) {
      Node classNameNode = classNode.getFirstChild();
      Node superClassNameNode = classNameNode.getNext();

      // If this is a class statement, or a class expression in a simple
      // assignment or var statement, convert it. In any other case, the
      // code is too dynamic, so return null.
      if (NodeUtil.isStatement(classNode)) {
        return new ClassDeclarationMetadata(
            classNode, classNameNode.getString(), false, classNameNode, superClassNameNode);
      } else if (parent.isAssign() && parent.getParent().isExprResult()) {
        // Add members after the EXPR_RESULT node:
        // example.C = class {}; example.C.prototype.foo = function() {};
        String fullClassName = parent.getFirstChild().getQualifiedName();
        if (fullClassName == null) {
          return null;
        }
        return new ClassDeclarationMetadata(
            parent.getParent(), fullClassName, true, classNameNode, superClassNameNode);
      } else if (parent.isName()) {
        // Add members after the 'var' statement.
        // var C = class {}; C.prototype.foo = function() {};
        return new ClassDeclarationMetadata(
            parent.getParent(), parent.getString(), true, classNameNode, superClassNameNode);
      } else {
        // Cannot handle this class declaration.
        return null;
      }
    }

    void insertNodeAndAdvance(Node newNode) {
      insertionPoint.getParent().addChildAfter(newNode, insertionPoint);
      insertionPoint = newNode;
    }

    boolean hasSuperClass() {
      return !superClassNameNode.isEmpty();
    }
  }
}
/**
 * Flattens global objects/namespaces by replacing each '.' with '$' in their names. This reduces
 * the number of property lookups the browser has to do and allows the {@link RenameVars} pass to
 * shorten namespaced names. For example, goog.events.handleEvent() -> goog$events$handleEvent() ->
 * Za().
 *
 * <p>If a global object's name is assigned to more than once, or if a property is added to the
 * global object in a complex expression, then none of its properties will be collapsed (for
 * safety/correctness).
 *
 * <p>If, after a global object is declared, it is never referenced except when its properties are
 * read or set, then the object will be removed after its properties have been collapsed.
 *
 * <p>Uninitialized variable stubs are created at a global object's declaration site for any of its
 * properties that are added late in a local scope.
 *
 * <p>Static properties of constructors are always collapsed, unsafely! For other objects: if, after
 * an object is declared, it is referenced directly in a way that might create an alias for it, then
 * none of its properties will be collapsed. This behavior is a safeguard to prevent the values
 * associated with the flattened names from getting out of sync with the object's actual property
 * values. For example, in the following case, an alias a$b, if created, could easily keep the value
 * 0 even after a.b became 5: <code> a = {b: 0}; c = a; c.b = 5; </code>.
 *
 * <p>This pass doesn't flatten property accesses of the form: a[b].
 *
 * <p>For lots of examples, see the unit test.
 */
class CollapseProperties implements CompilerPass {

  // Warnings
  static final DiagnosticType UNSAFE_NAMESPACE_WARNING =
      DiagnosticType.warning("JSC_UNSAFE_NAMESPACE", "incomplete alias created for namespace {0}");

  static final DiagnosticType NAMESPACE_REDEFINED_WARNING =
      DiagnosticType.warning("JSC_NAMESPACE_REDEFINED", "namespace {0} should not be redefined");

  static final DiagnosticType UNSAFE_THIS =
      DiagnosticType.warning("JSC_UNSAFE_THIS", "dangerous use of 'this' in static method {0}");

  static final DiagnosticType UNSAFE_CTOR_ALIASING =
      DiagnosticType.warning(
          "JSC_UNSAFE_CTOR_ALIASING",
          "Variable {0} aliases a constructor, " + "so it cannot be assigned multiple times");

  private AbstractCompiler compiler;

  /** Global namespace tree */
  private List<Name> globalNames;

  /** Maps names (e.g. "a.b.c") to nodes in the global namespace tree */
  private Map<String, Name> nameMap;

  private final boolean inlineAliases;

  /**
   * @param inlineAliases Whether we're allowed to inline local aliases of namespaces, etc. It's set
   *     to false only by the deprecated property- renaming policies {@code HEURISTIC} and {@code
   *     AGGRESSIVE_HEURISTIC}.
   */
  CollapseProperties(AbstractCompiler compiler, boolean inlineAliases) {
    this.compiler = compiler;
    this.inlineAliases = inlineAliases;
  }

  @Override
  public void process(Node externs, Node root) {
    GlobalNamespace namespace;
    namespace = new GlobalNamespace(compiler, root);

    if (inlineAliases) {
      inlineAliases(namespace);
    }
    nameMap = namespace.getNameIndex();
    globalNames = namespace.getNameForest();
    checkNamespaces();

    for (Name name : globalNames) {
      flattenReferencesToCollapsibleDescendantNames(name, name.getBaseName());
    }

    // We collapse property definitions after collapsing property references
    // because this step can alter the parse tree above property references,
    // invalidating the node ancestry stored with each reference.
    for (Name name : globalNames) {
      collapseDeclarationOfNameAndDescendants(name, name.getBaseName());
    }
  }

  /**
   * For each qualified name N in the global scope, we check if: (a) No ancestor of N is ever
   * aliased or assigned an unknown value type. (If N = "a.b.c", "a" and "a.b" are never aliased).
   * (b) N has exactly one write, and it lives in the global scope. (c) N is aliased in a local
   * scope. (d) N is aliased in global scope
   *
   * <p>If (a) is true, then GlobalNamespace must know all the writes to N. If (a) and (b) are true,
   * then N cannot change during the execution of a local scope. If (a) and (b) and (c) are true,
   * then the alias can be inlined if the alias obeys the usual rules for how we decide whether a
   * variable is inlineable. If (a) and (b) and (d) are true, then inline the alias if possible (if
   * it is assigned exactly once unconditionally).
   *
   * @see InlineVariables
   */
  private void inlineAliases(GlobalNamespace namespace) {
    // Invariant: All the names in the worklist meet condition (a).
    Deque<Name> workList = new ArrayDeque<>(namespace.getNameForest());

    while (!workList.isEmpty()) {
      Name name = workList.pop();

      // Don't attempt to inline a getter or setter property as a variable.
      if (name.type == Name.Type.GET || name.type == Name.Type.SET) {
        continue;
      }

      if (!name.inExterns && name.globalSets == 1 && name.localSets == 0 && name.aliasingGets > 0) {
        // {@code name} meets condition (b). Find all of its local aliases
        // and try to inline them.
        List<Ref> refs = new ArrayList<>(name.getRefs());
        for (Ref ref : refs) {
          if (ref.type == Type.ALIASING_GET && ref.scope.isLocal()) {
            // {@code name} meets condition (c). Try to inline it.
            // TODO(johnlenz): consider picking up new aliases at the end
            // of the pass instead of immediately like we do for global
            // inlines.
            if (inlineAliasIfPossible(name, ref, namespace)) {
              name.removeRef(ref);
            }
          } else if (ref.type == Type.ALIASING_GET
              && ref.scope.isGlobal()
              && ref.getTwin() == null) { // ignore aliases in chained assignments
            if (inlineGlobalAliasIfPossible(name, ref, namespace)) {
              name.removeRef(ref);
            }
          }
        }
      }

      // Check if {@code name} has any aliases left after the
      // local-alias-inlining above.
      if ((name.type == Name.Type.OBJECTLIT || name.type == Name.Type.FUNCTION)
          && name.aliasingGets == 0
          && name.props != null) {
        // All of {@code name}'s children meet condition (a), so they can be
        // added to the worklist.
        workList.addAll(name.props);
      }
    }
  }

  /**
   * Attempt to inline an global alias of a global name. This requires that the name is well
   * defined: assigned unconditionally, assigned exactly once. It is assumed that, the name for
   * which it is an alias must already meet these same requirements.
   *
   * @param alias The alias to inline
   * @return Whether the alias was inlined.
   */
  private boolean inlineGlobalAliasIfPossible(Name name, Ref alias, GlobalNamespace namespace) {
    // Ensure that the alias is assigned to global name at that the
    // declaration.
    Node aliasParent = alias.node.getParent();
    if (aliasParent.isAssign() && NodeUtil.isExecutedExactlyOnce(aliasParent)
        // We special-case for constructors here, to inline constructor aliases
        // more aggressively in global scope.
        // We do this because constructor properties are always collapsed,
        // so we want to inline the aliases also to avoid breakages.
        || aliasParent.isName() && name.isConstructor()) {
      Node lvalue = aliasParent.isName() ? aliasParent : aliasParent.getFirstChild();
      if (!lvalue.isQualifiedName()) {
        return false;
      }
      name = namespace.getSlot(lvalue.getQualifiedName());
      if (name != null && name.isInlinableGlobalAlias()) {
        Set<AstChange> newNodes = new LinkedHashSet<>();

        List<Ref> refs = new ArrayList<>(name.getRefs());
        for (Ref ref : refs) {
          switch (ref.type) {
            case SET_FROM_GLOBAL:
              continue;
            case DIRECT_GET:
            case ALIASING_GET:
              Node newNode = alias.node.cloneTree();
              Node node = ref.node;
              node.getParent().replaceChild(node, newNode);
              newNodes.add(new AstChange(ref.module, ref.scope, newNode));
              name.removeRef(ref);
              break;
            default:
              throw new IllegalStateException();
          }
        }

        rewriteAliasProps(name, alias.node, 0, newNodes);

        // just set the original alias to null.
        aliasParent.replaceChild(alias.node, IR.nullNode());
        compiler.reportCodeChange();

        // Inlining the variable may have introduced new references
        // to descendants of {@code name}. So those need to be collected now.
        namespace.scanNewNodes(newNodes);

        return true;
      }
    }
    return false;
  }

  /**
   * @param name The Name whose properties references should be updated.
   * @param value The value to use when rewriting.
   * @param depth The chain depth.
   * @param newNodes Expression nodes that have been updated.
   */
  private static void rewriteAliasProps(Name name, Node value, int depth, Set<AstChange> newNodes) {
    if (name.props == null) {
      return;
    }
    Preconditions.checkState(!value.matchesQualifiedName(name.getFullName()));
    for (Name prop : name.props) {
      rewriteAliasProps(prop, value, depth + 1, newNodes);
      List<Ref> refs = new ArrayList<>(prop.getRefs());
      for (Ref ref : refs) {
        Node target = ref.node;
        for (int i = 0; i <= depth; i++) {
          if (target.isGetProp()) {
            target = target.getFirstChild();
          } else if (NodeUtil.isObjectLitKey(target)) {
            // Object literal key definitions are a little trickier, as we
            // need to find the assignment target
            Node gparent = target.getParent().getParent();
            if (gparent.isAssign()) {
              target = gparent.getFirstChild();
            } else {
              Preconditions.checkState(NodeUtil.isObjectLitKey(gparent));
              target = gparent;
            }
          } else {
            throw new IllegalStateException("unexpected: " + target);
          }
        }
        Preconditions.checkState(target.isGetProp() || target.isName());
        target.getParent().replaceChild(target, value.cloneTree());
        prop.removeRef(ref);
        // Rescan the expression root.
        newNodes.add(new AstChange(ref.module, ref.scope, ref.node));
      }
    }
  }

  private boolean inlineAliasIfPossible(Name name, Ref alias, GlobalNamespace namespace) {
    // Ensure that the alias is assigned to a local variable at that
    // variable's declaration. If the alias's parent is a NAME,
    // then the NAME must be the child of a VAR node, and we must
    // be in a VAR assignment.
    Node aliasParent = alias.node.getParent();
    if (aliasParent.isName()) {
      // Ensure that the local variable is well defined and never reassigned.
      Scope scope = alias.scope;
      String aliasVarName = aliasParent.getString();
      Var aliasVar = scope.getVar(aliasVarName);

      ReferenceCollectingCallback collector =
          new ReferenceCollectingCallback(
              compiler,
              ReferenceCollectingCallback.DO_NOTHING_BEHAVIOR,
              Predicates.equalTo(aliasVar));
      collector.processScope(scope);

      ReferenceCollection aliasRefs = collector.getReferences(aliasVar);
      Set<AstChange> newNodes = new LinkedHashSet<>();

      if (aliasRefs.isWellDefined() && aliasRefs.firstReferenceIsAssigningDeclaration()) {
        if (!aliasRefs.isAssignedOnceInLifetime()) {
          // Static properties of constructors are always collapsed.
          // So, if a constructor is aliased and its properties are accessed from
          // the alias, we would like to inline the alias here to access the
          // properties correctly.
          // But if the aliased variable is assigned more than once, we can't
          // inline, so we warn.
          if (name.isConstructor()) {
            boolean accessPropsAfterAliasing = false;
            for (Reference ref : aliasRefs.references) {
              if (ref.getNode().getParent().isGetProp()) {
                accessPropsAfterAliasing = true;
                break;
              }
            }
            if (accessPropsAfterAliasing) {
              compiler.report(JSError.make(aliasParent, UNSAFE_CTOR_ALIASING, aliasVarName));
            }
          }
          return false;
        }

        // The alias is well-formed, so do the inlining now.
        int size = aliasRefs.references.size();
        for (int i = 1; i < size; i++) {
          ReferenceCollectingCallback.Reference aliasRef = aliasRefs.references.get(i);

          Node newNode = alias.node.cloneTree();
          aliasRef.getParent().replaceChild(aliasRef.getNode(), newNode);
          newNodes.add(new AstChange(getRefModule(aliasRef), aliasRef.getScope(), newNode));
        }

        // just set the original alias to null.
        aliasParent.replaceChild(alias.node, IR.nullNode());
        compiler.reportCodeChange();

        // Inlining the variable may have introduced new references
        // to descendants of {@code name}. So those need to be collected now.
        namespace.scanNewNodes(newNodes);
        return true;
      }
    }

    return false;
  }

  JSModule getRefModule(ReferenceCollectingCallback.Reference ref) {
    CompilerInput input = compiler.getInput(ref.getInputId());
    return input == null ? null : input.getModule();
  }

  /**
   * Runs through all namespaces (prefixes of classes and enums), and checks if any of them have
   * been used in an unsafe way.
   */
  private void checkNamespaces() {
    for (Name name : nameMap.values()) {
      if (name.isNamespaceObjectLit()
          && (name.aliasingGets > 0
              || name.localSets + name.globalSets > 1
              || name.deleteProps > 0)) {
        boolean initialized = name.getDeclaration() != null;
        for (Ref ref : name.getRefs()) {
          if (ref == name.getDeclaration()) {
            continue;
          }

          if (ref.type == Ref.Type.DELETE_PROP) {
            if (initialized) {
              warnAboutNamespaceRedefinition(name, ref);
            }
          } else if (ref.type == Ref.Type.SET_FROM_GLOBAL || ref.type == Ref.Type.SET_FROM_LOCAL) {
            if (initialized && !isSafeNamespaceReinit(ref)) {
              warnAboutNamespaceRedefinition(name, ref);
            }

            initialized = true;
          } else if (ref.type == Ref.Type.ALIASING_GET) {
            warnAboutNamespaceAliasing(name, ref);
          }
        }
      }
    }
  }

  private boolean isSafeNamespaceReinit(Ref ref) {
    // allow "a = a || {}" or "var a = a || {}"
    Node valParent = getValueParent(ref);
    Node val = valParent.getLastChild();
    if (val.getType() == Token.OR) {
      Node maybeName = val.getFirstChild();
      if (ref.node.matchesQualifiedName(maybeName)) {
        return true;
      }
    }

    return false;
  }

  /**
   * Gets the parent node of the value for any assignment to a Name. For example, in the assignment
   * {@code var x = 3;} the parent would be the NAME node.
   */
  private static Node getValueParent(Ref ref) {
    // there are two types of declarations: VARs and ASSIGNs
    return (ref.node.getParent() != null && ref.node.getParent().isVar())
        ? ref.node
        : ref.node.getParent();
  }

  /**
   * Reports a warning because a namespace was aliased.
   *
   * @param nameObj A namespace that is being aliased
   * @param ref The reference that forced the alias
   */
  private void warnAboutNamespaceAliasing(Name nameObj, Ref ref) {
    compiler.report(JSError.make(ref.node, UNSAFE_NAMESPACE_WARNING, nameObj.getFullName()));
  }

  /**
   * Reports a warning because a namespace was redefined.
   *
   * @param nameObj A namespace that is being redefined
   * @param ref The reference that set the namespace
   */
  private void warnAboutNamespaceRedefinition(Name nameObj, Ref ref) {
    compiler.report(JSError.make(ref.node, NAMESPACE_REDEFINED_WARNING, nameObj.getFullName()));
  }

  /**
   * Flattens all references to collapsible properties of a global name except their initial
   * definitions. Recurs on subnames.
   *
   * @param n An object representing a global name
   * @param alias The flattened name for {@code n}
   */
  private void flattenReferencesToCollapsibleDescendantNames(Name n, String alias) {
    if (n.props == null || n.isCollapsingExplicitlyDenied()) {
      return;
    }

    for (Name p : n.props) {
      String propAlias = appendPropForAlias(alias, p.getBaseName());

      if (p.canCollapse()) {
        flattenReferencesTo(p, propAlias);
      } else if (p.isSimpleStubDeclaration() && !p.isCollapsingExplicitlyDenied()) {
        flattenSimpleStubDeclaration(p, propAlias);
      }

      flattenReferencesToCollapsibleDescendantNames(p, propAlias);
    }
  }

  /** Flattens a stub declaration. This is mostly a hack to support legacy users. */
  private void flattenSimpleStubDeclaration(Name name, String alias) {
    Ref ref = Iterables.getOnlyElement(name.getRefs());
    Node nameNode = NodeUtil.newName(compiler, alias, ref.node, name.getFullName());
    Node varNode = IR.var(nameNode).useSourceInfoIfMissingFrom(nameNode);

    Preconditions.checkState(ref.node.getParent().isExprResult());
    Node parent = ref.node.getParent();
    Node grandparent = parent.getParent();
    grandparent.replaceChild(parent, varNode);
    compiler.reportCodeChange();
  }

  /**
   * Flattens all references to a collapsible property of a global name except its initial
   * definition.
   *
   * @param n A global property name (e.g. "a.b" or "a.b.c.d")
   * @param alias The flattened name (e.g. "a$b" or "a$b$c$d")
   */
  private void flattenReferencesTo(Name n, String alias) {
    String originalName = n.getFullName();
    for (Ref r : n.getRefs()) {
      if (r == n.getDeclaration()) {
        // Declarations are handled separately.
        continue;
      }
      Node rParent = r.node.getParent();
      // There are two cases when we shouldn't flatten a reference:
      // 1) Object literal keys, because duplicate keys show up as refs.
      // 2) References inside a complex assign. (a = x.y = 0). These are
      //    called TWIN references, because they show up twice in the
      //    reference list. Only collapse the set, not the alias.
      if (!NodeUtil.isObjectLitKey(r.node) && (r.getTwin() == null || r.isSet())) {
        flattenNameRef(alias, r.node, rParent, originalName);
      }
    }

    // Flatten all occurrences of a name as a prefix of its subnames. For
    // example, if {@code n} corresponds to the name "a.b", then "a.b" will be
    // replaced with "a$b" in all occurrences of "a.b.c", "a.b.c.d", etc.
    if (n.props != null) {
      for (Name p : n.props) {
        flattenPrefixes(alias, p, 1);
      }
    }
  }

  /**
   * Flattens all occurrences of a name as a prefix of subnames beginning with a particular subname.
   *
   * @param n A global property name (e.g. "a.b.c.d")
   * @param alias A flattened prefix name (e.g. "a$b")
   * @param depth The difference in depth between the property name and the prefix name (e.g. 2)
   */
  private void flattenPrefixes(String alias, Name n, int depth) {
    // Only flatten the prefix of a name declaration if the name being
    // initialized is fully qualified (i.e. not an object literal key).
    String originalName = n.getFullName();
    Ref decl = n.getDeclaration();
    if (decl != null && decl.node != null && decl.node.isGetProp()) {
      flattenNameRefAtDepth(alias, decl.node, depth, originalName);
    }

    for (Ref r : n.getRefs()) {
      if (r == decl) {
        // Declarations are handled separately.
        continue;
      }

      // References inside a complex assign (a = x.y = 0)
      // have twins. We should only flatten one of the twins.
      if (r.getTwin() == null || r.isSet()) {
        flattenNameRefAtDepth(alias, r.node, depth, originalName);
      }
    }

    if (n.props != null) {
      for (Name p : n.props) {
        flattenPrefixes(alias, p, depth + 1);
      }
    }
  }

  /**
   * Flattens a particular prefix of a single name reference.
   *
   * @param alias A flattened prefix name (e.g. "a$b")
   * @param n The node corresponding to a subproperty name (e.g. "a.b.c.d")
   * @param depth The difference in depth between the property name and the prefix name (e.g. 2)
   * @param originalName String version of the property name.
   */
  private void flattenNameRefAtDepth(String alias, Node n, int depth, String originalName) {
    // This method has to work for both GETPROP chains and, in rare cases,
    // OBJLIT keys, possibly nested. That's why we check for children before
    // proceeding. In the OBJLIT case, we don't need to do anything.
    int nType = n.getType();
    boolean isQName = nType == Token.NAME || nType == Token.GETPROP;
    boolean isObjKey = NodeUtil.isObjectLitKey(n);
    Preconditions.checkState(isObjKey || isQName);
    if (isQName) {
      for (int i = 1; i < depth && n.hasChildren(); i++) {
        n = n.getFirstChild();
      }
      if (n.isGetProp() && n.getFirstChild().isGetProp()) {
        flattenNameRef(alias, n.getFirstChild(), n, originalName);
      }
    }
  }

  /**
   * Replaces a GETPROP a.b.c with a NAME a$b$c.
   *
   * @param alias A flattened prefix name (e.g. "a$b")
   * @param n The GETPROP node corresponding to the original name (e.g. "a.b")
   * @param parent {@code n}'s parent
   * @param originalName String version of the property name.
   */
  private void flattenNameRef(String alias, Node n, Node parent, String originalName) {
    Preconditions.checkArgument(
        n.isGetProp(), "Expected GETPROP, found %s. Node: %s", Token.name(n.getType()), n);

    // BEFORE:
    //   getprop
    //     getprop
    //       name a
    //       string b
    //     string c
    // AFTER:
    //   name a$b$c
    Node ref = NodeUtil.newName(compiler, alias, n, originalName);
    NodeUtil.copyNameAnnotations(n.getLastChild(), ref);
    if (parent.isCall() && n == parent.getFirstChild()) {
      // The node was a call target, we are deliberately flatten these as
      // we node the "this" isn't provided by the namespace. Mark it as such:
      parent.putBooleanProp(Node.FREE_CALL, true);
    }

    TypeI type = n.getTypeI();
    if (type != null) {
      ref.setTypeI(type);
    }

    parent.replaceChild(n, ref);
    compiler.reportCodeChange();
  }

  /**
   * Collapses definitions of the collapsible properties of a global name. Recurs on subnames that
   * also represent JavaScript objects with collapsible properties.
   *
   * @param n A node representing a global name
   * @param alias The flattened name for {@code n}
   */
  private void collapseDeclarationOfNameAndDescendants(Name n, String alias) {
    boolean canCollapseChildNames = n.canCollapseUnannotatedChildNames();

    // Handle this name first so that nested object literals get unrolled.
    if (n.canCollapse()) {
      updateObjLitOrFunctionDeclaration(n, alias, canCollapseChildNames);
    }

    if (n.props == null) {
      return;
    }
    for (Name p : n.props) {
      // Recur first so that saved node ancestries are intact when needed.
      collapseDeclarationOfNameAndDescendants(p, appendPropForAlias(alias, p.getBaseName()));
      if (!p.inExterns
          && canCollapseChildNames
          && p.getDeclaration() != null
          && p.canCollapse()
          && p.getDeclaration().node != null
          && p.getDeclaration().node.getParent() != null
          && p.getDeclaration().node.getParent().isAssign()) {
        updateSimpleDeclaration(appendPropForAlias(alias, p.getBaseName()), p, p.getDeclaration());
      }
    }
  }

  /**
   * Updates the initial assignment to a collapsible property at global scope by changing it to a
   * variable declaration (e.g. a.b = 1 -> var a$b = 1). The property's value may either be a
   * primitive or an object literal or function whose properties aren't collapsible.
   *
   * @param alias The flattened property name (e.g. "a$b")
   * @param refName The name for the reference being updated.
   * @param ref An object containing information about the assignment getting updated
   */
  private void updateSimpleDeclaration(String alias, Name refName, Ref ref) {
    Node rvalue = ref.node.getNext();
    Node parent = ref.node.getParent();
    Node grandparent = parent.getParent();
    Node greatGrandparent = grandparent.getParent();

    if (rvalue != null && rvalue.isFunction()) {
      checkForHosedThisReferences(rvalue, refName.docInfo, refName);
    }

    // Create the new alias node.
    Node nameNode =
        NodeUtil.newName(compiler, alias, grandparent.getFirstChild(), refName.getFullName());
    NodeUtil.copyNameAnnotations(ref.node.getLastChild(), nameNode);

    if (grandparent.isExprResult()) {
      // BEFORE: a.b.c = ...;
      //   exprstmt
      //     assign
      //       getprop
      //         getprop
      //           name a
      //           string b
      //         string c
      //       NODE
      // AFTER: var a$b$c = ...;
      //   var
      //     name a$b$c
      //       NODE

      // Remove the r-value (NODE).
      parent.removeChild(rvalue);
      nameNode.addChildToFront(rvalue);

      Node varNode = IR.var(nameNode);
      greatGrandparent.replaceChild(grandparent, varNode);
    } else {
      // This must be a complex assignment.
      Preconditions.checkNotNull(ref.getTwin());

      // BEFORE:
      // ... (x.y = 3);
      //
      // AFTER:
      // var x$y;
      // ... (x$y = 3);

      Node current = grandparent;
      Node currentParent = grandparent.getParent();
      for (;
          !currentParent.isScript() && !currentParent.isBlock();
          current = currentParent, currentParent = currentParent.getParent()) {}

      // Create a stub variable declaration right
      // before the current statement.
      Node stubVar = IR.var(nameNode.cloneTree()).useSourceInfoIfMissingFrom(nameNode);
      currentParent.addChildBefore(stubVar, current);

      parent.replaceChild(ref.node, nameNode);
    }

    compiler.reportCodeChange();
  }

  /**
   * Updates the first initialization (a.k.a "declaration") of a global name. This involves
   * flattening the global name (if it's not just a global variable name already), collapsing object
   * literal keys into global variables, declaring stub global variables for properties added later
   * in a local scope.
   *
   * <p>It may seem odd that this function also takes care of declaring stubs for direct children.
   * The ultimate goal of this function is to eliminate the global name entirely (when possible), so
   * that "middlemen" namespaces disappear, and to do that we need to make sure that all the direct
   * children will be collapsed as well.
   *
   * @param n An object representing a global name (e.g. "a", "a.b.c")
   * @param alias The flattened name for {@code n} (e.g. "a", "a$b$c")
   * @param canCollapseChildNames Whether it's possible to collapse children of this name. (This is
   *     mostly passed for convenience; it's equivalent to n.canCollapseChildNames()).
   */
  private void updateObjLitOrFunctionDeclaration(
      Name n, String alias, boolean canCollapseChildNames) {
    Ref decl = n.getDeclaration();
    if (decl == null) {
      // Some names do not have declarations, because they
      // are only defined in local scopes.
      return;
    }

    if (decl.getTwin() != null) {
      // Twin declarations will get handled when normal references
      // are handled.
      return;
    }

    switch (decl.node.getParent().getType()) {
      case Token.ASSIGN:
        updateObjLitOrFunctionDeclarationAtAssignNode(n, alias, canCollapseChildNames);
        break;
      case Token.VAR:
        updateObjLitOrFunctionDeclarationAtVarNode(n, canCollapseChildNames);
        break;
      case Token.FUNCTION:
        updateFunctionDeclarationAtFunctionNode(n, canCollapseChildNames);
        break;
    }
  }

  /**
   * Updates the first initialization (a.k.a "declaration") of a global name that occurs at an
   * ASSIGN node. See comment for {@link #updateObjLitOrFunctionDeclaration}.
   *
   * @param n An object representing a global name (e.g. "a", "a.b.c")
   * @param alias The flattened name for {@code n} (e.g. "a", "a$b$c")
   */
  private void updateObjLitOrFunctionDeclarationAtAssignNode(
      Name n, String alias, boolean canCollapseChildNames) {
    // NOTE: It's important that we don't add additional nodes
    // (e.g. a var node before the exprstmt) because the exprstmt might be
    // the child of an if statement that's not inside a block).

    Ref ref = n.getDeclaration();
    Node rvalue = ref.node.getNext();
    Node varNode = new Node(Token.VAR);
    Node varParent = ref.node.getAncestor(3);
    Node grandparent = ref.node.getAncestor(2);
    boolean isObjLit = rvalue.isObjectLit();
    boolean insertedVarNode = false;

    if (isObjLit && n.canEliminate()) {
      // Eliminate the object literal altogether.
      varParent.replaceChild(grandparent, varNode);
      ref.node = null;
      insertedVarNode = true;

    } else if (!n.isSimpleName()) {
      // Create a VAR node to declare the name.
      if (rvalue.isFunction()) {
        checkForHosedThisReferences(rvalue, n.docInfo, n);
      }

      ref.node.getParent().removeChild(rvalue);

      Node nameNode = NodeUtil.newName(compiler, alias, ref.node.getAncestor(2), n.getFullName());

      JSDocInfo info = NodeUtil.getBestJSDocInfo(ref.node.getParent());
      if (ref.node.getLastChild().getBooleanProp(Node.IS_CONSTANT_NAME)
          || (info != null && info.isConstant())) {
        nameNode.putBooleanProp(Node.IS_CONSTANT_NAME, true);
      }

      if (info != null) {
        varNode.setJSDocInfo(info);
      }
      varNode.addChildToBack(nameNode);
      nameNode.addChildToFront(rvalue);
      varParent.replaceChild(grandparent, varNode);

      // Update the node ancestry stored in the reference.
      ref.node = nameNode;
      insertedVarNode = true;
    }

    if (canCollapseChildNames) {
      if (isObjLit) {
        declareVarsForObjLitValues(
            n, alias, rvalue, varNode, varParent.getChildBefore(varNode), varParent);
      }

      addStubsForUndeclaredProperties(n, alias, varParent, varNode);
    }

    if (insertedVarNode) {
      if (!varNode.hasChildren()) {
        varParent.removeChild(varNode);
      }
      compiler.reportCodeChange();
    }
  }

  /**
   * Warns about any references to "this" in the given FUNCTION. The function is getting collapsed,
   * so the references will change.
   */
  private void checkForHosedThisReferences(Node function, JSDocInfo docInfo, final Name name) {
    // A function is getting collapsed. Make sure that if it refers to
    // "this", it must be a constructor or documented with @this.
    if (docInfo == null || (!docInfo.isConstructor() && !docInfo.hasThisType())) {
      NodeTraversal.traverseEs6(
          compiler,
          function.getLastChild(),
          new NodeTraversal.AbstractShallowCallback() {
            @Override
            public void visit(NodeTraversal t, Node n, Node parent) {
              if (n.isThis()) {
                compiler.report(JSError.make(n, UNSAFE_THIS, name.getFullName()));
              }
            }
          });
    }
  }

  /**
   * Updates the first initialization (a.k.a "declaration") of a global name that occurs at a VAR
   * node. See comment for {@link #updateObjLitOrFunctionDeclaration}.
   *
   * @param n An object representing a global name (e.g. "a")
   */
  private void updateObjLitOrFunctionDeclarationAtVarNode(Name n, boolean canCollapseChildNames) {
    if (!canCollapseChildNames) {
      return;
    }

    Ref ref = n.getDeclaration();
    String name = ref.node.getString();
    Node rvalue = ref.node.getFirstChild();
    Node varNode = ref.node.getParent();
    Node grandparent = varNode.getParent();

    boolean isObjLit = rvalue.isObjectLit();
    int numChanges = 0;

    if (isObjLit) {
      numChanges +=
          declareVarsForObjLitValues(
              n, name, rvalue, varNode, grandparent.getChildBefore(varNode), grandparent);
    }

    numChanges += addStubsForUndeclaredProperties(n, name, grandparent, varNode);

    if (isObjLit && n.canEliminate()) {
      varNode.removeChild(ref.node);
      if (!varNode.hasChildren()) {
        grandparent.removeChild(varNode);
      }
      numChanges++;

      // Clear out the object reference, since we've eliminated it from the
      // parse tree.
      ref.node = null;
    }

    if (numChanges > 0) {
      compiler.reportCodeChange();
    }
  }

  /**
   * Updates the first initialization (a.k.a "declaration") of a global name that occurs at a
   * FUNCTION node. See comment for {@link #updateObjLitOrFunctionDeclaration}.
   *
   * @param n An object representing a global name (e.g. "a")
   */
  private void updateFunctionDeclarationAtFunctionNode(Name n, boolean canCollapseChildNames) {
    if (!canCollapseChildNames || !n.canCollapse()) {
      return;
    }

    Ref ref = n.getDeclaration();
    String fnName = ref.node.getString();
    addStubsForUndeclaredProperties(n, fnName, ref.node.getAncestor(2), ref.node.getParent());
  }

  /**
   * Declares global variables to serve as aliases for the values in an object literal, optionally
   * removing all of the object literal's keys and values.
   *
   * @param alias The object literal's flattened name (e.g. "a$b$c")
   * @param objlit The OBJLIT node
   * @param varNode The VAR node to which new global variables should be added as children
   * @param nameToAddAfter The child of {@code varNode} after which new variables should be added
   *     (may be null)
   * @param varParent {@code varNode}'s parent
   * @return The number of variables added
   */
  private int declareVarsForObjLitValues(
      Name objlitName,
      String alias,
      Node objlit,
      Node varNode,
      Node nameToAddAfter,
      Node varParent) {
    int numVars = 0;
    int arbitraryNameCounter = 0;
    boolean discardKeys = !objlitName.shouldKeepKeys();

    for (Node key = objlit.getFirstChild(), nextKey; key != null; key = nextKey) {
      Node value = key.getFirstChild();
      nextKey = key.getNext();

      // A get or a set can not be rewritten as a VAR.
      if (key.isGetterDef() || key.isSetterDef()) {
        continue;
      }

      // We generate arbitrary names for keys that aren't valid JavaScript
      // identifiers, since those keys are never referenced. (If they were,
      // this object literal's child names wouldn't be collapsible.) The only
      // reason that we don't eliminate them entirely is the off chance that
      // their values are expressions that have side effects.
      boolean isJsIdentifier = !key.isNumber() && TokenStream.isJSIdentifier(key.getString());
      String propName = isJsIdentifier ? key.getString() : String.valueOf(++arbitraryNameCounter);

      // If the name cannot be collapsed, skip it.
      String qName = objlitName.getFullName() + '.' + propName;
      Name p = nameMap.get(qName);
      if (p != null && !p.canCollapse()) {
        continue;
      }

      String propAlias = appendPropForAlias(alias, propName);
      Node refNode = null;
      if (discardKeys) {
        objlit.removeChild(key);
        value.detachFromParent();
      } else {
        // Substitute a reference for the value.
        refNode = IR.name(propAlias);
        if (key.getBooleanProp(Node.IS_CONSTANT_NAME)) {
          refNode.putBooleanProp(Node.IS_CONSTANT_NAME, true);
        }

        key.replaceChild(value, refNode);
      }

      // Declare the collapsed name as a variable with the original value.
      Node nameNode = IR.name(propAlias);
      nameNode.addChildToFront(value);
      if (key.getBooleanProp(Node.IS_CONSTANT_NAME)) {
        nameNode.putBooleanProp(Node.IS_CONSTANT_NAME, true);
      }
      Node newVar = IR.var(nameNode).useSourceInfoIfMissingFromForTree(key);
      if (nameToAddAfter != null) {
        varParent.addChildAfter(newVar, nameToAddAfter);
      } else {
        varParent.addChildBefore(newVar, varNode);
      }
      compiler.reportCodeChange();
      nameToAddAfter = newVar;

      // Update the global name's node ancestry if it hasn't already been
      // done. (Duplicate keys in an object literal can bring us here twice
      // for the same global name.)
      if (isJsIdentifier && p != null) {
        if (!discardKeys) {
          Ref newAlias = p.getDeclaration().cloneAndReclassify(Ref.Type.ALIASING_GET);
          newAlias.node = refNode;
          p.addRef(newAlias);
        }

        p.getDeclaration().node = nameNode;

        if (value.isFunction()) {
          checkForHosedThisReferences(value, key.getJSDocInfo(), p);
        }
      }

      numVars++;
    }
    return numVars;
  }

  /**
   * Adds global variable "stubs" for any properties of a global name that are only set in a local
   * scope or read but never set.
   *
   * @param n An object representing a global name (e.g. "a", "a.b.c")
   * @param alias The flattened name of the object whose properties we are adding stubs for (e.g.
   *     "a$b$c")
   * @param parent The node to which new global variables should be added as children
   * @param addAfter The child of after which new variables should be added
   * @return The number of variables added
   */
  private int addStubsForUndeclaredProperties(Name n, String alias, Node parent, Node addAfter) {
    Preconditions.checkState(n.canCollapseUnannotatedChildNames());
    Preconditions.checkArgument(NodeUtil.isStatementBlock(parent));
    Preconditions.checkNotNull(addAfter);
    if (n.props == null) {
      return 0;
    }
    int numStubs = 0;
    for (Name p : n.props) {
      if (p.needsToBeStubbed()) {
        String propAlias = appendPropForAlias(alias, p.getBaseName());
        Node nameNode = IR.name(propAlias);
        Node newVar = IR.var(nameNode).useSourceInfoIfMissingFromForTree(addAfter);
        parent.addChildAfter(newVar, addAfter);
        addAfter = newVar;
        numStubs++;
        compiler.reportCodeChange();
        // Determine if this is a constant var by checking the first
        // reference to it. Don't check the declaration, as it might be null.
        if (p.getRefs().get(0).node.getLastChild().getBooleanProp(Node.IS_CONSTANT_NAME)) {
          nameNode.putBooleanProp(Node.IS_CONSTANT_NAME, true);
        }
      }
    }
    return numStubs;
  }

  private static String appendPropForAlias(String root, String prop) {
    if (prop.indexOf('$') != -1) {
      // Encode '$' in a property as '$0'. Because '0' cannot be the
      // start of an identifier, this will never conflict with our
      // encoding from '.' -> '$'.
      prop = prop.replace("$", "$0");
    }
    return root + '$' + prop;
  }
}
/**
 * Rewrites "Polymer({})" calls into a form that is suitable for type checking and dead code
 * elimination. Also ensures proper format and types.
 *
 * <p>Only works with Polymer version: 0.8
 *
 * @author [email protected] (Jeremy Klein)
 */
final class PolymerPass extends AbstractPostOrderCallback implements HotSwapCompilerPass {

  // TODO(jlklein): Switch back to an error when everyone is upgraded to Polymer 1.0
  static final DiagnosticType POLYMER_DESCRIPTOR_NOT_VALID =
      DiagnosticType.warning(
          "JSC_POLYMER_DESCRIPTOR_NOT_VALID",
          "The argument to Polymer() is not an obj lit (perhaps because this is a pre-Polymer-1.0 "
              + "call). Ignoring this call.");

  static final DiagnosticType POLYMER_INVALID_DECLARATION =
      DiagnosticType.error(
          "JSC_POLYMER_INVALID_DECLARAION", "A Polymer() declaration cannot use 'let' or 'const'.");

  // Errors
  static final DiagnosticType POLYMER_MISSING_IS =
      DiagnosticType.error(
          "JSC_POLYMER_MISSING_IS", "The class descriptor must include an 'is' property.");

  static final DiagnosticType POLYMER_UNEXPECTED_PARAMS =
      DiagnosticType.error(
          "JSC_POLYMER_UNEXPECTED_PARAMS", "The class definition has too many arguments.");

  static final DiagnosticType POLYMER_MISSING_EXTERNS =
      DiagnosticType.error("JSC_POLYMER_MISSING_EXTERNS", "Missing Polymer externs.");

  static final DiagnosticType POLYMER_INVALID_PROPERTY =
      DiagnosticType.error(
          "JSC_POLYMER_INVALID_PROPERTY", "Polymer property has an invalid or missing type.");

  static final DiagnosticType POLYMER_INVALID_BEHAVIOR_ARRAY =
      DiagnosticType.error(
          "JSC_POLYMER_INVALID_BEHAVIOR_ARRAY", "The behaviors property must be an array literal.");

  static final DiagnosticType POLYMER_UNQUALIFIED_BEHAVIOR =
      DiagnosticType.error(
          "JSC_POLYMER_UNQUALIFIED_BEHAVIOR",
          "Behaviors must be global, fully qualified names which are declared as object literals or "
              + "array literals of other valid Behaviors.");

  static final DiagnosticType POLYMER_UNANNOTATED_BEHAVIOR =
      DiagnosticType.error(
          "JSC_POLYMER_UNANNOTATED_BEHAVIOR",
          "Behavior declarations must be annotated with @polymerBehavior.");

  static final DiagnosticType POLYMER_SHORTHAND_NOT_SUPPORTED =
      DiagnosticType.error(
          "JSC_POLYMER_SHORTHAND_NOT_SUPPORTED",
          "Shorthand assignment in object literal is not allowed in " + "Polymer call arguments");

  static final String VIRTUAL_FILE = "<PolymerPass.java>";

  private final AbstractCompiler compiler;
  private Node polymerElementExterns;
  private Set<String> nativeExternsAdded;
  private final Map<String, String> tagNameMap;
  private List<Node> polymerElementProps;
  private final ImmutableSet<String> behaviorNamesNotToCopy;
  private GlobalNamespace globalNames;

  public PolymerPass(AbstractCompiler compiler) {
    this.compiler = compiler;
    tagNameMap = TagNameToType.getMap();
    polymerElementProps = new ArrayList<>();
    nativeExternsAdded = new HashSet<>();
    behaviorNamesNotToCopy =
        ImmutableSet.of(
            "created",
            "attached",
            "detached",
            "attributeChanged",
            "configure",
            "ready",
            "properties",
            "listeners",
            "observers",
            "hostAttributes");
  }

  @Override
  public void process(Node externs, Node root) {
    FindPolymerExterns externsCallback = new FindPolymerExterns();
    NodeTraversal.traverse(compiler, externs, externsCallback);
    polymerElementExterns = externsCallback.polymerElementExterns;
    polymerElementProps = externsCallback.getpolymerElementProps();

    if (polymerElementExterns == null) {
      compiler.report(JSError.make(externs, POLYMER_MISSING_EXTERNS));
      return;
    }

    globalNames = new GlobalNamespace(compiler, externs, root);

    hotSwapScript(root, null);
  }

  /** Finds the externs for the PolymerElement base class and all of its properties. */
  private static class FindPolymerExterns extends AbstractPostOrderCallback {
    private Node polymerElementExterns;
    private ImmutableList.Builder<Node> polymerElementProps;
    private static final String POLYMER_ELEMENT_NAME = "PolymerElement";

    public FindPolymerExterns() {
      polymerElementProps = ImmutableList.builder();
    }

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (isPolymerElementExterns(n)) {
        polymerElementExterns = n;
      } else if (isPolymerElementPropExpr(n)) {
        polymerElementProps.add(n);
      }
    }

    /** @return Whether the node is the declaration of PolymerElement. */
    private boolean isPolymerElementExterns(Node value) {
      return value != null
          && value.isVar()
          && value.getFirstChild().matchesQualifiedName(POLYMER_ELEMENT_NAME);
    }

    /**
     * @return Whether the node is an expression result of an assignment to a property of
     *     PolymerElement.
     */
    private boolean isPolymerElementPropExpr(Node value) {
      return value != null
          && value.isExprResult()
          && value.getFirstChild().getFirstChild().isGetProp()
          && NodeUtil.getRootOfQualifiedName(value.getFirstChild().getFirstChild())
              .matchesQualifiedName(POLYMER_ELEMENT_NAME);
    }

    public List<Node> getpolymerElementProps() {
      return polymerElementProps.build();
    }
  }

  /**
   * For every Polymer Behavior, strip property type annotations and add suppress checktypes on
   * functions.
   */
  private static class SuppressBehaviors extends AbstractPostOrderCallback {
    private final AbstractCompiler compiler;

    public SuppressBehaviors(AbstractCompiler compiler) {
      this.compiler = compiler;
    }

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (isBehavior(n)) {
        if (!n.isVar() && !n.isAssign()) {
          compiler.report(JSError.make(n, POLYMER_UNQUALIFIED_BEHAVIOR));
          return;
        }

        // Add @nocollapse.
        JSDocInfoBuilder newDocs = JSDocInfoBuilder.maybeCopyFrom(n.getJSDocInfo());
        newDocs.recordNoCollapse();
        n.setJSDocInfo(newDocs.build());

        Node behaviorValue = n.getChildAtIndex(1);
        if (n.isVar()) {
          behaviorValue = n.getFirstChild().getFirstChild();
        }
        suppressBehavior(behaviorValue);
      }
    }

    /** @return Whether the node is the declaration of a Behavior. */
    private boolean isBehavior(Node value) {
      return value.getJSDocInfo() != null && value.getJSDocInfo().isPolymerBehavior();
    }

    /** Strip property type annotations and add suppress checkTypes and globalThis on functions. */
    private void suppressBehavior(Node behaviorValue) {
      if (behaviorValue == null) {
        compiler.report(JSError.make(behaviorValue, POLYMER_UNQUALIFIED_BEHAVIOR));
        return;
      }

      if (behaviorValue.isArrayLit()) {
        for (Node child : behaviorValue.children()) {
          suppressBehavior(child);
        }
      } else if (behaviorValue.isObjectLit()) {
        stripPropertyTypes(behaviorValue);
        addBehaviorSuppressions(behaviorValue);
      }
    }

    private void stripPropertyTypes(Node behaviorValue) {
      List<MemberDefinition> properties = extractProperties(behaviorValue);
      for (MemberDefinition property : properties) {
        property.name.removeProp(Node.JSDOC_INFO_PROP);
      }
    }

    private void suppressDefaultValues(Node behaviorValue) {
      for (MemberDefinition property : extractProperties(behaviorValue)) {
        if (!property.value.isObjectLit()) {
          continue;
        }

        Node defaultValue = NodeUtil.getFirstPropMatchingKey(property.value, "value");
        if (defaultValue == null || !defaultValue.isFunction()) {
          continue;
        }
        Node defaultValueKey = defaultValue.getParent();
        JSDocInfoBuilder suppressDoc =
            JSDocInfoBuilder.maybeCopyFrom(defaultValueKey.getJSDocInfo());
        suppressDoc.addSuppression("checkTypes");
        suppressDoc.addSuppression("globalThis");
        defaultValueKey.setJSDocInfo(suppressDoc.build());
      }
    }

    private void addBehaviorSuppressions(Node behaviorValue) {
      for (Node keyNode : behaviorValue.children()) {
        if (keyNode.getFirstChild().isFunction()) {
          keyNode.removeProp(Node.JSDOC_INFO_PROP);
          JSDocInfoBuilder suppressDoc = new JSDocInfoBuilder(true);
          suppressDoc.addSuppression("checkTypes");
          suppressDoc.addSuppression("globalThis");
          keyNode.setJSDocInfo(suppressDoc.build());
        }
      }
      suppressDefaultValues(behaviorValue);
    }
  }

  @Override
  public void hotSwapScript(Node scriptRoot, Node originalRoot) {
    NodeTraversal.traverse(compiler, scriptRoot, this);
    SuppressBehaviors suppressBehaviorsCallback = new SuppressBehaviors(compiler);
    NodeTraversal.traverse(compiler, scriptRoot, suppressBehaviorsCallback);
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    if (isPolymerCall(n)) {
      rewriteClassDefinition(n, parent, t);
    }
  }

  private void rewriteClassDefinition(Node n, Node parent, NodeTraversal t) {
    if (parent.getParent().isConst() || parent.getParent().isLet()) {
      compiler.report(JSError.make(n, POLYMER_INVALID_DECLARATION));
      return;
    }
    ClassDefinition def = extractClassDefinition(n);
    if (def != null) {
      if (NodeUtil.isNameDeclaration(parent.getParent()) || parent.isAssign()) {
        rewritePolymerClass(parent.getParent(), def, t);
      } else {
        rewritePolymerClass(parent, def, t);
      }
    }
  }

  private static class MemberDefinition {
    final JSDocInfo info;
    final Node name;
    final Node value;

    MemberDefinition(JSDocInfo info, Node name, Node value) {
      this.info = info;
      this.name = name;
      this.value = value;
    }
  }

  private static final class BehaviorDefinition {
    final List<MemberDefinition> props;
    final List<MemberDefinition> functionsToCopy;
    final List<MemberDefinition> nonPropertyMembersToCopy;
    final boolean isGlobalDeclaration;

    BehaviorDefinition(
        List<MemberDefinition> props,
        List<MemberDefinition> functionsToCopy,
        List<MemberDefinition> nonPropertyMembersToCopy,
        boolean isGlobalDeclaration) {
      this.props = props;
      this.functionsToCopy = functionsToCopy;
      this.nonPropertyMembersToCopy = nonPropertyMembersToCopy;
      this.isGlobalDeclaration = isGlobalDeclaration;
    }
  }

  private static final class ClassDefinition {
    /** The target node (LHS) for the Polymer element definition. */
    final Node target;

    /** The object literal passed to the call to the Polymer() function. */
    final Node descriptor;

    /** The constructor function for the element. */
    final MemberDefinition constructor;

    /** The name of the native HTML element which this element extends. */
    final String nativeBaseElement;

    /** Properties declared in the Polymer "properties" block. */
    final List<MemberDefinition> props;

    /** Flattened list of behavior definitions used by this element. */
    final List<BehaviorDefinition> behaviors;

    ClassDefinition(
        Node target,
        Node descriptor,
        JSDocInfo classInfo,
        MemberDefinition constructor,
        String nativeBaseElement,
        List<MemberDefinition> props,
        List<BehaviorDefinition> behaviors) {
      this.target = target;
      Preconditions.checkState(descriptor.isObjectLit());
      this.descriptor = descriptor;
      this.constructor = constructor;
      this.nativeBaseElement = nativeBaseElement;
      this.props = props;
      this.behaviors = behaviors;
    }
  }

  /**
   * Validates the class definition and if valid, destructively extracts the class definition from
   * the AST.
   */
  private ClassDefinition extractClassDefinition(Node callNode) {
    Node descriptor = NodeUtil.getArgumentForCallOrNew(callNode, 0);
    if (descriptor == null || !descriptor.isObjectLit()) {
      // report bad class definition
      compiler.report(JSError.make(callNode, POLYMER_DESCRIPTOR_NOT_VALID));
      return null;
    }

    int paramCount = callNode.getChildCount() - 1;
    if (paramCount != 1) {
      compiler.report(JSError.make(callNode, POLYMER_UNEXPECTED_PARAMS));
      return null;
    }

    Node elName = NodeUtil.getFirstPropMatchingKey(descriptor, "is");
    if (elName == null) {
      compiler.report(JSError.make(callNode, POLYMER_MISSING_IS));
      return null;
    }

    String elNameString = CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_CAMEL, elName.getString());
    elNameString += "Element";

    Node target;
    if (NodeUtil.isNameDeclaration(callNode.getParent().getParent())) {
      target = IR.name(callNode.getParent().getString());
    } else if (callNode.getParent().isAssign()) {
      target = callNode.getParent().getFirstChild().cloneTree();
    } else {
      target = IR.name(elNameString);
    }

    target.useSourceInfoIfMissingFrom(callNode);
    JSDocInfo classInfo = NodeUtil.getBestJSDocInfo(target);

    JSDocInfo ctorInfo = null;
    Node constructor = NodeUtil.getFirstPropMatchingKey(descriptor, "factoryImpl");
    if (constructor == null) {
      constructor = IR.function(IR.name(""), IR.paramList(), IR.block());
      constructor.useSourceInfoFromForTree(callNode);
    } else {
      ctorInfo = NodeUtil.getBestJSDocInfo(constructor);
    }

    Node baseClass = NodeUtil.getFirstPropMatchingKey(descriptor, "extends");
    String nativeBaseElement = baseClass == null ? null : baseClass.getString();

    Node behaviorArray = NodeUtil.getFirstPropMatchingKey(descriptor, "behaviors");
    List<BehaviorDefinition> behaviors = extractBehaviors(behaviorArray);
    List<MemberDefinition> allProperties = new LinkedList<>();
    for (BehaviorDefinition behavior : behaviors) {
      overwriteMembersIfPresent(allProperties, behavior.props);
    }
    overwriteMembersIfPresent(allProperties, extractProperties(descriptor));

    ClassDefinition def =
        new ClassDefinition(
            target,
            descriptor,
            classInfo,
            new MemberDefinition(ctorInfo, null, constructor),
            nativeBaseElement,
            allProperties,
            behaviors);
    return def;
  }

  /**
   * Appends a list of new MemberDefinitions to the end of a list and removes any previous
   * MemberDefinition in the list which has the same name as the new member.
   */
  private static void overwriteMembersIfPresent(
      List<MemberDefinition> list, List<MemberDefinition> newMembers) {
    for (MemberDefinition newMember : newMembers) {
      for (MemberDefinition member : list) {
        if (member.name.getString().equals(newMember.name.getString())) {
          list.remove(member);
          break;
        }
      }
      list.add(newMember);
    }
  }

  /**
   * Extracts all Behaviors from an array recursively. The array must be an array literal whose
   * value is known at compile-time. Entries in the array can be object literals or array literals
   * (of other behaviors). Behavior names must be global, fully qualified names.
   *
   * @see https://github.com/Polymer/polymer/blob/0.8-preview/PRIMER.md#behaviors
   * @return A list of all {@code BehaviorDefinitions} in the array.
   */
  private List<BehaviorDefinition> extractBehaviors(Node behaviorArray) {
    if (behaviorArray == null) {
      return ImmutableList.of();
    }

    if (!behaviorArray.isArrayLit()) {
      compiler.report(JSError.make(behaviorArray, POLYMER_INVALID_BEHAVIOR_ARRAY));
      return ImmutableList.of();
    }

    ImmutableList.Builder<BehaviorDefinition> behaviors = ImmutableList.builder();
    for (Node behaviorName : behaviorArray.children()) {
      if (behaviorName.isObjectLit()) {
        this.switchDollarSignPropsToBrackets(behaviorName);
        this.quoteListenerAndHostAttributeKeys(behaviorName);
        behaviors.add(
            new BehaviorDefinition(
                extractProperties(behaviorName),
                getBehaviorFunctionsToCopy(behaviorName),
                getNonPropertyMembersToCopy(behaviorName),
                !NodeUtil.isInFunction(behaviorName)));
        continue;
      }

      Name behaviorGlobalName = globalNames.getSlot(behaviorName.getQualifiedName());
      boolean isGlobalDeclaration = true;
      if (behaviorGlobalName == null) {
        compiler.report(JSError.make(behaviorName, POLYMER_UNQUALIFIED_BEHAVIOR));
        continue;
      }

      Ref behaviorDeclaration = behaviorGlobalName.getDeclaration();

      // Use any set as a backup declaration, even if it's local.
      if (behaviorDeclaration == null) {
        List<Ref> behaviorRefs = behaviorGlobalName.getRefs();
        for (Ref ref : behaviorRefs) {
          if (ref.isSet()) {
            isGlobalDeclaration = false;
            behaviorDeclaration = ref;
            break;
          }
        }
      }

      if (behaviorDeclaration == null) {
        compiler.report(JSError.make(behaviorName, POLYMER_UNQUALIFIED_BEHAVIOR));
        continue;
      }

      Node behaviorDeclarationNode = behaviorDeclaration.getNode();
      JSDocInfo behaviorInfo = NodeUtil.getBestJSDocInfo(behaviorDeclarationNode);
      if (behaviorInfo == null || !behaviorInfo.isPolymerBehavior()) {
        compiler.report(JSError.make(behaviorDeclarationNode, POLYMER_UNANNOTATED_BEHAVIOR));
      }

      Node behaviorValue = NodeUtil.getRValueOfLValue(behaviorDeclarationNode);

      if (behaviorValue == null) {
        compiler.report(JSError.make(behaviorName, POLYMER_UNQUALIFIED_BEHAVIOR));
      } else if (behaviorValue.isArrayLit()) {
        // Individual behaviors can also be arrays of behaviors. Parse them recursively.
        behaviors.addAll(extractBehaviors(behaviorValue));
      } else if (behaviorValue.isObjectLit()) {
        this.switchDollarSignPropsToBrackets(behaviorValue);
        this.quoteListenerAndHostAttributeKeys(behaviorValue);
        behaviors.add(
            new BehaviorDefinition(
                extractProperties(behaviorValue),
                getBehaviorFunctionsToCopy(behaviorValue),
                getNonPropertyMembersToCopy(behaviorValue),
                isGlobalDeclaration));
      } else {
        compiler.report(JSError.make(behaviorName, POLYMER_UNQUALIFIED_BEHAVIOR));
      }
    }

    return behaviors.build();
  }

  /**
   * @return A list of functions from a behavior which should be copied to the element prototype.
   */
  private List<MemberDefinition> getBehaviorFunctionsToCopy(Node behaviorObjLit) {
    Preconditions.checkState(behaviorObjLit.isObjectLit());
    ImmutableList.Builder<MemberDefinition> functionsToCopy = ImmutableList.builder();

    for (Node keyNode : behaviorObjLit.children()) {
      if ((keyNode.isStringKey() && keyNode.getFirstChild().isFunction()
              || keyNode.isMemberFunctionDef())
          && !behaviorNamesNotToCopy.contains(keyNode.getString())) {
        functionsToCopy.add(
            new MemberDefinition(
                NodeUtil.getBestJSDocInfo(keyNode), keyNode, keyNode.getFirstChild()));
      }
    }

    return functionsToCopy.build();
  }

  /**
   * @return A list of MemberDefinitions in a behavior which are not in the properties block, but
   *     should still be copied to the element prototype.
   */
  private List<MemberDefinition> getNonPropertyMembersToCopy(Node behaviorObjLit) {
    Preconditions.checkState(behaviorObjLit.isObjectLit());
    ImmutableList.Builder<MemberDefinition> membersToCopy = ImmutableList.builder();

    for (Node keyNode : behaviorObjLit.children()) {
      if (keyNode.isGetterDef()
          || (keyNode.isStringKey()
              && !keyNode.getFirstChild().isFunction()
              && !behaviorNamesNotToCopy.contains(keyNode.getString()))) {
        membersToCopy.add(
            new MemberDefinition(
                NodeUtil.getBestJSDocInfo(keyNode), keyNode, keyNode.getFirstChild()));
      }
    }

    return membersToCopy.build();
  }

  private static List<MemberDefinition> extractProperties(Node descriptor) {
    Node properties = NodeUtil.getFirstPropMatchingKey(descriptor, "properties");
    if (properties == null) {
      return ImmutableList.of();
    }

    ImmutableList.Builder<MemberDefinition> members = ImmutableList.builder();
    for (Node keyNode : properties.children()) {
      members.add(
          new MemberDefinition(
              NodeUtil.getBestJSDocInfo(keyNode), keyNode, keyNode.getFirstChild()));
    }
    return members.build();
  }

  private void rewritePolymerClass(Node exprRoot, final ClassDefinition cls, NodeTraversal t) {
    // Add {@code @lends} to the object literal.
    Node call = exprRoot.getFirstChild();
    if (call.isAssign()) {
      call = call.getChildAtIndex(1);
    } else if (call.isName()) {
      call = call.getFirstChild();
    }

    Node objLit = cls.descriptor;
    if (hasShorthandAssignment(objLit)) {
      compiler.report(JSError.make(objLit, POLYMER_SHORTHAND_NOT_SUPPORTED));
      return;
    }

    JSDocInfoBuilder objLitDoc = new JSDocInfoBuilder(true);
    objLitDoc.recordLends(cls.target.getQualifiedName() + ".prototype");
    objLit.setJSDocInfo(objLitDoc.build());

    this.addTypesToFunctions(objLit, cls.target.getQualifiedName());
    this.switchDollarSignPropsToBrackets(objLit);
    this.quoteListenerAndHostAttributeKeys(objLit);

    // For simplicity add everything into a block, before adding it to the AST.
    Node block = IR.block();

    if (cls.nativeBaseElement != null) {
      this.appendPolymerElementExterns(cls);
    }
    JSDocInfoBuilder constructorDoc = this.getConstructorDoc(cls);

    // Remove the original constructor JS docs from the objlit.
    Node ctorKey = cls.constructor.value.getParent();
    if (ctorKey != null) {
      ctorKey.removeProp(Node.JSDOC_INFO_PROP);
    }

    if (cls.target.isGetProp()) {
      // foo.bar = Polymer({...});
      Node assign = IR.assign(cls.target.cloneTree(), cls.constructor.value.cloneTree());
      assign.setJSDocInfo(constructorDoc.build());
      Node exprResult = IR.exprResult(assign);
      block.addChildToBack(exprResult);
    } else {
      // var foo = Polymer({...}); OR Polymer({...});
      Node var = IR.var(cls.target.cloneTree(), cls.constructor.value.cloneTree());
      var.setJSDocInfo(constructorDoc.build());
      block.addChildToBack(var);
    }

    appendPropertiesToBlock(cls, block, cls.target.getQualifiedName() + ".prototype.");
    appendBehaviorMembersToBlock(cls, block);
    List<MemberDefinition> readOnlyProps = parseReadOnlyProperties(cls, block);
    addInterfaceExterns(cls, readOnlyProps);
    removePropertyDocs(objLit);

    block.useSourceInfoIfMissingFromForTree(exprRoot);
    Node stmts = block.removeChildren();
    Node parent = exprRoot.getParent();

    // If the call to Polymer() is not in the global scope and the assignment target is not
    // namespaced (which likely means it's exported to the global scope), put the type declaration
    // into the global scope at the start of the current script.
    //
    // This avoids unknown type warnings which are a result of the compiler's poor understanding of
    // types declared inside IIFEs or any non-global scope. We should revisit this decision after
    // moving to the new type inference system which should be able to infer these types better.
    if (!t.getScope().isGlobal() && !cls.target.isGetProp()) {
      Node scriptNode = NodeUtil.getEnclosingScript(exprRoot);
      scriptNode.addChildrenToFront(stmts);
    } else {
      Node beforeRoot = parent.getChildBefore(exprRoot);
      if (beforeRoot == null) {
        parent.addChildrenToFront(stmts);
      } else {
        parent.addChildrenAfter(stmts, beforeRoot);
      }
    }

    if (exprRoot.isVar()) {
      Node assignExpr = varToAssign(exprRoot);
      parent.replaceChild(exprRoot, assignExpr);
    }

    compiler.reportCodeChange();
  }

  /** Add an @this annotation to all functions in the objLit. */
  private void addTypesToFunctions(Node objLit, String thisType) {
    Preconditions.checkState(objLit.isObjectLit());
    for (Node keyNode : objLit.children()) {
      Node value = keyNode.getLastChild();
      if (value != null && value.isFunction()) {
        JSDocInfoBuilder fnDoc = JSDocInfoBuilder.maybeCopyFrom(keyNode.getJSDocInfo());
        fnDoc.recordThisType(
            new JSTypeExpression(new Node(Token.BANG, IR.string(thisType)), VIRTUAL_FILE));
        keyNode.setJSDocInfo(fnDoc.build());
      }
    }

    // Add @this and @return to default property values.
    for (MemberDefinition property : extractProperties(objLit)) {
      if (!property.value.isObjectLit()) {
        continue;
      }
      if (hasShorthandAssignment(property.value)) {
        compiler.report(JSError.make(property.value, POLYMER_SHORTHAND_NOT_SUPPORTED));
        return;
      }

      Node defaultValue = NodeUtil.getFirstPropMatchingKey(property.value, "value");
      if (defaultValue == null || !defaultValue.isFunction()) {
        continue;
      }
      Node defaultValueKey = defaultValue.getParent();
      JSDocInfoBuilder fnDoc = JSDocInfoBuilder.maybeCopyFrom(defaultValueKey.getJSDocInfo());
      fnDoc.recordThisType(
          new JSTypeExpression(new Node(Token.BANG, IR.string(thisType)), VIRTUAL_FILE));
      fnDoc.recordReturnType(getTypeFromProperty(property));
      defaultValueKey.setJSDocInfo(fnDoc.build());
    }
  }

  /** Switches all "this.$.foo" to "this.$['foo']". */
  private void switchDollarSignPropsToBrackets(Node objLit) {
    Preconditions.checkState(objLit.isObjectLit());
    for (Node keyNode : objLit.children()) {
      Node value = keyNode.getFirstChild();
      if (value != null && value.isFunction()) {
        NodeUtil.visitPostOrder(
            value.getLastChild(),
            new NodeUtil.Visitor() {
              @Override
              public void visit(Node n) {
                if (n.isString()
                    && n.getString().equals("$")
                    && n.getParent().isGetProp()
                    && n.getParent().getParent().isGetProp()) {
                  Node dollarChildProp = n.getParent().getParent();
                  dollarChildProp.setType(Token.GETELEM);
                  compiler.reportCodeChange();
                }
              }
            },
            Predicates.<Node>alwaysTrue());
      }
    }
  }

  /**
   * Makes sure that the keys for listeners and hostAttributes blocks are quoted to avoid renaming.
   */
  private void quoteListenerAndHostAttributeKeys(Node objLit) {
    Preconditions.checkState(objLit.isObjectLit());
    for (Node keyNode : objLit.children()) {
      if (keyNode.isComputedProp()) {
        continue;
      }
      if (!keyNode.getString().equals("listeners")
          && !keyNode.getString().equals("hostAttributes")) {
        continue;
      }
      for (Node keyToQuote : keyNode.getFirstChild().children()) {
        keyToQuote.setQuotedString();
      }
    }
  }

  /** Appends all properties in the ClassDefinition to the prototype of the custom element. */
  private void appendPropertiesToBlock(final ClassDefinition cls, Node block, String basePath) {
    for (MemberDefinition prop : cls.props) {
      Node propertyNode =
          IR.exprResult(NodeUtil.newQName(compiler, basePath + prop.name.getString()));
      JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(prop.info);

      JSTypeExpression propType = getTypeFromProperty(prop);
      if (propType == null) {
        return;
      }
      info.recordType(propType);
      propertyNode.getFirstChild().setJSDocInfo(info.build());

      block.addChildToBack(propertyNode);
    }
  }

  /** Remove all JSDocs from properties of a class definition */
  private void removePropertyDocs(final Node objLit) {
    for (MemberDefinition prop : extractProperties(objLit)) {
      prop.name.removeProp(Node.JSDOC_INFO_PROP);
    }
  }

  /** Appends all required behavior functions and non-property members to the given block. */
  private void appendBehaviorMembersToBlock(final ClassDefinition cls, Node block) {
    String qualifiedPath = cls.target.getQualifiedName() + ".prototype.";
    Map<String, Node> nameToExprResult = new HashMap<>();
    for (BehaviorDefinition behavior : cls.behaviors) {
      for (MemberDefinition behaviorFunction : behavior.functionsToCopy) {
        String fnName = behaviorFunction.name.getString();
        // Don't copy functions already defined by the element itself.
        if (NodeUtil.getFirstPropMatchingKey(cls.descriptor, fnName) != null) {
          continue;
        }

        // Avoid copying over the same function twice. The last definition always wins.
        if (nameToExprResult.containsKey(fnName)) {
          block.removeChild(nameToExprResult.get(fnName));
        }

        Node fnValue = behaviorFunction.value.cloneTree();
        Node exprResult =
            IR.exprResult(IR.assign(NodeUtil.newQName(compiler, qualifiedPath + fnName), fnValue));
        JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(behaviorFunction.info);

        // Behaviors whose declarations are not in the global scope may contain references to
        // symbols which do not exist in the element's scope. Only copy a function stub. See
        if (!behavior.isGlobalDeclaration) {
          NodeUtil.getFunctionBody(fnValue).removeChildren();
        }

        exprResult.getFirstChild().setJSDocInfo(info.build());
        block.addChildToBack(exprResult);
        nameToExprResult.put(fnName, exprResult);
      }

      // Copy other members.
      for (MemberDefinition behaviorProp : behavior.nonPropertyMembersToCopy) {
        String propName = behaviorProp.name.getString();
        if (nameToExprResult.containsKey(propName)) {
          block.removeChild(nameToExprResult.get(propName));
        }

        Node exprResult = IR.exprResult(NodeUtil.newQName(compiler, qualifiedPath + propName));
        JSDocInfoBuilder info = JSDocInfoBuilder.maybeCopyFrom(behaviorProp.info);

        if (behaviorProp.name.isGetterDef()) {
          info = new JSDocInfoBuilder(true);
          if (behaviorProp.info != null && behaviorProp.info.getReturnType() != null) {
            info.recordType(behaviorProp.info.getReturnType());
          }
        }

        exprResult.getFirstChild().setJSDocInfo(info.build());
        block.addChildToBack(exprResult);
        nameToExprResult.put(propName, exprResult);
      }
    }
  }

  /**
   * Generates the _set* setters for readonly properties and appends them to the given block.
   *
   * @return A List of all readonly properties.
   */
  private List<MemberDefinition> parseReadOnlyProperties(final ClassDefinition cls, Node block) {
    String qualifiedPath = cls.target.getQualifiedName() + ".prototype.";
    ImmutableList.Builder<MemberDefinition> readOnlyProps = ImmutableList.builder();

    for (MemberDefinition prop : cls.props) {
      // Generate the setter for readOnly properties.
      if (prop.value.isObjectLit()) {
        Node readOnlyValue = NodeUtil.getFirstPropMatchingKey(prop.value, "readOnly");
        if (readOnlyValue != null && readOnlyValue.isTrue()) {
          block.addChildToBack(makeReadOnlySetter(prop.name.getString(), qualifiedPath));
          readOnlyProps.add(prop);
        }
      }
    }

    return readOnlyProps.build();
  }

  /**
   * Gets the JSTypeExpression for a given property using its "type" key.
   *
   * @see https://github.com/Polymer/polymer/blob/0.8-preview/PRIMER.md#configuring-properties
   */
  private JSTypeExpression getTypeFromProperty(MemberDefinition property) {
    if (property.info != null && property.info.hasType()) {
      return property.info.getType();
    }

    String typeString = "";
    if (property.value.isObjectLit()) {
      Node typeValue = NodeUtil.getFirstPropMatchingKey(property.value, "type");
      if (typeValue == null || !typeValue.isName()) {
        compiler.report(JSError.make(property.name, POLYMER_INVALID_PROPERTY));
        return null;
      }
      typeString = typeValue.getString();
    } else if (property.value.isName()) {
      typeString = property.value.getString();
    }

    Node typeNode = null;
    switch (typeString) {
      case "Boolean":
      case "String":
      case "Number":
        typeNode = IR.string(typeString.toLowerCase());
        break;
      case "Array":
      case "Function":
      case "Object":
      case "Date":
        typeNode = new Node(Token.BANG, IR.string(typeString));
        break;
      default:
        compiler.report(JSError.make(property.name, POLYMER_INVALID_PROPERTY));
        return null;
    }

    return new JSTypeExpression(typeNode, VIRTUAL_FILE);
  }

  /**
   * Adds the generated setter for a readonly property.
   *
   * @see https://www.polymer-project.org/0.8/docs/devguide/properties.html#read-only
   */
  private Node makeReadOnlySetter(String propName, String qualifiedPath) {
    String setterName = "_set" + propName.substring(0, 1).toUpperCase() + propName.substring(1);
    Node fnNode = IR.function(IR.name(""), IR.paramList(IR.name(propName)), IR.block());
    Node exprResNode =
        IR.exprResult(IR.assign(NodeUtil.newQName(compiler, qualifiedPath + setterName), fnNode));

    JSDocInfoBuilder info = new JSDocInfoBuilder(true);
    // This is overriding a generated function which was added to the interface in
    // {@code addInterfaceExterns}.
    info.recordOverride();
    exprResNode.getFirstChild().setJSDocInfo(info.build());

    return exprResNode;
  }

  /**
   * Duplicates the PolymerElement externs with a different element base class if needed. For
   * example, if the base class is HTMLInputElement, then a class PolymerInputElement will be added.
   * If the element does not extend a native HTML element, this method is a no-op.
   */
  private void appendPolymerElementExterns(final ClassDefinition cls) {
    if (!nativeExternsAdded.add(cls.nativeBaseElement)) {
      return;
    }

    Node block = IR.block();

    Node baseExterns = polymerElementExterns.cloneTree();
    String polymerElementType = getPolymerElementType(cls);
    baseExterns.getFirstChild().setString(polymerElementType);

    String elementType = tagNameMap.get(cls.nativeBaseElement);
    JSTypeExpression elementBaseType =
        new JSTypeExpression(new Node(Token.BANG, IR.string(elementType)), VIRTUAL_FILE);
    JSDocInfoBuilder baseDocs = JSDocInfoBuilder.copyFrom(baseExterns.getJSDocInfo());
    baseDocs.changeBaseType(elementBaseType);
    baseExterns.setJSDocInfo(baseDocs.build());
    block.addChildToBack(baseExterns);

    for (Node baseProp : polymerElementProps) {
      Node newProp = baseProp.cloneTree();
      Node newPropRootName =
          NodeUtil.getRootOfQualifiedName(newProp.getFirstChild().getFirstChild());
      newPropRootName.setString(polymerElementType);
      block.addChildToBack(newProp);
    }

    Node parent = polymerElementExterns.getParent();
    Node stmts = block.removeChildren();
    parent.addChildrenAfter(stmts, polymerElementExterns);

    compiler.reportCodeChange();
  }

  /**
   * Adds an interface for the given ClassDefinition to externs. This allows generated setter
   * functions for read-only properties to avoid renaming altogether.
   *
   * @see https://www.polymer-project.org/0.8/docs/devguide/properties.html#read-only
   */
  private void addInterfaceExterns(
      final ClassDefinition cls, List<MemberDefinition> readOnlyProps) {
    Node block = IR.block();

    String interfaceName = getInterfaceName(cls);
    Node fnNode = IR.function(IR.name(""), IR.paramList(), IR.block());
    Node varNode = IR.var(NodeUtil.newQName(compiler, interfaceName), fnNode);

    JSDocInfoBuilder info = new JSDocInfoBuilder(true);
    info.recordInterface();
    varNode.setJSDocInfo(info.build());
    block.addChildToBack(varNode);

    appendPropertiesToBlock(cls, block, interfaceName + ".prototype.");
    for (MemberDefinition prop : readOnlyProps) {
      // Add all _set* functions to avoid renaming.
      String propName = prop.name.getString();
      String setterName = "_set" + propName.substring(0, 1).toUpperCase() + propName.substring(1);
      Node setterExprNode =
          IR.exprResult(NodeUtil.newQName(compiler, interfaceName + ".prototype." + setterName));

      JSDocInfoBuilder setterInfo = new JSDocInfoBuilder(true);
      JSTypeExpression propType = getTypeFromProperty(prop);
      setterInfo.recordParameter(propName, propType);
      setterExprNode.getFirstChild().setJSDocInfo(setterInfo.build());

      block.addChildToBack(setterExprNode);
    }

    Node parent = polymerElementExterns.getParent();
    Node stmts = block.removeChildren();
    parent.addChildrenToBack(stmts);

    compiler.reportCodeChange();
  }

  /** @return The name of the generated extern interface which the element implements. */
  private String getInterfaceName(final ClassDefinition cls) {
    return "Polymer" + cls.target.getQualifiedName().replaceAll("\\.", "_") + "Interface";
  }

  /** @return The proper constructor doc for the Polymer call. */
  private JSDocInfoBuilder getConstructorDoc(final ClassDefinition cls) {
    JSDocInfoBuilder constructorDoc = JSDocInfoBuilder.maybeCopyFrom(cls.constructor.info);
    constructorDoc.recordConstructor();

    JSTypeExpression baseType =
        new JSTypeExpression(
            new Node(Token.BANG, IR.string(getPolymerElementType(cls))), VIRTUAL_FILE);
    constructorDoc.recordBaseType(baseType);

    String interfaceName = getInterfaceName(cls);
    JSTypeExpression interfaceType =
        new JSTypeExpression(new Node(Token.BANG, IR.string(interfaceName)), VIRTUAL_FILE);
    constructorDoc.recordImplementedInterface(interfaceType);

    return constructorDoc;
  }

  /** @return An assign replacing the equivalent var declaration. */
  private static Node varToAssign(Node var) {
    Node assign =
        IR.assign(IR.name(var.getFirstChild().getString()), var.getFirstChild().removeFirstChild());
    return IR.exprResult(assign).useSourceInfoFromForTree(var);
  }

  /** @return The PolymerElement type string for a class definition. */
  private static String getPolymerElementType(final ClassDefinition cls) {
    return String.format(
        "Polymer%sElement",
        cls.nativeBaseElement == null
            ? ""
            : CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_CAMEL, cls.nativeBaseElement));
  }

  /** @return Whether the call represents a call to Polymer. */
  private static boolean isPolymerCall(Node value) {
    return value != null && value.isCall() && value.getFirstChild().matchesQualifiedName("Polymer");
  }

  private boolean hasShorthandAssignment(Node objLit) {
    Preconditions.checkState(objLit.isObjectLit());
    for (Node property : objLit.children()) {
      if (property.isStringKey() && !property.hasChildren()) {
        return true;
      }
    }
    return false;
  }
}
/**
 * Ensures string literals matching certain patterns are only used as goog.getCssName parameters.
 *
 * @author [email protected] (Martin Kretzschmar)
 */
@GwtIncompatible("java.util.regex")
class CheckMissingGetCssName extends AbstractPostOrderCallback implements CompilerPass {
  private final AbstractCompiler compiler;
  private final CheckLevel level;
  private final Matcher blacklist;

  static final String GET_CSS_NAME_FUNCTION = "goog.getCssName";
  static final String GET_UNIQUE_ID_FUNCTION = ".getUniqueId";

  static final DiagnosticType MISSING_GETCSSNAME =
      DiagnosticType.disabled(
          "JSC_MISSING_GETCSSNAME", "missing goog.getCssName around literal ''{0}''");

  CheckMissingGetCssName(AbstractCompiler compiler, CheckLevel level, String blacklistRegex) {
    this.compiler = compiler;
    this.level = level;
    this.blacklist = Pattern.compile("\\b(?:" + blacklistRegex + ")").matcher("");
  }

  @Override
  public void process(Node externs, Node root) {
    NodeTraversal.traverseEs6(compiler, root, this);
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    if (n.isString() && !parent.isGetProp() && !parent.isRegExp()) {
      String s = n.getString();

      for (blacklist.reset(s); blacklist.find(); ) {
        if (parent.isTemplateLit()) {
          if (parent.getChildCount() > 1) {
            // Ignore template string with substitutions
            continue;
          } else {
            n = parent;
          }
        }
        if (insideGetCssNameCall(n)) {
          continue;
        }
        if (insideGetUniqueIdCall(n)) {
          continue;
        }
        if (insideAssignmentToIdConstant(n)) {
          continue;
        }
        compiler.report(t.makeError(n, level, MISSING_GETCSSNAME, blacklist.group()));
      }
    }
  }

  /** Returns whether the node is an argument of a goog.getCssName call. */
  private static boolean insideGetCssNameCall(Node n) {
    Node parent = n.getParent();
    return parent.isCall() && parent.getFirstChild().matchesQualifiedName(GET_CSS_NAME_FUNCTION);
  }

  /**
   * Returns whether the node is an argument of a function that returns a unique id (the last part
   * of the qualified name matches GET_UNIQUE_ID_FUNCTION).
   */
  private static boolean insideGetUniqueIdCall(Node n) {
    Node parent = n.getParent();
    String name = parent.isCall() ? parent.getFirstChild().getQualifiedName() : null;

    return name != null && name.endsWith(GET_UNIQUE_ID_FUNCTION);
  }

  /**
   * Returns whether the node is the right hand side of an assignment or initialization of a
   * variable named *_ID of *_ID_.
   */
  private boolean insideAssignmentToIdConstant(Node n) {
    Node parent = n.getParent();
    if (parent.isAssign()) {
      String qname = parent.getFirstChild().getQualifiedName();
      return qname != null && isIdName(qname);
    } else if (parent.isName()) {
      Node grandParent = parent.getParent();
      if (grandParent != null && NodeUtil.isNameDeclaration(grandParent)) {
        String name = parent.getString();
        return isIdName(name);
      } else {
        return false;
      }
    } else {
      return false;
    }
  }

  private static boolean isIdName(String name) {
    return name.endsWith("ID") || name.endsWith("ID_");
  }
}
/**
 * Checks that the code obeys the static restrictions of strict mode:
 *
 * <ol>
 *   <li>No use of "with".
 *   <li>No deleting variables, functions, or arguments.
 *   <li>No re-declarations or assignments of "eval" or arguments.
 *   <li>No use of arguments.callee
 *   <li>No use of arguments.caller
 *   <li>Class: Always under strict mode
 *   <li>In addition, no duplicate class method names
 * </ol>
 */
class StrictModeCheck extends AbstractPostOrderCallback implements CompilerPass {

  static final DiagnosticType USE_OF_WITH =
      DiagnosticType.warning(
          "JSC_USE_OF_WITH", "The 'with' statement cannot be used in ES5 strict mode.");

  static final DiagnosticType EVAL_DECLARATION =
      DiagnosticType.warning(
          "JSC_EVAL_DECLARATION", "\"eval\" cannot be redeclared in ES5 strict mode");

  static final DiagnosticType EVAL_ASSIGNMENT =
      DiagnosticType.warning(
          "JSC_EVAL_ASSIGNMENT", "the \"eval\" object cannot be reassigned in ES5 strict mode");

  static final DiagnosticType ARGUMENTS_DECLARATION =
      DiagnosticType.warning(
          "JSC_ARGUMENTS_DECLARATION", "\"arguments\" cannot be redeclared in ES5 strict mode");

  static final DiagnosticType ARGUMENTS_ASSIGNMENT =
      DiagnosticType.warning(
          "JSC_ARGUMENTS_ASSIGNMENT",
          "the \"arguments\" object cannot be reassigned in ES5 strict mode");

  static final DiagnosticType ARGUMENTS_CALLEE_FORBIDDEN =
      DiagnosticType.warning(
          "JSC_ARGUMENTS_CALLEE_FORBIDDEN",
          "\"arguments.callee\" cannot be used in ES5 strict mode");

  static final DiagnosticType ARGUMENTS_CALLER_FORBIDDEN =
      DiagnosticType.warning(
          "JSC_ARGUMENTS_CALLER_FORBIDDEN",
          "\"arguments.caller\" cannot be used in ES5 strict mode");

  static final DiagnosticType FUNCTION_CALLER_FORBIDDEN =
      DiagnosticType.warning(
          "JSC_FUNCTION_CALLER_FORBIDDEN",
          "A function''s \"caller\" property cannot be used in ES5 strict mode");

  static final DiagnosticType FUNCTION_ARGUMENTS_PROP_FORBIDDEN =
      DiagnosticType.warning(
          "JSC_FUNCTION_ARGUMENTS_PROP_FORBIDDEN",
          "A function''s \"arguments\" property cannot be used in ES5 strict mode");

  static final DiagnosticType DELETE_VARIABLE =
      DiagnosticType.warning(
          "JSC_DELETE_VARIABLE",
          "variables, functions, and arguments cannot be deleted in " + "ES5 strict mode");

  static final DiagnosticType DUPLICATE_OBJECT_KEY =
      DiagnosticType.warning(
          "JSC_DUPLICATE_OBJECT_KEY",
          "object literals cannot contain duplicate keys in ES5 strict mode");

  static final DiagnosticType DUPLICATE_CLASS_METHODS =
      DiagnosticType.error(
          "JSC_DUPLICATE_CLASS_METHODS", "Classes cannot contain duplicate method names");

  static final DiagnosticType BAD_FUNCTION_DECLARATION =
      DiagnosticType.error(
          "JSC_BAD_FUNCTION_DECLARATION",
          "functions can only be declared at top level or immediately within "
              + "another function in ES5 strict mode");

  private final AbstractCompiler compiler;
  private final boolean noVarCheck;

  StrictModeCheck(AbstractCompiler compiler) {
    this(compiler, false);
  }

  StrictModeCheck(AbstractCompiler compiler, boolean noVarCheck) {
    this.compiler = compiler;
    this.noVarCheck = noVarCheck;
  }

  @Override
  public void process(Node externs, Node root) {
    NodeTraversal.traverseRoots(compiler, this, externs, root);
    NodeTraversal.traverseEs6(compiler, root, new NonExternChecks());
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    if (n.isFunction()) {
      checkFunctionUse(t, n);
    } else if (n.isAssign()) {
      checkAssignment(t, n);
    } else if (n.isDelProp()) {
      checkDelete(t, n);
    } else if (n.isObjectLit()) {
      checkObjectLiteralOrClass(t, n);
    } else if (n.isClass()) {
      checkObjectLiteralOrClass(t, n.getLastChild());
    } else if (n.isWith()) {
      checkWith(t, n);
    }
  }

  /** Reports a warning for with statements. */
  private static void checkWith(NodeTraversal t, Node n) {
    JSDocInfo info = n.getJSDocInfo();
    boolean allowWith = info != null && info.getSuppressions().contains("with");
    if (!allowWith) {
      t.report(n, USE_OF_WITH);
    }
  }

  /** Checks that the function is used legally. */
  private static void checkFunctionUse(NodeTraversal t, Node n) {
    if (NodeUtil.isFunctionDeclaration(n) && !NodeUtil.isHoistedFunctionDeclaration(n)) {
      t.report(n, BAD_FUNCTION_DECLARATION);
    }
  }

  /**
   * Determines if the given name is a declaration, which can be a declaration of a variable,
   * function, or argument.
   */
  private static boolean isDeclaration(Node n) {
    switch (n.getParent().getType()) {
      case Token.LET:
      case Token.CONST:
      case Token.VAR:
      case Token.FUNCTION:
      case Token.CATCH:
        return true;

      case Token.PARAM_LIST:
        return n.getParent().getParent().isFunction();

      default:
        return false;
    }
  }

  /** Checks that an assignment is not to the "arguments" object. */
  private static void checkAssignment(NodeTraversal t, Node n) {
    if (n.getFirstChild().isName()) {
      if ("arguments".equals(n.getFirstChild().getString())) {
        t.report(n, ARGUMENTS_ASSIGNMENT);
      } else if ("eval".equals(n.getFirstChild().getString())) {
        // Note that assignment to eval is already illegal because any use of
        // that name is illegal.
        t.report(n, EVAL_ASSIGNMENT);
      }
    }
  }

  /** Checks that variables, functions, and arguments are not deleted. */
  private static void checkDelete(NodeTraversal t, Node n) {
    if (n.getFirstChild().isName()) {
      Var v = t.getScope().getVar(n.getFirstChild().getString());
      if (v != null) {
        t.report(n, DELETE_VARIABLE);
      }
    }
  }

  /** Checks that object literal keys or class method names are valid. */
  private static void checkObjectLiteralOrClass(NodeTraversal t, Node n) {
    Set<String> getters = new HashSet<>();
    Set<String> setters = new HashSet<>();
    for (Node key = n.getFirstChild(); key != null; key = key.getNext()) {
      if (!key.isSetterDef()) {
        // normal property and getter cases
        if (!getters.add(key.getString())) {
          if (n.isClassMembers()) {
            t.report(key, DUPLICATE_CLASS_METHODS);
          } else {
            t.report(key, DUPLICATE_OBJECT_KEY);
          }
        }
      }
      if (!key.isGetterDef()) {
        // normal property and setter cases
        if (!setters.add(key.getString())) {
          if (n.isClassMembers()) {
            t.report(key, DUPLICATE_CLASS_METHODS);
          } else {
            t.report(key, DUPLICATE_OBJECT_KEY);
          }
        }
      }
    }
  }

  /** Checks that are performed on non-extern code only. */
  private static class NonExternChecks extends AbstractPostOrderCallback {
    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if ((n.isName()) && isDeclaration(n)) {
        checkDeclaration(t, n);
      } else if (n.isGetProp()) {
        checkGetProp(t, n);
      }
    }

    /** Checks for illegal declarations. */
    private void checkDeclaration(NodeTraversal t, Node n) {
      if ("eval".equals(n.getString())) {
        t.report(n, EVAL_DECLARATION);
      } else if ("arguments".equals(n.getString())) {
        t.report(n, ARGUMENTS_DECLARATION);
      }
    }

    /** Checks that the arguments.callee is not used. */
    private void checkGetProp(NodeTraversal t, Node n) {
      Node target = n.getFirstChild();
      Node prop = n.getLastChild();
      if (prop.getString().equals("callee")) {
        if (target.isName() && target.getString().equals("arguments")) {
          t.report(n, ARGUMENTS_CALLEE_FORBIDDEN);
        }
      } else if (prop.getString().equals("caller")) {
        if (target.isName() && target.getString().equals("arguments")) {
          t.report(n, ARGUMENTS_CALLER_FORBIDDEN);
        } else if (isFunctionType(target)) {
          t.report(n, FUNCTION_CALLER_FORBIDDEN);
        }
      } else if (prop.getString().equals("arguments") && isFunctionType(target)) {
        t.report(n, FUNCTION_ARGUMENTS_PROP_FORBIDDEN);
      }
    }
  }

  private static boolean isFunctionType(Node n) {
    TypeI type = n.getTypeI();
    return (type != null && type.isFunctionType());
  }
}
/**
 * Process goog.tweak primitives. Checks that:
 *
 * <ul>
 *   <li>parameters to goog.tweak.register* are literals of the correct type.
 *   <li>the parameter to goog.tweak.get* is a string literal.
 *   <li>parameters to goog.tweak.overrideDefaultValue are literals of the correct type.
 *   <li>tweak IDs passed to goog.tweak.get* and goog.tweak.overrideDefaultValue correspond to
 *       registered tweaks.
 *   <li>all calls to goog.tweak.register* and goog.tweak.overrideDefaultValue are within the
 *       top-level context.
 *   <li>each tweak is registered only once.
 *   <li>calls to goog.tweak.overrideDefaultValue occur before the call to the corresponding
 *       goog.tweak.register* function.
 * </ul>
 *
 * @author [email protected] (Andrew Grieve)
 */
class ProcessTweaks implements CompilerPass {

  private final AbstractCompiler compiler;
  private final boolean stripTweaks;
  private final SortedMap<String, Node> compilerDefaultValueOverrides;

  private static final CharMatcher ID_MATCHER =
      CharMatcher.inRange('a', 'z')
          .or(CharMatcher.inRange('A', 'Z'))
          .or(CharMatcher.anyOf("0123456789_."));

  // Warnings and Errors.
  static final DiagnosticType UNKNOWN_TWEAK_WARNING =
      DiagnosticType.warning("JSC_UNKNOWN_TWEAK_WARNING", "no tweak registered with ID {0}");

  static final DiagnosticType TWEAK_MULTIPLY_REGISTERED_ERROR =
      DiagnosticType.error(
          "JSC_TWEAK_MULTIPLY_REGISTERED_ERROR", "Tweak {0} has already been registered.");

  static final DiagnosticType NON_LITERAL_TWEAK_ID_ERROR =
      DiagnosticType.error("JSC_NON_LITERAL_TWEAK_ID_ERROR", "tweak ID must be a string literal");

  static final DiagnosticType INVALID_TWEAK_DEFAULT_VALUE_WARNING =
      DiagnosticType.warning(
          "JSC_INVALID_TWEAK_DEFAULT_VALUE_WARNING",
          "tweak {0} registered with {1} must have a default value that is a "
              + "literal of type {2}");

  static final DiagnosticType NON_GLOBAL_TWEAK_INIT_ERROR =
      DiagnosticType.error(
          "JSC_NON_GLOBAL_TWEAK_INIT_ERROR",
          "tweak declaration {0} must occur in the global scope");

  static final DiagnosticType TWEAK_OVERRIDE_AFTER_REGISTERED_ERROR =
      DiagnosticType.error(
          "JSC_TWEAK_OVERRIDE_AFTER_REGISTERED_ERROR",
          "Cannot override the default value of tweak {0} after it has been " + "registered");

  static final DiagnosticType TWEAK_WRONG_GETTER_TYPE_WARNING =
      DiagnosticType.warning(
          "JSC_TWEAK_WRONG_GETTER_TYPE_WARNING",
          "tweak getter function {0} used for tweak registered using {1}");

  static final DiagnosticType INVALID_TWEAK_ID_ERROR =
      DiagnosticType.error(
          "JSC_INVALID_TWEAK_ID_ERROR",
          "tweak ID contains illegal characters. Only letters, numbers, _ " + "and . are allowed");

  /** An enum of goog.tweak functions. */
  private static enum TweakFunction {
    REGISTER_BOOLEAN("goog.tweak.registerBoolean", "boolean", Token.TRUE, Token.FALSE),
    REGISTER_NUMBER("goog.tweak.registerNumber", "number", Token.NUMBER),
    REGISTER_STRING("goog.tweak.registerString", "string", Token.STRING),
    OVERRIDE_DEFAULT_VALUE("goog.tweak.overrideDefaultValue"),
    GET_COMPILER_OVERRIDES("goog.tweak.getCompilerOverrides_"),
    GET_BOOLEAN("goog.tweak.getBoolean", REGISTER_BOOLEAN),
    GET_NUMBER("goog.tweak.getNumber", REGISTER_NUMBER),
    GET_STRING("goog.tweak.getString", REGISTER_STRING);

    final String name;
    final String expectedTypeName;
    final int validNodeTypeA;
    final int validNodeTypeB;
    final TweakFunction registerFunction;

    TweakFunction(String name) {
      this(name, null, Token.ERROR, Token.ERROR, null);
    }

    TweakFunction(String name, String expectedTypeName, int validNodeTypeA) {
      this(name, expectedTypeName, validNodeTypeA, Token.ERROR, null);
    }

    TweakFunction(String name, String expectedTypeName, int validNodeTypeA, int validNodeTypeB) {
      this(name, expectedTypeName, validNodeTypeA, validNodeTypeB, null);
    }

    TweakFunction(String name, TweakFunction registerFunction) {
      this(name, null, Token.ERROR, Token.ERROR, registerFunction);
    }

    TweakFunction(
        String name,
        String expectedTypeName,
        int validNodeTypeA,
        int validNodeTypeB,
        TweakFunction registerFunction) {
      this.name = name;
      this.expectedTypeName = expectedTypeName;
      this.validNodeTypeA = validNodeTypeA;
      this.validNodeTypeB = validNodeTypeB;
      this.registerFunction = registerFunction;
    }

    boolean isValidNodeType(int type) {
      return type == validNodeTypeA || type == validNodeTypeB;
    }

    boolean isCorrectRegisterFunction(TweakFunction registerFunction) {
      Preconditions.checkNotNull(registerFunction);
      return this.registerFunction == registerFunction;
    }

    boolean isGetterFunction() {
      return registerFunction != null;
    }

    String getName() {
      return name;
    }

    String getExpectedTypeName() {
      return expectedTypeName;
    }

    Node createDefaultValueNode() {
      switch (this) {
        case REGISTER_BOOLEAN:
          return IR.falseNode();
        case REGISTER_NUMBER:
          return IR.number(0);
        case REGISTER_STRING:
          return IR.string("");
        default:
          throw new IllegalStateException();
      }
    }
  }

  // A map of function name -> TweakFunction.
  private static final Map<String, TweakFunction> TWEAK_FUNCTIONS_MAP;

  static {
    TWEAK_FUNCTIONS_MAP = Maps.newHashMap();
    for (TweakFunction func : TweakFunction.values()) {
      TWEAK_FUNCTIONS_MAP.put(func.getName(), func);
    }
  }

  ProcessTweaks(
      AbstractCompiler compiler,
      boolean stripTweaks,
      Map<String, Node> compilerDefaultValueOverrides) {
    this.compiler = compiler;
    this.stripTweaks = stripTweaks;
    // Having the map sorted is required for the unit tests to be deterministic.
    this.compilerDefaultValueOverrides = Maps.newTreeMap();
    this.compilerDefaultValueOverrides.putAll(compilerDefaultValueOverrides);
  }

  @Override
  public void process(Node externs, Node root) {
    CollectTweaksResult result = collectTweaks(root);
    applyCompilerDefaultValueOverrides(result.tweakInfos);

    boolean changed = false;

    if (stripTweaks) {
      changed = stripAllCalls(result.tweakInfos);
    } else if (!compilerDefaultValueOverrides.isEmpty()) {
      changed = replaceGetCompilerOverridesCalls(result.getOverridesCalls);
    }
    if (changed) {
      compiler.reportCodeChange();
    }
  }

  /**
   * Passes the compiler default value overrides to the JS by replacing calls to
   * goog.tweak.getCompilerOverrids_ with a map of tweak ID->default value;
   */
  private boolean replaceGetCompilerOverridesCalls(List<TweakFunctionCall> calls) {
    for (TweakFunctionCall call : calls) {
      Node callNode = call.callNode;
      Node objNode = createCompilerDefaultValueOverridesVarNode(callNode);
      callNode.getParent().replaceChild(callNode, objNode);
    }
    return !calls.isEmpty();
  }

  /**
   * Removes all CALL nodes in the given TweakInfos, replacing calls to getter functions with the
   * tweak's default value.
   */
  private boolean stripAllCalls(Map<String, TweakInfo> tweakInfos) {
    for (TweakInfo tweakInfo : tweakInfos.values()) {
      boolean isRegistered = tweakInfo.isRegistered();
      for (TweakFunctionCall functionCall : tweakInfo.functionCalls) {
        Node callNode = functionCall.callNode;
        Node parent = callNode.getParent();
        if (functionCall.tweakFunc.isGetterFunction()) {
          Node newValue;
          if (isRegistered) {
            newValue = tweakInfo.getDefaultValueNode().cloneNode();
          } else {
            // When we find a getter of an unregistered tweak, there has
            // already been a warning about it, so now just use a default
            // value when stripping.
            TweakFunction registerFunction = functionCall.tweakFunc.registerFunction;
            newValue = registerFunction.createDefaultValueNode();
          }
          parent.replaceChild(callNode, newValue);
        } else {
          Node voidZeroNode = IR.voidNode(IR.number(0).srcref(callNode)).srcref(callNode);
          parent.replaceChild(callNode, voidZeroNode);
        }
      }
    }
    return !tweakInfos.isEmpty();
  }

  /** Creates a JS object that holds a map of tweakId -> default value override. */
  private Node createCompilerDefaultValueOverridesVarNode(Node sourceInformationNode) {
    Node objNode = IR.objectlit().srcref(sourceInformationNode);
    for (Entry<String, Node> entry : compilerDefaultValueOverrides.entrySet()) {
      Node objKeyNode = IR.stringKey(entry.getKey()).copyInformationFrom(sourceInformationNode);
      Node objValueNode = entry.getValue().cloneNode().copyInformationFrom(sourceInformationNode);
      objKeyNode.addChildToBack(objValueNode);
      objNode.addChildToBack(objKeyNode);
    }
    return objNode;
  }

  /** Sets the default values of tweaks based on compiler options. */
  private void applyCompilerDefaultValueOverrides(Map<String, TweakInfo> tweakInfos) {
    for (Entry<String, Node> entry : compilerDefaultValueOverrides.entrySet()) {
      String tweakId = entry.getKey();
      TweakInfo tweakInfo = tweakInfos.get(tweakId);
      if (tweakInfo == null) {
        compiler.report(JSError.make(UNKNOWN_TWEAK_WARNING, tweakId));
      } else {
        TweakFunction registerFunc = tweakInfo.registerCall.tweakFunc;
        Node value = entry.getValue();
        if (!registerFunc.isValidNodeType(value.getType())) {
          compiler.report(
              JSError.make(
                  INVALID_TWEAK_DEFAULT_VALUE_WARNING,
                  tweakId,
                  registerFunc.getName(),
                  registerFunc.getExpectedTypeName()));
        } else {
          tweakInfo.defaultValueNode = value;
        }
      }
    }
  }

  /**
   * Finds all calls to goog.tweak functions and emits warnings/errors if any of the calls have
   * issues.
   *
   * @return A map of {@link TweakInfo} structures, keyed by tweak ID.
   */
  private CollectTweaksResult collectTweaks(Node root) {
    CollectTweaks pass = new CollectTweaks();
    NodeTraversal.traverse(compiler, root, pass);

    Map<String, TweakInfo> tweakInfos = pass.allTweaks;
    for (TweakInfo tweakInfo : tweakInfos.values()) {
      tweakInfo.emitAllWarnings();
    }
    return new CollectTweaksResult(tweakInfos, pass.getOverridesCalls);
  }

  private static final class CollectTweaksResult {
    final Map<String, TweakInfo> tweakInfos;
    final List<TweakFunctionCall> getOverridesCalls;

    CollectTweaksResult(
        Map<String, TweakInfo> tweakInfos, List<TweakFunctionCall> getOverridesCalls) {
      this.tweakInfos = tweakInfos;
      this.getOverridesCalls = getOverridesCalls;
    }
  }

  /** Processes all calls to goog.tweak functions. */
  private final class CollectTweaks extends AbstractPostOrderCallback {
    final Map<String, TweakInfo> allTweaks = Maps.newHashMap();
    final List<TweakFunctionCall> getOverridesCalls = Lists.newArrayList();

    @SuppressWarnings("incomplete-switch")
    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (!n.isCall()) {
        return;
      }

      String callName = n.getFirstChild().getQualifiedName();
      TweakFunction tweakFunc = TWEAK_FUNCTIONS_MAP.get(callName);
      if (tweakFunc == null) {
        return;
      }

      if (tweakFunc == TweakFunction.GET_COMPILER_OVERRIDES) {
        getOverridesCalls.add(new TweakFunctionCall(tweakFunc, n));
        return;
      }

      // Ensure the first parameter (the tweak ID) is a string literal.
      Node tweakIdNode = n.getFirstChild().getNext();
      if (!tweakIdNode.isString()) {
        compiler.report(t.makeError(tweakIdNode, NON_LITERAL_TWEAK_ID_ERROR));
        return;
      }
      String tweakId = tweakIdNode.getString();

      // Make sure there is a TweakInfo structure for it.
      TweakInfo tweakInfo = allTweaks.get(tweakId);
      if (tweakInfo == null) {
        tweakInfo = new TweakInfo(tweakId);
        allTweaks.put(tweakId, tweakInfo);
      }

      switch (tweakFunc) {
        case REGISTER_BOOLEAN:
        case REGISTER_NUMBER:
        case REGISTER_STRING:
          // Ensure the ID contains only valid characters.
          if (!ID_MATCHER.matchesAllOf(tweakId)) {
            compiler.report(t.makeError(tweakIdNode, INVALID_TWEAK_ID_ERROR));
          }

          // Ensure tweaks are registered in the global scope.
          if (!t.inGlobalScope()) {
            compiler.report(t.makeError(n, NON_GLOBAL_TWEAK_INIT_ERROR, tweakId));
            break;
          }

          // Ensure tweaks are registered only once.
          if (tweakInfo.isRegistered()) {
            compiler.report(t.makeError(n, TWEAK_MULTIPLY_REGISTERED_ERROR, tweakId));
            break;
          }

          Node tweakDefaultValueNode = tweakIdNode.getNext().getNext();
          tweakInfo.addRegisterCall(t.getSourceName(), tweakFunc, n, tweakDefaultValueNode);
          break;
        case OVERRIDE_DEFAULT_VALUE:
          // Ensure tweaks overrides occur in the global scope.
          if (!t.inGlobalScope()) {
            compiler.report(t.makeError(n, NON_GLOBAL_TWEAK_INIT_ERROR, tweakId));
            break;
          }
          // Ensure tweak overrides occur before the tweak is registered.
          if (tweakInfo.isRegistered()) {
            compiler.report(t.makeError(n, TWEAK_OVERRIDE_AFTER_REGISTERED_ERROR, tweakId));
            break;
          }

          tweakDefaultValueNode = tweakIdNode.getNext();
          tweakInfo.addOverrideDefaultValueCall(
              t.getSourceName(), tweakFunc, n, tweakDefaultValueNode);
          break;
        case GET_BOOLEAN:
        case GET_NUMBER:
        case GET_STRING:
          tweakInfo.addGetterCall(t.getSourceName(), tweakFunc, n);
      }
    }
  }

  /** Holds information about a call to a goog.tweak function. */
  private static final class TweakFunctionCall {
    final TweakFunction tweakFunc;
    final Node callNode;
    final Node valueNode;

    TweakFunctionCall(TweakFunction tweakFunc, Node callNode) {
      this(tweakFunc, callNode, null);
    }

    TweakFunctionCall(TweakFunction tweakFunc, Node callNode, Node valueNode) {
      this.callNode = callNode;
      this.tweakFunc = tweakFunc;
      this.valueNode = valueNode;
    }

    Node getIdNode() {
      return callNode.getFirstChild().getNext();
    }
  }

  /** Stores information about a single tweak. */
  private final class TweakInfo {
    final String tweakId;
    final List<TweakFunctionCall> functionCalls;
    TweakFunctionCall registerCall;
    Node defaultValueNode;

    TweakInfo(String tweakId) {
      this.tweakId = tweakId;
      functionCalls = Lists.newArrayList();
    }

    /**
     * If this tweak is registered, then looks for type warnings in default value parameters and
     * getter functions. If it is not registered, emits an error for each function call.
     */
    void emitAllWarnings() {
      if (isRegistered()) {
        emitAllTypeWarnings();
      } else {
        emitUnknownTweakErrors();
      }
    }

    /**
     * Emits a warning for each default value parameter that has the wrong type and for each getter
     * function that was used for the wrong type of tweak.
     */
    void emitAllTypeWarnings() {
      for (TweakFunctionCall call : functionCalls) {
        Node valueNode = call.valueNode;
        TweakFunction tweakFunc = call.tweakFunc;
        TweakFunction registerFunc = registerCall.tweakFunc;
        if (valueNode != null) {
          // For register* and overrideDefaultValue calls, ensure the default
          // value is a literal of the correct type.
          if (!registerFunc.isValidNodeType(valueNode.getType())) {
            compiler.report(
                JSError.make(
                    valueNode,
                    INVALID_TWEAK_DEFAULT_VALUE_WARNING,
                    tweakId,
                    registerFunc.getName(),
                    registerFunc.getExpectedTypeName()));
          }
        } else if (tweakFunc.isGetterFunction()) {
          // For getter calls, ensure the correct getter was used.
          if (!tweakFunc.isCorrectRegisterFunction(registerFunc)) {
            compiler.report(
                JSError.make(
                    call.callNode,
                    TWEAK_WRONG_GETTER_TYPE_WARNING,
                    tweakFunc.getName(),
                    registerFunc.getName()));
          }
        }
      }
    }

    /** Emits an error for each function call that was found. */
    void emitUnknownTweakErrors() {
      for (TweakFunctionCall call : functionCalls) {
        compiler.report(JSError.make(call.getIdNode(), UNKNOWN_TWEAK_WARNING, tweakId));
      }
    }

    void addRegisterCall(
        String sourceName, TweakFunction tweakFunc, Node callNode, Node defaultValueNode) {
      registerCall = new TweakFunctionCall(tweakFunc, callNode, defaultValueNode);
      functionCalls.add(registerCall);
    }

    void addOverrideDefaultValueCall(
        String sourceName, TweakFunction tweakFunc, Node callNode, Node defaultValueNode) {
      functionCalls.add(new TweakFunctionCall(tweakFunc, callNode, defaultValueNode));
      this.defaultValueNode = defaultValueNode;
    }

    void addGetterCall(String sourceName, TweakFunction tweakFunc, Node callNode) {
      functionCalls.add(new TweakFunctionCall(tweakFunc, callNode));
    }

    boolean isRegistered() {
      return registerCall != null;
    }

    Node getDefaultValueNode() {
      Preconditions.checkState(isRegistered());
      // Use calls to goog.tweak.overrideDefaultValue() first.
      if (defaultValueNode != null) {
        return defaultValueNode;
      }
      // Use the value passed to the register function next.
      if (registerCall.valueNode != null) {
        return registerCall.valueNode;
      }
      // Otherwise, use the default value for the tweak's type.
      return registerCall.tweakFunc.createDefaultValueNode();
    }
  }
}
Example #24
0
/**
 * Checks for non side effecting statements such as
 * <pre>
 * var s = "this string is "
 *         "continued on the next line but you forgot the +";
 * x == foo();  // should that be '='?
 * foo();;  // probably just a stray-semicolon. Doesn't hurt to check though
 * </p>
 * and generates warnings.
 *
 */
final class CheckSideEffects extends AbstractPostOrderCallback implements HotSwapCompilerPass {

  static final DiagnosticType USELESS_CODE_ERROR =
      DiagnosticType.warning("JSC_USELESS_CODE", "Suspicious code. {0}");

  static final String PROTECTOR_FN = "JSCOMPILER_PRESERVE";

  private final CheckLevel level;

  private final List<Node> problemNodes = Lists.newArrayList();

  private final AbstractCompiler compiler;

  private final boolean protectSideEffectFreeCode;

  CheckSideEffects(AbstractCompiler compiler, CheckLevel level, boolean protectSideEffectFreeCode) {
    this.compiler = compiler;
    this.level = level;
    this.protectSideEffectFreeCode = protectSideEffectFreeCode;
  }

  @Override
  public void process(Node externs, Node root) {
    NodeTraversal.traverse(compiler, root, this);

    // Code with hidden side-effect code is common, for example
    // accessing "el.offsetWidth" forces a reflow in browsers, to allow this
    // will still allowing local dead code removal in general,
    // protect the "side-effect free" code in the source.
    //
    if (protectSideEffectFreeCode) {
      protectSideEffects();
    }
  }

  @Override
  public void hotSwapScript(Node scriptRoot, Node originalRoot) {
    NodeTraversal.traverse(compiler, scriptRoot, this);
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    // VOID nodes appear when there are extra semicolons at the BLOCK level.
    // I've been unable to think of any cases where this indicates a bug,
    // and apparently some people like keeping these semicolons around,
    // so we'll allow it.
    if (n.isEmpty() || n.isComma()) {
      return;
    }

    if (parent == null) {
      return;
    }

    int pt = parent.getType();
    if (pt == Token.COMMA) {
      Node gramps = parent.getParent();
      if (gramps.isCall() && parent == gramps.getFirstChild()) {
        // Semantically, a direct call to eval is different from an indirect
        // call to an eval. See Ecma-262 S15.1.2.1. So it's ok for the first
        // expression to a comma to be a no-op if it's used to indirect
        // an eval.
        if (n == parent.getFirstChild()
            && parent.getChildCount() == 2
            && n.getNext().isName()
            && "eval".equals(n.getNext().getString())) {
          return;
        }
      }

      if (n == parent.getLastChild()) {
        for (Node an : parent.getAncestors()) {
          int ancestorType = an.getType();
          if (ancestorType == Token.COMMA) continue;
          if (ancestorType != Token.EXPR_RESULT && ancestorType != Token.BLOCK) return;
          else break;
        }
      }
    } else if (pt != Token.EXPR_RESULT && pt != Token.BLOCK) {
      if (pt == Token.FOR
          && parent.getChildCount() == 4
          && (n == parent.getFirstChild() || n == parent.getFirstChild().getNext().getNext())) {
        // Fall through and look for warnings for the 1st and 3rd child
        // of a for.
      } else {
        return; // it might be ok to not have a side-effect
      }
    }

    boolean isSimpleOp = NodeUtil.isSimpleOperatorType(n.getType());
    if (isSimpleOp || !NodeUtil.mayHaveSideEffects(n, t.getCompiler())) {
      if (n.isQualifiedName() && n.getJSDocInfo() != null) {
        // This no-op statement was there so that JSDoc information could
        // be attached to the name. This check should not complain about it.
        return;
      } else if (n.isExprResult()) {
        // we already reported the problem when we visited the child.
        return;
      }

      String msg = "This code lacks side-effects. Is there a bug?";
      if (n.isString()) {
        msg = "Is there a missing '+' on the previous line?";
      } else if (isSimpleOp) {
        msg =
            "The result of the '"
                + Token.name(n.getType()).toLowerCase()
                + "' operator is not being used.";
      }

      t.getCompiler().report(t.makeError(n, level, USELESS_CODE_ERROR, msg));
      // TODO(johnlenz): determine if it is necessary to
      // try to protect side-effect free statements as well.
      if (!NodeUtil.isStatement(n)) {
        problemNodes.add(n);
      }
    }
  }

  /**
   * Protect side-effect free nodes by making them parameters to a extern function call. This call
   * will be removed after all the optimizations passes have run.
   */
  private void protectSideEffects() {
    if (!problemNodes.isEmpty()) {
      addExtern();
      for (Node n : problemNodes) {
        Node name = IR.name(PROTECTOR_FN).srcref(n);
        name.putBooleanProp(Node.IS_CONSTANT_NAME, true);
        Node replacement = IR.call(name).srcref(n);
        replacement.putBooleanProp(Node.FREE_CALL, true);
        n.getParent().replaceChild(n, replacement);
        replacement.addChildToBack(n);
      }
      compiler.reportCodeChange();
    }
  }

  private void addExtern() {
    Node name = IR.name(PROTECTOR_FN);
    name.putBooleanProp(Node.IS_CONSTANT_NAME, true);
    Node var = IR.var(name);
    // Add "@noalias" so we can strip the method when AliasExternals is enabled.
    JSDocInfoBuilder builder = new JSDocInfoBuilder(false);
    builder.recordNoAlias();
    var.setJSDocInfo(builder.build(var));
    CompilerInput input = compiler.getSynthesizedExternsInput();
    input.getAstRoot(compiler).addChildrenToBack(var);
    compiler.reportCodeChange();
  }

  /** Remove side-effect sync functions. */
  static class StripProtection extends AbstractPostOrderCallback implements CompilerPass {

    private final AbstractCompiler compiler;

    StripProtection(AbstractCompiler compiler) {
      this.compiler = compiler;
    }

    @Override
    public void process(Node externs, Node root) {
      NodeTraversal.traverse(compiler, root, this);
    }

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      if (n.isCall()) {
        Node target = n.getFirstChild();
        // TODO(johnlenz): add this to the coding convention
        // so we can remove goog.reflect.sinkValue as well.
        if (target.isName() && target.getString().equals(PROTECTOR_FN)) {
          Node expr = n.getLastChild();
          n.detachChildren();
          parent.replaceChild(n, expr);
        }
      }
    }
  }
}
/** Peephole optimization to fold constants (e.g. x + 1 + 7 --> x + 8). */
class PeepholeFoldConstants extends AbstractPeepholeOptimization {

  // TODO(johnlenz): optimizations should not be emiting errors. Move these to
  // a check pass.
  static final DiagnosticType INVALID_GETELEM_INDEX_ERROR =
      DiagnosticType.warning("JSC_INVALID_GETELEM_INDEX_ERROR", "Array index not integer: {0}");

  static final DiagnosticType INDEX_OUT_OF_BOUNDS_ERROR =
      DiagnosticType.warning("JSC_INDEX_OUT_OF_BOUNDS_ERROR", "Array index out of bounds: {0}");

  static final DiagnosticType NEGATING_A_NON_NUMBER_ERROR =
      DiagnosticType.warning(
          "JSC_NEGATING_A_NON_NUMBER_ERROR", "Can''t negate non-numeric value: {0}");

  static final DiagnosticType BITWISE_OPERAND_OUT_OF_RANGE =
      DiagnosticType.warning(
          "JSC_BITWISE_OPERAND_OUT_OF_RANGE",
          "Operand out of range, bitwise operation will lose information: {0}");

  static final DiagnosticType SHIFT_AMOUNT_OUT_OF_BOUNDS =
      DiagnosticType.warning(
          "JSC_SHIFT_AMOUNT_OUT_OF_BOUNDS", "Shift amount out of bounds (see right operand): {0}");

  static final DiagnosticType FRACTIONAL_BITWISE_OPERAND =
      DiagnosticType.warning("JSC_FRACTIONAL_BITWISE_OPERAND", "Fractional bitwise operand: {0}");

  private static final double MAX_FOLD_NUMBER = Math.pow(2, 53);

  private final boolean late;

  private final boolean shouldUseTypes;

  /**
   * @param late When late is false, this mean we are currently running before most of the other
   *     optimizations. In this case we would avoid optimizations that would make the code harder to
   *     analyze. When this is true, we would do anything to minimize for size.
   */
  PeepholeFoldConstants(boolean late, boolean shouldUseTypes) {
    this.late = late;
    this.shouldUseTypes = shouldUseTypes;
  }

  @Override
  Node optimizeSubtree(Node subtree) {
    switch (subtree.getType()) {
      case CALL:
        return tryFoldCall(subtree);
      case NEW:
        return tryFoldCtorCall(subtree);

      case TYPEOF:
        return tryFoldTypeof(subtree);

      case NOT:
      case POS:
      case NEG:
      case BITNOT:
        tryReduceOperandsForOp(subtree);
        return tryFoldUnaryOperator(subtree);

      case VOID:
        return tryReduceVoid(subtree);

      default:
        tryReduceOperandsForOp(subtree);
        return tryFoldBinaryOperator(subtree);
    }
  }

  private Node tryFoldBinaryOperator(Node subtree) {
    Node left = subtree.getFirstChild();

    if (left == null) {
      return subtree;
    }

    Node right = left.getNext();

    if (right == null) {
      return subtree;
    }

    // If we've reached here, node is truly a binary operator.
    switch (subtree.getType()) {
      case GETPROP:
        return tryFoldGetProp(subtree, left, right);

      case GETELEM:
        return tryFoldGetElem(subtree, left, right);

      case INSTANCEOF:
        return tryFoldInstanceof(subtree, left, right);

      case AND:
      case OR:
        return tryFoldAndOr(subtree, left, right);

      case LSH:
      case RSH:
      case URSH:
        return tryFoldShift(subtree, left, right);

      case ASSIGN:
        return tryFoldAssign(subtree, left, right);

      case ASSIGN_BITOR:
      case ASSIGN_BITXOR:
      case ASSIGN_BITAND:
      case ASSIGN_LSH:
      case ASSIGN_RSH:
      case ASSIGN_URSH:
      case ASSIGN_ADD:
      case ASSIGN_SUB:
      case ASSIGN_MUL:
      case ASSIGN_DIV:
      case ASSIGN_MOD:
        return tryUnfoldAssignOp(subtree, left, right);

      case ADD:
        return tryFoldAdd(subtree, left, right);

      case SUB:
      case DIV:
      case MOD:
        return tryFoldArithmeticOp(subtree, left, right);

      case MUL:
      case BITAND:
      case BITOR:
      case BITXOR:
        Node result = tryFoldArithmeticOp(subtree, left, right);
        if (result != subtree) {
          return result;
        }
        return tryFoldLeftChildOp(subtree, left, right);

      case LT:
      case GT:
      case LE:
      case GE:
      case EQ:
      case NE:
      case SHEQ:
      case SHNE:
        return tryFoldComparison(subtree, left, right);

      default:
        return subtree;
    }
  }

  private Node tryReduceVoid(Node n) {
    Node child = n.getFirstChild();
    if ((!child.isNumber() || child.getDouble() != 0.0) && !mayHaveSideEffects(n)) {
      n.replaceChild(child, IR.number(0));
      reportCodeChange();
    }
    return n;
  }

  private void tryReduceOperandsForOp(Node n) {
    switch (n.getType()) {
      case ADD:
        Node left = n.getFirstChild();
        Node right = n.getLastChild();
        if (!NodeUtil.mayBeString(left, shouldUseTypes)
            && !NodeUtil.mayBeString(right, shouldUseTypes)) {
          tryConvertOperandsToNumber(n);
        }
        break;
      case ASSIGN_BITOR:
      case ASSIGN_BITXOR:
      case ASSIGN_BITAND:
        // TODO(johnlenz): convert these to integers.
      case ASSIGN_LSH:
      case ASSIGN_RSH:
      case ASSIGN_URSH:
      case ASSIGN_SUB:
      case ASSIGN_MUL:
      case ASSIGN_MOD:
      case ASSIGN_DIV:
        tryConvertToNumber(n.getLastChild());
        break;
      case BITNOT:
      case BITOR:
      case BITXOR:
      case BITAND:
      case LSH:
      case RSH:
      case URSH:
      case SUB:
      case MUL:
      case MOD:
      case DIV:
      case POS:
      case NEG:
        tryConvertOperandsToNumber(n);
        break;
    }
  }

  private void tryConvertOperandsToNumber(Node n) {
    Node next;
    for (Node c = n.getFirstChild(); c != null; c = next) {
      next = c.getNext();
      tryConvertToNumber(c);
    }
  }

  private void tryConvertToNumber(Node n) {
    switch (n.getType()) {
      case NUMBER:
        // Nothing to do
        return;
      case AND:
      case OR:
      case COMMA:
        tryConvertToNumber(n.getLastChild());
        return;
      case HOOK:
        tryConvertToNumber(n.getSecondChild());
        tryConvertToNumber(n.getLastChild());
        return;
      case NAME:
        if (!NodeUtil.isUndefined(n)) {
          return;
        }
        break;
    }

    Double result = NodeUtil.getNumberValue(n, shouldUseTypes);
    if (result == null) {
      return;
    }

    double value = result;

    Node replacement = NodeUtil.numberNode(value, n);
    if (replacement.isEquivalentTo(n)) {
      return;
    }

    n.getParent().replaceChild(n, replacement);
    reportCodeChange();
  }

  /**
   * Folds 'typeof(foo)' if foo is a literal, e.g. typeof("bar") --> "string" typeof(6) --> "number"
   */
  private Node tryFoldTypeof(Node originalTypeofNode) {
    Preconditions.checkArgument(originalTypeofNode.isTypeOf());

    Node argumentNode = originalTypeofNode.getFirstChild();
    if (argumentNode == null || !NodeUtil.isLiteralValue(argumentNode, true)) {
      return originalTypeofNode;
    }

    String typeNameString = null;

    switch (argumentNode.getType()) {
      case FUNCTION:
        typeNameString = "function";
        break;
      case STRING:
        typeNameString = "string";
        break;
      case NUMBER:
        typeNameString = "number";
        break;
      case TRUE:
      case FALSE:
        typeNameString = "boolean";
        break;
      case NULL:
      case OBJECTLIT:
      case ARRAYLIT:
        typeNameString = "object";
        break;
      case VOID:
        typeNameString = "undefined";
        break;
      case NAME:
        // We assume here that programs don't change the value of the
        // keyword undefined to something other than the value undefined.
        if ("undefined".equals(argumentNode.getString())) {
          typeNameString = "undefined";
        }
        break;
    }

    if (typeNameString != null) {
      Node newNode = IR.string(typeNameString);
      originalTypeofNode.getParent().replaceChild(originalTypeofNode, newNode);
      reportCodeChange();

      return newNode;
    }

    return originalTypeofNode;
  }

  private Node tryFoldUnaryOperator(Node n) {
    Preconditions.checkState(n.hasOneChild(), n);

    Node left = n.getFirstChild();
    Node parent = n.getParent();

    if (left == null) {
      return n;
    }

    TernaryValue leftVal = NodeUtil.getPureBooleanValue(left);
    if (leftVal == TernaryValue.UNKNOWN) {
      return n;
    }

    switch (n.getType()) {
      case NOT:
        // Don't fold !0 and !1 back to false.
        if (late && left.isNumber()) {
          double numValue = left.getDouble();
          if (numValue == 0 || numValue == 1) {
            return n;
          }
        }
        Node replacementNode = NodeUtil.booleanNode(!leftVal.toBoolean(true));
        parent.replaceChild(n, replacementNode);
        reportCodeChange();
        return replacementNode;
      case POS:
        if (NodeUtil.isNumericResult(left)) {
          // POS does nothing to numeric values.
          parent.replaceChild(n, left.detachFromParent());
          reportCodeChange();
          return left;
        }
        return n;
      case NEG:
        if (left.isName()) {
          if (left.getString().equals("Infinity")) {
            // "-Infinity" is valid and a literal, don't modify it.
            return n;
          } else if (left.getString().equals("NaN")) {
            // "-NaN" is "NaN".
            n.removeChild(left);
            parent.replaceChild(n, left);
            reportCodeChange();
            return left;
          }
        }

        if (left.isNumber()) {
          double negNum = -left.getDouble();

          Node negNumNode = IR.number(negNum);
          parent.replaceChild(n, negNumNode);
          reportCodeChange();
          return negNumNode;
        } else {
          // left is not a number node, so do not replace, but warn the
          // user because they can't be doing anything good
          report(NEGATING_A_NON_NUMBER_ERROR, left);
          return n;
        }
      case BITNOT:
        try {
          double val = left.getDouble();
          if (val >= Integer.MIN_VALUE && val <= Integer.MAX_VALUE) {
            int intVal = (int) val;
            if (intVal == val) {
              Node notIntValNode = IR.number(~intVal);
              parent.replaceChild(n, notIntValNode);
              reportCodeChange();
              return notIntValNode;
            } else {
              report(FRACTIONAL_BITWISE_OPERAND, left);
              return n;
            }
          } else {
            report(BITWISE_OPERAND_OUT_OF_RANGE, left);
            return n;
          }
        } catch (UnsupportedOperationException ex) {
          // left is not a number node, so do not replace, but warn the
          // user because they can't be doing anything good
          report(NEGATING_A_NON_NUMBER_ERROR, left);
          return n;
        }
      default:
        return n;
    }
  }

  /** Try to fold {@code left instanceof right} into {@code true} or {@code false}. */
  private Node tryFoldInstanceof(Node n, Node left, Node right) {
    Preconditions.checkArgument(n.isInstanceOf());

    // TODO(johnlenz) Use type information if available to fold
    // instanceof.
    if (NodeUtil.isLiteralValue(left, true) && !mayHaveSideEffects(right)) {

      Node replacementNode = null;

      if (NodeUtil.isImmutableValue(left)) {
        // Non-object types are never instances.
        replacementNode = IR.falseNode();
      } else if (right.isName() && "Object".equals(right.getString())) {
        replacementNode = IR.trueNode();
      }

      if (replacementNode != null) {
        n.getParent().replaceChild(n, replacementNode);
        reportCodeChange();
        return replacementNode;
      }
    }

    return n;
  }

  private Node tryFoldAssign(Node n, Node left, Node right) {
    Preconditions.checkArgument(n.isAssign());

    if (!late) {
      return n;
    }

    // Tries to convert x = x + y -> x += y;
    if (!right.hasChildren() || right.getSecondChild() != right.getLastChild()) {
      // RHS must have two children.
      return n;
    }

    if (mayHaveSideEffects(left)) {
      return n;
    }

    Node newRight;
    if (areNodesEqualForInlining(left, right.getFirstChild())) {
      newRight = right.getLastChild();
    } else if (NodeUtil.isCommutative(right.getType())
        && areNodesEqualForInlining(left, right.getLastChild())) {
      newRight = right.getFirstChild();
    } else {
      return n;
    }

    Token newType = null;
    switch (right.getType()) {
      case ADD:
        newType = Token.ASSIGN_ADD;
        break;
      case BITAND:
        newType = Token.ASSIGN_BITAND;
        break;
      case BITOR:
        newType = Token.ASSIGN_BITOR;
        break;
      case BITXOR:
        newType = Token.ASSIGN_BITXOR;
        break;
      case DIV:
        newType = Token.ASSIGN_DIV;
        break;
      case LSH:
        newType = Token.ASSIGN_LSH;
        break;
      case MOD:
        newType = Token.ASSIGN_MOD;
        break;
      case MUL:
        newType = Token.ASSIGN_MUL;
        break;
      case RSH:
        newType = Token.ASSIGN_RSH;
        break;
      case SUB:
        newType = Token.ASSIGN_SUB;
        break;
      case URSH:
        newType = Token.ASSIGN_URSH;
        break;
      default:
        return n;
    }

    Node newNode = new Node(newType, left.detachFromParent(), newRight.detachFromParent());
    n.getParent().replaceChild(n, newNode);

    reportCodeChange();

    return newNode;
  }

  private Node tryUnfoldAssignOp(Node n, Node left, Node right) {
    if (late) {
      return n;
    }

    if (!n.hasChildren() || n.getSecondChild() != n.getLastChild()) {
      return n;
    }

    if (mayHaveSideEffects(left)) {
      return n;
    }

    // Tries to convert x += y -> x = x + y;
    Token op = NodeUtil.getOpFromAssignmentOp(n);
    Node replacement =
        IR.assign(
            left.detachFromParent(),
            new Node(op, left.cloneTree(), right.detachFromParent()).srcref(n));
    n.getParent().replaceChild(n, replacement);
    reportCodeChange();

    return replacement;
  }

  /** Try to fold a AND/OR node. */
  private Node tryFoldAndOr(Node n, Node left, Node right) {
    Node parent = n.getParent();

    Node result = null;

    Token type = n.getType();

    TernaryValue leftVal = NodeUtil.getImpureBooleanValue(left);

    if (leftVal != TernaryValue.UNKNOWN) {
      boolean lval = leftVal.toBoolean(true);

      // (TRUE || x) => TRUE (also, (3 || x) => 3)
      // (FALSE && x) => FALSE
      if (lval && type == Token.OR || !lval && type == Token.AND) {
        result = left;

      } else if (!mayHaveSideEffects(left)) {
        // (FALSE || x) => x
        // (TRUE && x) => x
        result = right;
      } else {
        // Left side may have side effects, but we know its boolean value.
        // e.g. true_with_sideeffects || foo() => true_with_sideeffects, foo()
        // or: false_with_sideeffects && foo() => false_with_sideeffects, foo()
        // This, combined with PeepholeRemoveDeadCode, helps reduce expressions
        // like "x() || false || z()".
        n.detachChildren();
        result = IR.comma(left, right);
      }
    }

    // Note: Right hand side folding is handled by
    // PeepholeMinimizeConditions#tryMinimizeCondition

    if (result != null) {
      // Fold it!
      n.detachChildren();
      parent.replaceChild(n, result);
      reportCodeChange();

      return result;
    } else {
      return n;
    }
  }

  /**
   * Expressions such as [foo() + 'a' + 'b'] generate parse trees where no node has two const
   * children ((foo() + 'a') + 'b'), so tryFoldAdd() won't fold it -- tryFoldLeftChildAdd() will
   * (for Strings). Specifically, it folds Add expressions where: - The left child is also and add
   * expression - The right child is a constant value - The left child's right child is a STRING
   * constant.
   */
  private Node tryFoldChildAddString(Node n, Node left, Node right) {

    if (NodeUtil.isLiteralValue(right, false) && left.isAdd()) {

      Node ll = left.getFirstChild();
      Node lr = ll.getNext();

      // Left's right child MUST be a string. We would not want to fold
      // foo() + 2 + 'a' because we don't know what foo() will return, and
      // therefore we don't know if left is a string concat, or a numeric add.
      if (lr.isString()) {
        String leftString = NodeUtil.getStringValue(lr);
        String rightString = NodeUtil.getStringValue(right);
        if (leftString != null && rightString != null) {
          left.removeChild(ll);
          String result = leftString + rightString;
          n.replaceChild(left, ll);
          n.replaceChild(right, IR.string(result));
          reportCodeChange();
          return n;
        }
      }
    }

    if (NodeUtil.isLiteralValue(left, false) && right.isAdd()) {

      Node rl = right.getFirstChild();
      Node rr = right.getLastChild();

      // Left's right child MUST be a string. We would not want to fold
      // foo() + 2 + 'a' because we don't know what foo() will return, and
      // therefore we don't know if left is a string concat, or a numeric add.
      if (rl.isString()) {
        String leftString = NodeUtil.getStringValue(left);
        String rightString = NodeUtil.getStringValue(rl);
        if (leftString != null && rightString != null) {
          right.removeChild(rr);
          String result = leftString + rightString;
          n.replaceChild(right, rr);
          n.replaceChild(left, IR.string(result));
          reportCodeChange();
          return n;
        }
      }
    }

    return n;
  }

  /** Try to fold an ADD node with constant operands */
  private Node tryFoldAddConstantString(Node n, Node left, Node right) {
    if (left.isString() || right.isString() || left.isArrayLit() || right.isArrayLit()) {
      // Add strings.
      String leftString = NodeUtil.getStringValue(left);
      String rightString = NodeUtil.getStringValue(right);
      if (leftString != null && rightString != null) {
        Node newStringNode = IR.string(leftString + rightString);
        n.getParent().replaceChild(n, newStringNode);
        reportCodeChange();
        return newStringNode;
      }
    }

    return n;
  }

  /** Try to fold arithmetic binary operators */
  private Node tryFoldArithmeticOp(Node n, Node left, Node right) {
    Node result = performArithmeticOp(n.getType(), left, right);
    if (result != null) {
      result.useSourceInfoIfMissingFromForTree(n);
      n.getParent().replaceChild(n, result);
      reportCodeChange();
      return result;
    }
    return n;
  }

  /** Try to fold arithmetic binary operators */
  private Node performArithmeticOp(Token opType, Node left, Node right) {
    // Unlike other operations, ADD operands are not always converted
    // to Number.
    if (opType == Token.ADD
        && (NodeUtil.mayBeString(left, shouldUseTypes)
            || NodeUtil.mayBeString(right, shouldUseTypes))) {
      return null;
    }

    double result;

    // TODO(johnlenz): Handle NaN with unknown value. BIT ops convert NaN
    // to zero so this is a little awkward here.

    Double lValObj = NodeUtil.getNumberValue(left, shouldUseTypes);
    if (lValObj == null) {
      return null;
    }
    Double rValObj = NodeUtil.getNumberValue(right, shouldUseTypes);
    if (rValObj == null) {
      return null;
    }

    double lval = lValObj;
    double rval = rValObj;

    switch (opType) {
      case BITAND:
        result = NodeUtil.toInt32(lval) & NodeUtil.toInt32(rval);
        break;
      case BITOR:
        result = NodeUtil.toInt32(lval) | NodeUtil.toInt32(rval);
        break;
      case BITXOR:
        result = NodeUtil.toInt32(lval) ^ NodeUtil.toInt32(rval);
        break;
      case ADD:
        result = lval + rval;
        break;
      case SUB:
        result = lval - rval;
        break;
      case MUL:
        result = lval * rval;
        break;
      case MOD:
        if (rval == 0) {
          return null;
        }
        result = lval % rval;
        break;
      case DIV:
        if (rval == 0) {
          return null;
        }
        result = lval / rval;
        break;
      default:
        throw new Error("Unexpected arithmetic operator");
    }

    // TODO(johnlenz): consider removing the result length check.
    // length of the left and right value plus 1 byte for the operator.
    if ((String.valueOf(result).length()
                <= String.valueOf(lval).length() + String.valueOf(rval).length() + 1

            // Do not try to fold arithmetic for numbers > 2^53. After that
            // point, fixed-point math starts to break down and become inaccurate.
            && Math.abs(result) <= MAX_FOLD_NUMBER)
        || Double.isNaN(result)
        || result == Double.POSITIVE_INFINITY
        || result == Double.NEGATIVE_INFINITY) {
      return NodeUtil.numberNode(result, null);
    }
    return null;
  }

  /**
   * Expressions such as [foo() * 10 * 20] generate parse trees where no node has two const children
   * ((foo() * 10) * 20), so performArithmeticOp() won't fold it -- tryFoldLeftChildOp() will.
   * Specifically, it folds associative expressions where: - The left child is also an associative
   * expression of the same time. - The right child is a constant NUMBER constant. - The left
   * child's right child is a NUMBER constant.
   */
  private Node tryFoldLeftChildOp(Node n, Node left, Node right) {
    Token opType = n.getType();
    Preconditions.checkState(
        (NodeUtil.isAssociative(opType) && NodeUtil.isCommutative(opType)) || n.isAdd());

    Preconditions.checkState(!n.isAdd() || !NodeUtil.mayBeString(n, shouldUseTypes));

    // Use getNumberValue to handle constants like "NaN" and "Infinity"
    // other values are converted to numbers elsewhere.
    Double rightValObj = NodeUtil.getNumberValue(right, shouldUseTypes);
    if (rightValObj != null && left.getType() == opType) {
      Preconditions.checkState(left.getChildCount() == 2);

      Node ll = left.getFirstChild();
      Node lr = ll.getNext();

      Node valueToCombine = ll;
      Node replacement = performArithmeticOp(opType, valueToCombine, right);
      if (replacement == null) {
        valueToCombine = lr;
        replacement = performArithmeticOp(opType, valueToCombine, right);
      }
      if (replacement != null) {
        // Remove the child that has been combined
        left.removeChild(valueToCombine);
        // Replace the left op with the remaining child.
        n.replaceChild(left, left.removeFirstChild());
        // New "-Infinity" node need location info explicitly
        // added.
        replacement.useSourceInfoIfMissingFromForTree(right);
        n.replaceChild(right, replacement);
        reportCodeChange();
      }
    }

    return n;
  }

  private Node tryFoldAdd(Node node, Node left, Node right) {
    Preconditions.checkArgument(node.isAdd());

    if (NodeUtil.mayBeString(node, shouldUseTypes)) {
      if (NodeUtil.isLiteralValue(left, false) && NodeUtil.isLiteralValue(right, false)) {
        // '6' + 7
        return tryFoldAddConstantString(node, left, right);
      } else {
        // a + 7 or 6 + a
        return tryFoldChildAddString(node, left, right);
      }
    } else {
      // Try arithmetic add
      Node result = tryFoldArithmeticOp(node, left, right);
      if (result != node) {
        return result;
      }
      return tryFoldLeftChildOp(node, left, right);
    }
  }

  /** Try to fold shift operations */
  private Node tryFoldShift(Node n, Node left, Node right) {
    if (left.isNumber() && right.isNumber()) {

      double result;
      double lval = left.getDouble();
      double rval = right.getDouble();

      // check ranges.  We do not do anything that would clip the double to
      // a 32-bit range, since the user likely does not intend that.
      if (lval < Integer.MIN_VALUE) {
        report(BITWISE_OPERAND_OUT_OF_RANGE, left);
        return n;
      }
      // only the lower 5 bits are used when shifting, so don't do anything
      // if the shift amount is outside [0,32)
      if (!(rval >= 0 && rval < 32)) {
        report(SHIFT_AMOUNT_OUT_OF_BOUNDS, n);
        return n;
      }

      int rvalInt = (int) rval;
      if (rvalInt != rval) {
        report(FRACTIONAL_BITWISE_OPERAND, right);
        return n;
      }

      switch (n.getType()) {
        case LSH:
        case RSH:
          // Convert the numbers to ints
          if (lval > Integer.MAX_VALUE) {
            report(BITWISE_OPERAND_OUT_OF_RANGE, left);
            return n;
          }
          int lvalInt = (int) lval;
          if (lvalInt != lval) {
            report(FRACTIONAL_BITWISE_OPERAND, left);
            return n;
          }
          if (n.getType() == Token.LSH) {
            result = lvalInt << rvalInt;
          } else {
            result = lvalInt >> rvalInt;
          }
          break;
        case URSH:
          // JavaScript handles zero shifts on signed numbers differently than
          // Java as an Java int can not represent the unsigned 32-bit number
          // where JavaScript can so use a long here.
          long maxUint32 = 0xffffffffL;
          if (lval > maxUint32) {
            report(BITWISE_OPERAND_OUT_OF_RANGE, left);
            return n;
          }
          long lvalLong = (long) lval;
          if (lvalLong != lval) {
            report(FRACTIONAL_BITWISE_OPERAND, left);
            return n;
          }
          result = (lvalLong & maxUint32) >>> rvalInt;
          break;
        default:
          throw new AssertionError("Unknown shift operator: " + n.getType());
      }

      Node newNumber = IR.number(result);
      n.getParent().replaceChild(n, newNumber);
      reportCodeChange();

      return newNumber;
    }

    return n;
  }

  /** Try to fold comparison nodes, e.g == */
  private Node tryFoldComparison(Node n, Node left, Node right) {
    TernaryValue result = evaluateComparison(n.getType(), left, right, shouldUseTypes);
    if (result == TernaryValue.UNKNOWN) {
      return n;
    }

    Node newNode = NodeUtil.booleanNode(result.toBoolean(true));
    n.getParent().replaceChild(n, newNode);
    reportCodeChange();

    return newNode;
  }

  /** http://www.ecma-international.org/ecma-262/6.0/#sec-abstract-relational-comparison */
  private static TernaryValue tryAbstractRelationalComparison(
      Node left, Node right, boolean useTypes, boolean willNegate) {
    // First, try to evaluate based on the general type.
    ValueType leftValueType = NodeUtil.getKnownValueType(left);
    ValueType rightValueType = NodeUtil.getKnownValueType(right);
    if (leftValueType != ValueType.UNDETERMINED && rightValueType != ValueType.UNDETERMINED) {
      if (leftValueType == ValueType.STRING && rightValueType == ValueType.STRING) {
        String lv = NodeUtil.getStringValue(left);
        String rv = NodeUtil.getStringValue(right);
        if (lv != null && rv != null) {
          // In JS, browsers parse \v differently. So do not compare strings if one contains \v.
          if (lv.indexOf('\u000B') != -1 || rv.indexOf('\u000B') != -1) {
            return TernaryValue.UNKNOWN;
          } else {
            return TernaryValue.forBoolean(lv.compareTo(rv) < 0);
          }
        } else if (left.isTypeOf()
            && right.isTypeOf()
            && left.getFirstChild().isName()
            && right.getFirstChild().isName()
            && left.getFirstChild().getString().equals(right.getFirstChild().getString())) {
          // Special case: `typeof a < typeof a` is always false.
          return TernaryValue.FALSE;
        }
      }
    }
    // Then, try to evaluate based on the value of the node. Try comparing as numbers.
    Double lv = NodeUtil.getNumberValue(left, useTypes);
    Double rv = NodeUtil.getNumberValue(right, useTypes);
    if (lv == null || rv == null) {
      // Special case: `x < x` is always false.
      //
      // TODO(moz): If we knew the named value wouldn't be NaN, it would be nice to handle
      // LE and GE. We should use type information if available here.
      if (!willNegate && left.isName() && right.isName()) {
        if (left.getString().equals(right.getString())) {
          return TernaryValue.FALSE;
        }
      }
      return TernaryValue.UNKNOWN;
    }
    if (Double.isNaN(lv) || Double.isNaN(rv)) {
      return TernaryValue.forBoolean(willNegate);
    } else {
      return TernaryValue.forBoolean(lv.doubleValue() < rv.doubleValue());
    }
  }

  /** http://www.ecma-international.org/ecma-262/6.0/#sec-abstract-equality-comparison */
  private static TernaryValue tryAbstractEqualityComparison(
      Node left, Node right, boolean useTypes) {
    // Evaluate based on the general type.
    ValueType leftValueType = NodeUtil.getKnownValueType(left);
    ValueType rightValueType = NodeUtil.getKnownValueType(right);
    if (leftValueType != ValueType.UNDETERMINED && rightValueType != ValueType.UNDETERMINED) {
      // Delegate to strict equality comparison for values of the same type.
      if (leftValueType == rightValueType) {
        return tryStrictEqualityComparison(left, right, useTypes);
      }
      if ((leftValueType == ValueType.NULL && rightValueType == ValueType.VOID)
          || (leftValueType == ValueType.VOID && rightValueType == ValueType.NULL)) {
        return TernaryValue.TRUE;
      }
      if ((leftValueType == ValueType.NUMBER && rightValueType == ValueType.STRING)
          || rightValueType == ValueType.BOOLEAN) {
        Double rv = NodeUtil.getNumberValue(right, useTypes);
        return rv == null
            ? TernaryValue.UNKNOWN
            : tryAbstractEqualityComparison(left, IR.number(rv), useTypes);
      }
      if ((leftValueType == ValueType.STRING && rightValueType == ValueType.NUMBER)
          || leftValueType == ValueType.BOOLEAN) {
        Double lv = NodeUtil.getNumberValue(left, useTypes);
        return lv == null
            ? TernaryValue.UNKNOWN
            : tryAbstractEqualityComparison(IR.number(lv), right, useTypes);
      }
      if ((leftValueType == ValueType.STRING || leftValueType == ValueType.NUMBER)
          && rightValueType == ValueType.OBJECT) {
        return TernaryValue.UNKNOWN;
      }
      if (leftValueType == ValueType.OBJECT
          && (rightValueType == ValueType.STRING || rightValueType == ValueType.NUMBER)) {
        return TernaryValue.UNKNOWN;
      }
      return TernaryValue.FALSE;
    }
    // In general, the rest of the cases cannot be folded.
    return TernaryValue.UNKNOWN;
  }

  /** http://www.ecma-international.org/ecma-262/6.0/#sec-strict-equality-comparison */
  private static TernaryValue tryStrictEqualityComparison(Node left, Node right, boolean useTypes) {
    // First, try to evaluate based on the general type.
    ValueType leftValueType = NodeUtil.getKnownValueType(left);
    ValueType rightValueType = NodeUtil.getKnownValueType(right);
    if (leftValueType != ValueType.UNDETERMINED && rightValueType != ValueType.UNDETERMINED) {
      // Strict equality can only be true for values of the same type.
      if (leftValueType != rightValueType) {
        return TernaryValue.FALSE;
      }
      switch (leftValueType) {
        case VOID:
        case NULL:
          return TernaryValue.TRUE;
        case NUMBER:
          {
            if (NodeUtil.isNaN(left)) {
              return TernaryValue.FALSE;
            }
            if (NodeUtil.isNaN(right)) {
              return TernaryValue.FALSE;
            }
            Double lv = NodeUtil.getNumberValue(left, useTypes);
            Double rv = NodeUtil.getNumberValue(right, useTypes);
            if (lv != null && rv != null) {
              return TernaryValue.forBoolean(lv.doubleValue() == rv.doubleValue());
            }
            break;
          }
        case STRING:
          {
            String lv = NodeUtil.getStringValue(left);
            String rv = NodeUtil.getStringValue(right);
            if (lv != null && rv != null) {
              // In JS, browsers parse \v differently. So do not consider strings
              // equal if one contains \v.
              if (lv.indexOf('\u000B') != -1 || rv.indexOf('\u000B') != -1) {
                return TernaryValue.UNKNOWN;
              } else {
                return lv.equals(rv) ? TernaryValue.TRUE : TernaryValue.FALSE;
              }
            } else if (left.isTypeOf()
                && right.isTypeOf()
                && left.getFirstChild().isName()
                && right.getFirstChild().isName()
                && left.getFirstChild().getString().equals(right.getFirstChild().getString())) {
              // Special case, typeof a == typeof a is always true.
              return TernaryValue.TRUE;
            }
            break;
          }
        case BOOLEAN:
          {
            TernaryValue lv = NodeUtil.getPureBooleanValue(left);
            TernaryValue rv = NodeUtil.getPureBooleanValue(right);
            return lv.and(rv).or(lv.not().and(rv.not()));
          }
        default: // Symbol and Object cannot be folded in the general case.
          return TernaryValue.UNKNOWN;
      }
    }

    // Then, try to evaluate based on the value of the node. There's only one special case:
    // Any strict equality comparison against NaN returns false.
    if (NodeUtil.isNaN(left) || NodeUtil.isNaN(right)) {
      return TernaryValue.FALSE;
    }
    return TernaryValue.UNKNOWN;
  }

  static TernaryValue evaluateComparison(Token op, Node left, Node right, boolean useTypes) {
    // Don't try to minimize side-effects here.
    if (NodeUtil.mayHaveSideEffects(left) || NodeUtil.mayHaveSideEffects(right)) {
      return TernaryValue.UNKNOWN;
    }

    switch (op) {
      case EQ:
        return tryAbstractEqualityComparison(left, right, useTypes);
      case NE:
        return tryAbstractEqualityComparison(left, right, useTypes).not();
      case SHEQ:
        return tryStrictEqualityComparison(left, right, useTypes);
      case SHNE:
        return tryStrictEqualityComparison(left, right, useTypes).not();
      case LT:
        return tryAbstractRelationalComparison(left, right, useTypes, false);
      case GT:
        return tryAbstractRelationalComparison(right, left, useTypes, false);
      case LE:
        return tryAbstractRelationalComparison(right, left, useTypes, true).not();
      case GE:
        return tryAbstractRelationalComparison(left, right, useTypes, true).not();
    }
    throw new IllegalStateException("Unexpected operator for comparison");
  }

  /**
   * Try to fold away unnecessary object instantiation. e.g. this[new String('eval')] -> this.eval
   */
  private Node tryFoldCtorCall(Node n) {
    Preconditions.checkArgument(n.isNew());

    // we can remove this for GETELEM calls (anywhere else?)
    if (inForcedStringContext(n)) {
      return tryFoldInForcedStringContext(n);
    }
    return n;
  }

  /** Remove useless calls: Object.defineProperties(o, {}) -> o */
  private Node tryFoldCall(Node n) {
    Preconditions.checkArgument(n.isCall());

    if (NodeUtil.isObjectDefinePropertiesDefinition(n)) {
      Node srcObj = n.getLastChild();
      if (srcObj.isObjectLit() && !srcObj.hasChildren()) {
        Node parent = n.getParent();
        Node destObj = n.getSecondChild().detachFromParent();
        parent.replaceChild(n, destObj);
        reportCodeChange();
      }
    }
    return n;
  }

  /** Returns whether this node must be coerced to a string. */
  private static boolean inForcedStringContext(Node n) {
    if (n.getParent().isGetElem() && n.getParent().getLastChild() == n) {
      return true;
    }

    // we can fold in the case "" + new String("")
    return n.getParent().isAdd();
  }

  private Node tryFoldInForcedStringContext(Node n) {
    // For now, we only know how to fold ctors.
    Preconditions.checkArgument(n.isNew());

    Node objectType = n.getFirstChild();
    if (!objectType.isName()) {
      return n;
    }

    if (objectType.getString().equals("String")) {
      Node value = objectType.getNext();
      String stringValue = null;
      if (value == null) {
        stringValue = "";
      } else {
        if (!NodeUtil.isImmutableValue(value)) {
          return n;
        }

        stringValue = NodeUtil.getStringValue(value);
      }

      if (stringValue == null) {
        return n;
      }

      Node parent = n.getParent();
      Node newString = IR.string(stringValue);

      parent.replaceChild(n, newString);
      newString.useSourceInfoIfMissingFrom(parent);
      reportCodeChange();

      return newString;
    }
    return n;
  }

  /** Try to fold array-element. e.g [1, 2, 3][10]; */
  private Node tryFoldGetElem(Node n, Node left, Node right) {
    Preconditions.checkArgument(n.isGetElem());

    if (left.isObjectLit()) {
      return tryFoldObjectPropAccess(n, left, right);
    }

    if (left.isArrayLit()) {
      return tryFoldArrayAccess(n, left, right);
    }

    if (left.isString()) {
      return tryFoldStringArrayAccess(n, left, right);
    }
    return n;
  }

  /** Try to fold array-length. e.g [1, 2, 3].length ==> 3, [x, y].length ==> 2 */
  private Node tryFoldGetProp(Node n, Node left, Node right) {
    Preconditions.checkArgument(n.isGetProp());

    if (left.isObjectLit()) {
      return tryFoldObjectPropAccess(n, left, right);
    }

    if (right.isString() && right.getString().equals("length")) {
      int knownLength = -1;
      switch (left.getType()) {
        case ARRAYLIT:
          if (mayHaveSideEffects(left)) {
            // Nope, can't fold this, without handling the side-effects.
            return n;
          }
          knownLength = left.getChildCount();
          break;
        case STRING:
          knownLength = left.getString().length();
          break;
        default:
          // Not a foldable case, forget it.
          return n;
      }

      Preconditions.checkState(knownLength != -1);
      Node lengthNode = IR.number(knownLength);
      n.getParent().replaceChild(n, lengthNode);
      reportCodeChange();

      return lengthNode;
    }

    return n;
  }

  private Node tryFoldArrayAccess(Node n, Node left, Node right) {
    // If GETPROP/GETELEM is used as assignment target the array literal is
    // acting as a temporary we can't fold it here:
    //    "[][0] += 1"
    if (NodeUtil.isAssignmentTarget(n)) {
      return n;
    }

    if (!right.isNumber()) {
      // Sometimes people like to use complex expressions to index into
      // arrays, or strings to index into array methods.
      return n;
    }

    double index = right.getDouble();
    int intIndex = (int) index;
    if (intIndex != index) {
      report(INVALID_GETELEM_INDEX_ERROR, right);
      return n;
    }

    if (intIndex < 0) {
      report(INDEX_OUT_OF_BOUNDS_ERROR, right);
      return n;
    }

    Node current = left.getFirstChild();
    Node elem = null;
    for (int i = 0; current != null; i++) {
      if (i != intIndex) {
        if (mayHaveSideEffects(current)) {
          return n;
        }
      } else {
        elem = current;
      }

      current = current.getNext();
    }

    if (elem == null) {
      report(INDEX_OUT_OF_BOUNDS_ERROR, right);
      return n;
    }

    if (elem.isEmpty()) {
      elem = NodeUtil.newUndefinedNode(elem);
    } else {
      left.removeChild(elem);
    }

    // Replace the entire GETELEM with the value
    n.getParent().replaceChild(n, elem);
    reportCodeChange();
    return elem;
  }

  private Node tryFoldStringArrayAccess(Node n, Node left, Node right) {
    // If GETPROP/GETELEM is used as assignment target the array literal is
    // acting as a temporary we can't fold it here:
    //    "[][0] += 1"
    if (NodeUtil.isAssignmentTarget(n)) {
      return n;
    }

    if (!right.isNumber()) {
      // Sometimes people like to use complex expressions to index into
      // arrays, or strings to index into array methods.
      return n;
    }

    double index = right.getDouble();
    int intIndex = (int) index;
    if (intIndex != index) {
      report(INVALID_GETELEM_INDEX_ERROR, right);
      return n;
    }

    if (intIndex < 0) {
      report(INDEX_OUT_OF_BOUNDS_ERROR, right);
      return n;
    }

    Preconditions.checkState(left.isString());
    String value = left.getString();
    if (intIndex >= value.length()) {
      report(INDEX_OUT_OF_BOUNDS_ERROR, right);
      return n;
    }

    char c = 0;
    // Note: For now skip the strings with unicode
    // characters as I don't understand the differences
    // between Java and JavaScript.
    for (int i = 0; i <= intIndex; i++) {
      c = value.charAt(i);
      if (c < 32 || c > 127) {
        return n;
      }
    }
    Node elem = IR.string(Character.toString(c));

    // Replace the entire GETELEM with the value
    n.getParent().replaceChild(n, elem);
    reportCodeChange();
    return elem;
  }

  private Node tryFoldObjectPropAccess(Node n, Node left, Node right) {
    Preconditions.checkArgument(NodeUtil.isGet(n));

    if (!left.isObjectLit() || !right.isString()) {
      return n;
    }

    if (NodeUtil.isAssignmentTarget(n)) {
      // If GETPROP/GETELEM is used as assignment target the object literal is
      // acting as a temporary we can't fold it here:
      //    "{a:x}.a += 1" is not "x += 1"
      return n;
    }

    // find the last definition in the object literal
    Node key = null;
    Node value = null;
    for (Node c = left.getFirstChild(); c != null; c = c.getNext()) {
      if (c.getString().equals(right.getString())) {
        switch (c.getType()) {
          case SETTER_DEF:
            continue;
          case GETTER_DEF:
          case STRING_KEY:
            if (value != null && mayHaveSideEffects(value)) {
              // The previously found value had side-effects
              return n;
            }
            key = c;
            value = key.getFirstChild();
            break;
          default:
            throw new IllegalStateException();
        }
      } else if (mayHaveSideEffects(c.getFirstChild())) {
        // We don't handle the side-effects here as they might need a temporary
        // or need to be reordered.
        return n;
      }
    }

    // Didn't find a definition of the name in the object literal, it might
    // be coming from the Object prototype
    if (value == null) {
      return n;
    }

    if (value.isFunction() && NodeUtil.referencesThis(value)) {
      // 'this' may refer to the object we are trying to remove
      return n;
    }

    Node replacement = value.detachFromParent();
    if (key.isGetterDef()) {
      replacement = IR.call(replacement);
      replacement.putBooleanProp(Node.FREE_CALL, true);
    }

    n.getParent().replaceChild(n, replacement);
    reportCodeChange();
    return n;
  }
}
/**
 * The syntactic scope creator scans the parse tree to create a Scope object containing all the
 * variable declarations in that scope.
 *
 * <p>This implementation is not thread-safe.
 */
class SyntacticScopeCreator implements ScopeCreator {
  private final AbstractCompiler compiler;
  private Scope scope;
  private String sourceName;
  private final RedeclarationHandler redeclarationHandler;

  // The arguments variable is special, in that it's declared in every local
  // scope, but not explicitly declared.
  private static final String ARGUMENTS = "arguments";

  public static final DiagnosticType VAR_MULTIPLY_DECLARED_ERROR =
      DiagnosticType.error("JSC_VAR_MULTIPLY_DECLARED_ERROR", "Variable {0} first declared in {1}");

  public static final DiagnosticType VAR_ARGUMENTS_SHADOWED_ERROR =
      DiagnosticType.error(
          "JSC_VAR_ARGUMENTS_SHADOWED_ERROR", "Shadowing \"arguments\" is not allowed");

  /** Creates a ScopeCreator. */
  SyntacticScopeCreator(AbstractCompiler compiler) {
    this.compiler = compiler;
    this.redeclarationHandler = new DefaultRedeclarationHandler();
  }

  SyntacticScopeCreator(AbstractCompiler compiler, RedeclarationHandler redeclarationHandler) {
    this.compiler = compiler;
    this.redeclarationHandler = redeclarationHandler;
  }

  public Scope createScope(Node n, Scope parent) {
    sourceName = null;
    if (parent == null) {
      scope = new Scope(n, compiler);
    } else {
      scope = new Scope(parent, n);
    }

    scanRoot(n, parent);

    sourceName = null;
    Scope returnedScope = scope;
    scope = null;
    return returnedScope;
  }

  private void scanRoot(Node n, Scope parent) {
    if (n.getType() == Token.FUNCTION) {
      sourceName = (String) n.getProp(Node.SOURCENAME_PROP);

      final Node fnNameNode = n.getFirstChild();
      final Node args = fnNameNode.getNext();
      final Node body = args.getNext();

      // Bleed the function name into the scope, if it hasn't
      // been declared in the outer scope.
      String fnName = fnNameNode.getString();
      if (!fnName.isEmpty() && NodeUtil.isFunctionExpression(n)) {
        declareVar(fnName, fnNameNode, n, null, null, n);
      }

      // Args: Declare function variables
      Preconditions.checkState(args.getType() == Token.LP);
      for (Node a = args.getFirstChild(); a != null; a = a.getNext()) {
        Preconditions.checkState(a.getType() == Token.NAME);
        declareVar(a.getString(), a, args, n, null, n);
      }

      // Body
      scanVars(body, n);
    } else {
      // It's the global block
      Preconditions.checkState(scope.getParent() == null);
      scanVars(n, null);
    }
  }

  /** Scans and gather variables declarations under a Node */
  private void scanVars(Node n, Node parent) {
    switch (n.getType()) {
      case Token.VAR:
        // Declare all variables. e.g. var x = 1, y, z;
        for (Node child = n.getFirstChild(); child != null; ) {
          Node next = child.getNext();
          Preconditions.checkState(child.getType() == Token.NAME);

          String name = child.getString();
          declareVar(name, child, n, parent, null, n);
          child = next;
        }
        return;

      case Token.FUNCTION:
        if (NodeUtil.isFunctionExpression(n)) {
          return;
        }

        String fnName = n.getFirstChild().getString();
        if (fnName.isEmpty()) {
          // This is invalid, but allow it so the checks can catch it.
          return;
        }
        declareVar(fnName, n.getFirstChild(), n, parent, null, n);
        return; // should not examine function's children

      case Token.CATCH:
        Preconditions.checkState(n.getChildCount() == 2);
        Preconditions.checkState(n.getFirstChild().getType() == Token.NAME);
        // the first child is the catch var and the third child
        // is the code block

        final Node var = n.getFirstChild();
        final Node block = var.getNext();

        declareVar(var.getString(), var, n, parent, null, n);
        scanVars(block, n);
        return; // only one child to scan

      case Token.SCRIPT:
        sourceName = (String) n.getProp(Node.SOURCENAME_PROP);
        break;
    }

    // Variables can only occur in statement-level nodes, so
    // we only need to traverse children in a couple special cases.
    if (NodeUtil.isControlStructure(n) || NodeUtil.isStatementBlock(n)) {
      for (Node child = n.getFirstChild(); child != null; ) {
        Node next = child.getNext();
        scanVars(child, n);
        child = next;
      }
    }
  }

  /** Interface for injectable duplicate handling. */
  interface RedeclarationHandler {
    void onRedeclaration(
        Scope s, String name, Node n, Node parent, Node gramps, Node nodeWithLineNumber);
  }

  /** The default handler for duplicate declarations. */
  private class DefaultRedeclarationHandler implements RedeclarationHandler {
    public void onRedeclaration(
        Scope s, String name, Node n, Node parent, Node gramps, Node nodeWithLineNumber) {
      // Don't allow multiple variables to be declared at the top level scope
      if (scope.isGlobal()) {
        Scope.Var origVar = scope.getVar(name);
        Node origParent = origVar.getParentNode();
        if (origParent.getType() == Token.CATCH && parent.getType() == Token.CATCH) {
          // Okay, both are 'catch(x)' variables.
          return;
        }

        boolean allowDupe = false;
        JSDocInfo info = n.getJSDocInfo();
        if (info == null) {
          info = parent.getJSDocInfo();
        }
        allowDupe = info != null && info.getSuppressions().contains("duplicate");

        if (!allowDupe) {
          compiler.report(
              JSError.make(
                  sourceName,
                  nodeWithLineNumber,
                  VAR_MULTIPLY_DECLARED_ERROR,
                  name,
                  (origVar.input != null ? origVar.input.getName() : "??")));
        }
      } else if (name.equals(ARGUMENTS) && !NodeUtil.isVarDeclaration(n)) {
        // Disallow shadowing "arguments" as we can't handle with our current
        // scope modeling.
        compiler.report(JSError.make(sourceName, nodeWithLineNumber, VAR_ARGUMENTS_SHADOWED_ERROR));
      }
    }
  }

  /**
   * Declares a variable.
   *
   * @param name The variable name
   * @param n The node corresponding to the variable name (usually a NAME node)
   * @param parent The parent node of {@code n}
   * @param gramps The parent node of {@code parent}
   * @param declaredType The variable's type, according to JSDoc
   * @param nodeWithLineNumber The node to use to access the line number of the variable
   *     declaration, if needed
   */
  private void declareVar(
      String name, Node n, Node parent, Node gramps, JSType declaredType, Node nodeWithLineNumber) {
    if (scope.isDeclared(name, false) || (scope.isLocal() && name.equals(ARGUMENTS))) {
      redeclarationHandler.onRedeclaration(scope, name, n, parent, gramps, nodeWithLineNumber);
    } else {
      scope.declare(name, n, declaredType, compiler.getInput(sourceName));
    }
  }
}
/** Tests {@link BasicErrorManager}. */
public class BasicErrorManagerTest extends TestCase {
  private static final String NULL_SOURCE = null;

  private LeveledJSErrorComparator comparator = new LeveledJSErrorComparator();

  static final CheckLevel E = CheckLevel.ERROR;

  private static final DiagnosticType FOO_TYPE = DiagnosticType.error("TEST_FOO", "Foo");

  private static final DiagnosticType JOO_TYPE = DiagnosticType.error("TEST_JOO", "Joo");

  public void testOrderingBothNull() throws Exception {
    assertEquals(0, comparator.compare(null, null));
  }

  public void testOrderingSourceName1() throws Exception {
    JSError e1 = JSError.make(NULL_SOURCE, -1, -1, FOO_TYPE);
    JSError e2 = JSError.make("a", -1, -1, FOO_TYPE);

    assertSmaller(error(e1), error(e2));
  }

  public void testOrderingSourceName2() throws Exception {
    JSError e1 = JSError.make("a", -1, -1, FOO_TYPE);
    JSError e2 = JSError.make("b", -1, -1, FOO_TYPE);

    assertSmaller(error(e1), error(e2));
  }

  public void testOrderingLineno1() throws Exception {
    JSError e1 = JSError.make(NULL_SOURCE, -1, -1, FOO_TYPE);
    JSError e2 = JSError.make(NULL_SOURCE, 2, -1, FOO_TYPE);

    assertSmaller(error(e1), error(e2));
  }

  public void testOrderingLineno2() throws Exception {
    JSError e1 = JSError.make(NULL_SOURCE, 8, -1, FOO_TYPE);
    JSError e2 = JSError.make(NULL_SOURCE, 56, -1, FOO_TYPE);
    assertSmaller(error(e1), error(e2));
  }

  public void testOrderingCheckLevel() throws Exception {
    JSError e1 = JSError.make(NULL_SOURCE, -1, -1, FOO_TYPE);
    JSError e2 = JSError.make(NULL_SOURCE, -1, -1, FOO_TYPE);

    assertSmaller(warning(e1), error(e2));
  }

  public void testOrderingCharno1() throws Exception {
    JSError e1 = JSError.make(NULL_SOURCE, 5, -1, FOO_TYPE);
    JSError e2 = JSError.make(NULL_SOURCE, 5, 2, FOO_TYPE);

    assertSmaller(error(e1), error(e2));
    // CheckLevel preempts charno comparison
    assertSmaller(warning(e1), error(e2));
  }

  public void testOrderingCharno2() throws Exception {
    JSError e1 = JSError.make(NULL_SOURCE, 8, 7, FOO_TYPE);
    JSError e2 = JSError.make(NULL_SOURCE, 8, 5, FOO_TYPE);

    assertSmaller(error(e2), error(e1));
    // CheckLevel preempts charno comparison
    assertSmaller(warning(e2), error(e1));
  }

  public void testOrderingDescription() throws Exception {
    JSError e1 = JSError.make(NULL_SOURCE, -1, -1, FOO_TYPE);
    JSError e2 = JSError.make(NULL_SOURCE, -1, -1, JOO_TYPE);

    assertSmaller(error(e1), error(e2));
  }

  private ErrorWithLevel error(JSError e) {
    return new ErrorWithLevel(e, CheckLevel.ERROR);
  }

  private ErrorWithLevel warning(JSError e) {
    return new ErrorWithLevel(e, CheckLevel.WARNING);
  }

  private void assertSmaller(ErrorWithLevel p1, ErrorWithLevel p2) {
    int p1p2 = comparator.compare(p1, p2);
    assertTrue(Integer.toString(p1p2), p1p2 < 0);
    int p2p1 = comparator.compare(p2, p1);
    assertTrue(Integer.toString(p2p1), p2p1 > 0);
  }
}
/**
 * Replace known jQuery aliases and methods with standard conventions so that the compiler
 * recognizes them. Expected replacements include: - jQuery.fn -> jQuery.prototype - jQuery.extend
 * -> expanded into direct object assignments - jQuery.expandedEach -> expand into direct
 * assignments
 *
 * @author [email protected] (Chad Killingsworth)
 */
class ExpandJqueryAliases extends AbstractPostOrderCallback implements CompilerPass {
  private final AbstractCompiler compiler;
  private final CodingConvention convention;
  private static final Logger logger = Logger.getLogger(ExpandJqueryAliases.class.getName());

  static final DiagnosticType JQUERY_UNABLE_TO_EXPAND_INVALID_LIT_ERROR =
      DiagnosticType.warning(
          "JSC_JQUERY_UNABLE_TO_EXPAND_INVALID_LIT",
          "jQuery.expandedEach call cannot be expanded because the first "
              + "argument must be an object literal or an array of strings "
              + "literal.");

  static final DiagnosticType JQUERY_UNABLE_TO_EXPAND_INVALID_NAME_ERROR =
      DiagnosticType.error(
          "JSC_JQUERY_UNABLE_TO_EXPAND_INVALID_NAME",
          "jQuery.expandedEach expansion would result in the invalid " + "property name \"{0}\".");

  static final DiagnosticType JQUERY_USELESS_EACH_EXPANSION =
      DiagnosticType.warning(
          "JSC_JQUERY_USELESS_EACH_EXPANSION",
          "jQuery.expandedEach was not expanded as no valid property "
              + "assignments were encountered. Consider using jQuery.each instead.");

  private static final Set<String> JQUERY_EXTEND_NAMES =
      ImmutableSet.of("jQuery.extend", "jQuery.fn.extend", "jQuery.prototype.extend");

  private static final String JQUERY_EXPANDED_EACH_NAME = "jQuery.expandedEach";

  private final PeepholeOptimizationsPass peepholePasses;

  ExpandJqueryAliases(AbstractCompiler compiler) {
    this.compiler = compiler;
    this.convention = compiler.getCodingConvention();

    // All of the "early" peephole optimizations.
    // These passes should make the code easier to analyze.
    // Passes, such as StatementFusion, are omitted for this reason.
    final boolean late = false;
    this.peepholePasses =
        new PeepholeOptimizationsPass(
            compiler,
            new PeepholeMinimizeConditions(late),
            new PeepholeSubstituteAlternateSyntax(late),
            new PeepholeReplaceKnownMethods(late),
            new PeepholeRemoveDeadCode(),
            new PeepholeFoldConstants(late),
            new PeepholeCollectPropertyAssignments());
  }

  /**
   * Check that Node n is a call to one of the jQuery.extend methods that we can expand. Valid calls
   * are single argument calls where the first argument is an object literal or two argument calls
   * where the first argument is a name and the second argument is an object literal.
   */
  public static boolean isJqueryExtendCall(Node n, String qname, AbstractCompiler compiler) {
    if (JQUERY_EXTEND_NAMES.contains(qname)) {
      Node firstArgument = n.getNext();
      if (firstArgument == null) {
        return false;
      }

      Node secondArgument = firstArgument.getNext();
      if ((firstArgument.isObjectLit() && secondArgument == null)
          || (firstArgument.isName()
              || NodeUtil.isGet(firstArgument)
                  && !NodeUtil.mayHaveSideEffects(firstArgument, compiler)
                  && secondArgument != null
                  && secondArgument.isObjectLit()
                  && secondArgument.getNext() == null)) {
        return true;
      }
    }
    return false;
  }

  public boolean isJqueryExpandedEachCall(Node call, String qName) {
    Preconditions.checkArgument(call.isCall());
    return call.getFirstChild() != null && JQUERY_EXPANDED_EACH_NAME.equals(qName);
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    if (n.isGetProp() && convention.isPrototypeAlias(n)) {
      maybeReplaceJqueryPrototypeAlias(n);

    } else if (n.isCall()) {
      Node callTarget = n.getFirstChild();
      String qName = callTarget.getQualifiedName();

      if (isJqueryExtendCall(callTarget, qName, this.compiler)) {
        maybeExpandJqueryExtendCall(n);

      } else if (isJqueryExpandedEachCall(n, qName)) {
        maybeExpandJqueryEachCall(t, n);
      }
    }
  }

  @Override
  public void process(Node externs, Node root) {
    logger.fine("Expanding Jquery Aliases");

    NodeTraversal.traverseEs6(compiler, root, this);
  }

  private void maybeReplaceJqueryPrototypeAlias(Node n) {
    // Check to see if this is the assignment of the original alias.
    // If so, leave it intact.
    if (NodeUtil.isLValue(n)) {
      Node maybeAssign = n.getParent();
      while (!NodeUtil.isStatement(maybeAssign) && !maybeAssign.isAssign()) {
        maybeAssign = maybeAssign.getParent();
      }

      if (maybeAssign.isAssign()) {
        maybeAssign = maybeAssign.getParent();
        if (maybeAssign.isBlock() || maybeAssign.isScript() || NodeUtil.isStatement(maybeAssign)) {
          return;
        }
      }
    }

    Node fn = n.getLastChild();
    if (fn != null) {
      n.replaceChild(fn, IR.string("prototype").srcref(fn));
      compiler.reportCodeChange();
    }
  }

  /**
   * Expand jQuery.extend (and derivative) calls into direct object assignments Example:
   * jQuery.extend(obj1, {prop1: val1, prop2: val2}) -> obj1.prop1 = val1; obj1.prop2 = val2;
   */
  private void maybeExpandJqueryExtendCall(Node n) {
    Node callTarget = n.getFirstChild();
    Node objectToExtend = callTarget.getNext(); // first argument
    Node extendArg = objectToExtend.getNext(); // second argument
    boolean ensureObjectDefined = true;

    if (extendArg == null) {
      // Only one argument was specified, so extend jQuery namespace
      extendArg = objectToExtend;
      objectToExtend = callTarget.getFirstChild();
      ensureObjectDefined = false;
    } else if (objectToExtend.isGetProp()
        && (objectToExtend.getLastChild().getString().equals("prototype")
            || convention.isPrototypeAlias(objectToExtend))) {
      ensureObjectDefined = false;
    }

    // Check for an empty object literal
    if (!extendArg.hasChildren()) {
      return;
    }

    // Since we are expanding jQuery.extend calls into multiple statements,
    // encapsulate the new statements in a new block.
    Node fncBlock = IR.block().srcref(n);

    if (ensureObjectDefined) {
      Node assignVal = IR.or(objectToExtend.cloneTree(), IR.objectlit().srcref(n)).srcref(n);
      Node assign = IR.assign(objectToExtend.cloneTree(), assignVal).srcref(n);
      fncBlock.addChildrenToFront(IR.exprResult(assign).srcref(n));
    }

    while (extendArg.hasChildren()) {
      Node currentProp = extendArg.removeFirstChild();
      currentProp.setType(Token.STRING);

      Node propValue = currentProp.removeFirstChild();

      Node newProp;
      if (currentProp.isQuotedString()) {
        newProp = IR.getelem(objectToExtend.cloneTree(), currentProp).srcref(currentProp);
      } else {
        newProp = IR.getprop(objectToExtend.cloneTree(), currentProp).srcref(currentProp);
      }

      Node assignNode = IR.assign(newProp, propValue).srcref(currentProp);
      fncBlock.addChildToBack(IR.exprResult(assignNode).srcref(currentProp));
    }

    // Check to see if the return value is used. If not, replace the original
    // call with new block. Otherwise, wrap the statements in an
    // immediately-called anonymous function.
    if (n.getParent().isExprResult()) {
      Node parent = n.getParent();
      parent.getParent().replaceChild(parent, fncBlock);
    } else {
      Node targetVal;
      if ("jQuery.prototype".equals(objectToExtend.getQualifiedName())) {
        // When extending the jQuery prototype, return the jQuery namespace.
        // This is not commonly used.
        targetVal = objectToExtend.removeFirstChild();
      } else {
        targetVal = objectToExtend.detachFromParent();
      }
      fncBlock.addChildToBack(IR.returnNode(targetVal).srcref(targetVal));

      Node fnc = IR.function(IR.name("").srcref(n), IR.paramList().srcref(n), fncBlock).srcref(n);

      // add an explicit "call" statement so that we can maintain
      // the same reference for "this"
      Node newCallTarget = IR.getprop(fnc, IR.string("call").srcref(n)).srcref(n);
      n.replaceChild(callTarget, newCallTarget);
      n.putBooleanProp(Node.FREE_CALL, false);

      // remove any other pre-existing call arguments
      while (newCallTarget.getNext() != null) {
        n.removeChildAfter(newCallTarget);
      }
      n.addChildToBack(IR.thisNode().srcref(n));
    }
    compiler.reportCodeChange();
  }

  /**
   * Expand a jQuery.expandedEach call
   *
   * <p>Expanded jQuery.expandedEach calls will replace the GETELEM nodes of a property assignment
   * with GETPROP nodes to allow for renaming.
   */
  private void maybeExpandJqueryEachCall(NodeTraversal t, Node n) {
    Node objectToLoopOver = n.getChildAtIndex(1);

    if (objectToLoopOver == null) {
      return;
    }

    Node callbackFunction = objectToLoopOver.getNext();
    if (callbackFunction == null || !callbackFunction.isFunction()) {
      return;
    }

    // Run the peephole optimizations on the first argument to handle
    // cases like ("a " + "b").split(" ")
    peepholePasses.process(null, n.getChildAtIndex(1));

    // Create a reference tree
    Node nClone = n.cloneTree();

    objectToLoopOver = nClone.getChildAtIndex(1);

    // Check to see if the first argument is something we recognize and can
    // expand.
    if (!objectToLoopOver.isObjectLit()
        && !(objectToLoopOver.isArrayLit() && isArrayLitValidForExpansion(objectToLoopOver))) {
      t.report(n, JQUERY_UNABLE_TO_EXPAND_INVALID_LIT_ERROR, (String) null);
      return;
    }

    // Find all references to the callback function arguments
    List<Node> keyNodeReferences = new ArrayList<>();
    List<Node> valueNodeReferences = new ArrayList<>();

    new NodeTraversal(
            compiler,
            new FindCallbackArgumentReferences(
                callbackFunction,
                keyNodeReferences,
                valueNodeReferences,
                objectToLoopOver.isArrayLit()))
        .traverseInnerNode(
            NodeUtil.getFunctionBody(callbackFunction), callbackFunction, t.getScope());

    if (keyNodeReferences.isEmpty()) {
      // We didn't do anything useful ...
      t.report(n, JQUERY_USELESS_EACH_EXPANSION, (String) null);
      return;
    }

    Node fncBlock =
        tryExpandJqueryEachCall(
            t, nClone, callbackFunction, keyNodeReferences, valueNodeReferences);

    if (fncBlock != null && fncBlock.hasChildren()) {
      replaceOriginalJqueryEachCall(n, fncBlock);
    } else {
      // We didn't do anything useful ...
      t.report(n, JQUERY_USELESS_EACH_EXPANSION, (String) null);
    }
  }

  private Node tryExpandJqueryEachCall(
      NodeTraversal t, Node n, Node callbackFunction, List<Node> keyNodes, List<Node> valueNodes) {

    Node callTarget = n.getFirstChild();
    Node objectToLoopOver = callTarget.getNext();

    // New block to contain the expanded statements
    Node fncBlock = IR.block().srcref(callTarget);

    boolean isValidExpansion = true;

    // Expand the jQuery.expandedEach call
    Node key = objectToLoopOver.getFirstChild(), val = null;
    for (int i = 0; key != null; key = key.getNext(), i++) {
      if (key != null) {
        if (objectToLoopOver.isArrayLit()) {
          // Arrays have a value of their index number
          val = IR.number(i).srcref(key);
        } else {
          val = key.getFirstChild();
        }
      }

      // Keep track of the replaced nodes so we can reset the tree
      List<Node> newKeys = new ArrayList<>();
      List<Node> newValues = new ArrayList<>();
      List<Node> origGetElems = new ArrayList<>();
      List<Node> newGetProps = new ArrayList<>();

      // Replace all of the key nodes with the prop name
      for (int j = 0; j < keyNodes.size(); j++) {
        Node origNode = keyNodes.get(j);
        Node ancestor = origNode.getParent();

        Node newNode = IR.string(key.getString()).srcref(key);
        newKeys.add(newNode);
        ancestor.replaceChild(origNode, newNode);

        // Walk up the tree to see if the key is used in a GETELEM
        // assignment
        while (ancestor != null && !NodeUtil.isStatement(ancestor) && !ancestor.isGetElem()) {
          ancestor = ancestor.getParent();
        }

        // Convert GETELEM nodes to GETPROP nodes so that they can be
        // renamed or removed.
        if (ancestor != null && ancestor.isGetElem()) {

          Node propObject = ancestor;
          while (propObject.isGetProp() || propObject.isGetElem()) {
            propObject = propObject.getFirstChild();
          }

          Node ancestorClone = ancestor.cloneTree();
          // Run the peephole passes to handle cases such as
          // obj['lit' + key] = val;
          peepholePasses.process(null, ancestorClone.getChildAtIndex(1));
          Node prop = ancestorClone.getChildAtIndex(1);

          if (prop.isString()
              && NodeUtil.isValidPropertyName(LanguageMode.ECMASCRIPT3, prop.getString())) {
            Node target = ancestorClone.getFirstChild();
            Node newGetProp = IR.getprop(target.detachFromParent(), prop.detachFromParent());
            newGetProps.add(newGetProp);
            origGetElems.add(ancestor);
            ancestor.getParent().replaceChild(ancestor, newGetProp);
          } else {
            if (prop.isString()
                && !NodeUtil.isValidPropertyName(LanguageMode.ECMASCRIPT3, prop.getString())) {
              t.report(n, JQUERY_UNABLE_TO_EXPAND_INVALID_NAME_ERROR, prop.getString());
            }
            isValidExpansion = false;
          }
        }
      }

      if (isValidExpansion) {
        // Replace all of the value nodes with the prop value
        for (int j = 0; val != null && j < valueNodes.size(); j++) {
          Node origNode = valueNodes.get(j);
          Node newNode = val.cloneTree();
          newValues.add(newNode);
          origNode.getParent().replaceChild(origNode, newNode);
        }

        // Wrap the new tree in an anonymous function call
        Node fnc =
            IR.function(
                    IR.name("").srcref(key),
                    IR.paramList().srcref(key),
                    callbackFunction.getChildAtIndex(2).cloneTree())
                .srcref(key);
        Node call = IR.call(fnc).srcref(key);
        call.putBooleanProp(Node.FREE_CALL, true);
        fncBlock.addChildToBack(IR.exprResult(call).srcref(call));
      }

      // Reset the source tree
      for (int j = 0; j < newGetProps.size(); j++) {
        newGetProps.get(j).getParent().replaceChild(newGetProps.get(j), origGetElems.get(j));
      }
      for (int j = 0; j < newKeys.size(); j++) {
        newKeys.get(j).getParent().replaceChild(newKeys.get(j), keyNodes.get(j));
      }
      for (int j = 0; j < newValues.size(); j++) {
        newValues.get(j).getParent().replaceChild(newValues.get(j), valueNodes.get(j));
      }

      if (!isValidExpansion) {
        return null;
      }
    }
    return fncBlock;
  }

  private void replaceOriginalJqueryEachCall(Node n, Node expandedBlock) {
    // Check to see if the return value of the original jQuery.expandedEach
    // call is used. If so, we need to wrap each loop expansion in an anonymous
    // function and return the original objectToLoopOver.
    if (n.getParent().isExprResult()) {
      Node parent = n.getParent();
      Node grandparent = parent.getParent();
      Node insertAfter = parent;
      while (expandedBlock.hasChildren()) {
        Node child = expandedBlock.getFirstChild().detachFromParent();
        grandparent.addChildAfter(child, insertAfter);
        insertAfter = child;
      }
      grandparent.removeChild(parent);
    } else {
      // Return the original object
      Node callTarget = n.getFirstChild();
      Node objectToLoopOver = callTarget.getNext();

      objectToLoopOver.detachFromParent();
      Node ret = IR.returnNode(objectToLoopOver).srcref(callTarget);
      expandedBlock.addChildToBack(ret);

      // Wrap all of the expanded loop calls in a new anonymous function
      Node fnc =
          IR.function(
              IR.name("").srcref(callTarget), IR.paramList().srcref(callTarget), expandedBlock);
      n.replaceChild(callTarget, fnc);
      n.putBooleanProp(Node.FREE_CALL, true);

      // remove any other pre-existing call arguments
      while (fnc.getNext() != null) {
        n.removeChildAfter(fnc);
      }
    }
    compiler.reportCodeChange();
  }

  private boolean isArrayLitValidForExpansion(Node n) {
    Iterator<Node> iter = n.children().iterator();
    while (iter.hasNext()) {
      Node child = iter.next();
      if (!child.isString()) {
        return false;
      }
    }
    return true;
  }

  /**
   * Given a jQuery.expandedEach callback function, traverse it and collect any references to its
   * parameter names.
   */
  static class FindCallbackArgumentReferences extends AbstractPostOrderCallback
      implements ScopedCallback {

    private final String keyName;
    private final String valueName;
    private Scope startingScope;
    private List<Node> keyReferences;
    private List<Node> valueReferences;

    FindCallbackArgumentReferences(
        Node functionRoot,
        List<Node> keyReferences,
        List<Node> valueReferences,
        boolean useArrayMode) {
      Preconditions.checkState(functionRoot.isFunction());

      String keyString = null, valueString = null;
      Node callbackParams = NodeUtil.getFunctionParameters(functionRoot);
      Node param = callbackParams.getFirstChild();
      if (param != null) {
        Preconditions.checkState(param.isName());
        keyString = param.getString();

        param = param.getNext();
        if (param != null) {
          Preconditions.checkState(param.isName());
          valueString = param.getString();
        }
      }

      this.keyName = keyString;
      this.valueName = valueString;

      // For arrays, the keyString is the index number of the element.
      // We're interested in the value of the element instead
      if (useArrayMode) {
        this.keyReferences = valueReferences;
        this.valueReferences = keyReferences;
      } else {
        this.keyReferences = keyReferences;
        this.valueReferences = valueReferences;
      }

      this.startingScope = null;
    }

    private boolean isShadowed(String name, Scope scope) {
      Var nameVar = scope.getVar(name);
      return nameVar != null && nameVar.getScope() != this.startingScope;
    }

    @Override
    public void visit(NodeTraversal t, Node n, Node parent) {
      // In the top scope, "this" is a reference to "value"
      boolean isThis = false;
      if (t.getScope().getClosestHoistScope() == this.startingScope) {
        isThis = n.isThis();
      }

      if (isThis || n.isName() && !isShadowed(n.getString(), t.getScope())) {
        String nodeValue = isThis ? null : n.getString();
        if (!isThis && keyName != null && nodeValue.equals(keyName)) {
          keyReferences.add(n);
        } else if (isThis || (valueName != null && nodeValue.equals(valueName))) {
          valueReferences.add(n);
        }
      }
    }

    /**
     * As we enter each scope, make sure that the scope doesn't define a local variable with the
     * same name as our original callback method parameter names.
     */
    @Override
    public void enterScope(NodeTraversal t) {
      if (this.startingScope == null) {
        this.startingScope = t.getScope();
      }
    }

    @Override
    public void exitScope(NodeTraversal t) {}
  }
}
/**
 * Rewrites "goog.defineClass" into a form that is suitable for type checking and dead code
 * elimination.
 *
 * @author [email protected] (John Lenz)
 */
class ClosureRewriteClass extends AbstractPostOrderCallback implements HotSwapCompilerPass {

  // Errors
  static final DiagnosticType GOOG_CLASS_TARGET_INVALID =
      DiagnosticType.error(
          "JSC_GOOG_CLASS_TARGET_INVALID", "Unsupported class definition expression.");

  static final DiagnosticType GOOG_CLASS_SUPER_CLASS_NOT_VALID =
      DiagnosticType.error(
          "JSC_GOOG_CLASS_SUPER_CLASS_NOT_VALID",
          "The super class must be null or a valid name reference");

  static final DiagnosticType GOOG_CLASS_DESCRIPTOR_NOT_VALID =
      DiagnosticType.error(
          "JSC_GOOG_CLASS_DESCRIPTOR_NOT_VALID", "The class descriptor must be an object literal");

  static final DiagnosticType GOOG_CLASS_CONSTRUCTOR_MISSING =
      DiagnosticType.error(
          "JSC_GOOG_CLASS_CONSTRUCTOR_MISSING",
          "The constructor expression is missing for the class descriptor");

  static final DiagnosticType GOOG_CLASS_CONSTRUCTOR_ON_INTERFACE =
      DiagnosticType.error(
          "JSC_GOOG_CLASS_CONSTRUCTOR_ON_INTERFACE",
          "Should not have a constructor expression for an interface");

  static final DiagnosticType GOOG_CLASS_STATICS_NOT_VALID =
      DiagnosticType.error(
          "JSC_GOOG_CLASS_STATICS_NOT_VALID",
          "The class statics descriptor must be an object or function literal");

  static final DiagnosticType GOOG_CLASS_UNEXPECTED_PARAMS =
      DiagnosticType.error(
          "JSC_GOOG_CLASS_UNEXPECTED_PARAMS", "The class definition has too many arguments.");

  // Warnings
  static final DiagnosticType GOOG_CLASS_NG_INJECT_ON_CLASS =
      DiagnosticType.warning(
          "JSC_GOOG_CLASS_NG_INJECT_ON_CLASS",
          "@ngInject should be declared on the constructor, not on the class.");

  private final AbstractCompiler compiler;

  public ClosureRewriteClass(AbstractCompiler compiler) {
    this.compiler = compiler;
  }

  @Override
  public void process(Node externs, Node root) {
    hotSwapScript(root, null);
  }

  @Override
  public void hotSwapScript(Node scriptRoot, Node originalRoot) {
    NodeTraversal.traverse(compiler, scriptRoot, this);
  }

  @Override
  public void visit(NodeTraversal t, Node n, Node parent) {
    if (n.isCall() && isGoogDefineClass(n)) {
      if (!validateUsage(n)) {
        compiler.report(JSError.make(n, GOOG_CLASS_TARGET_INVALID));
      }
    }
    maybeRewriteClassDefinition(n);
  }

  private boolean validateUsage(Node n) {
    // There are only three valid usage patterns for of goog.defineClass
    //   var ClassName = googDefineClass
    //   namespace.ClassName = googDefineClass
    //   and within an objectlit, used by the goog.defineClass.
    Node parent = n.getParent();
    switch (parent.getType()) {
      case Token.NAME:
        return true;
      case Token.ASSIGN:
        return n == parent.getLastChild() && parent.getParent().isExprResult();
      case Token.STRING_KEY:
        return isContainedInGoogDefineClass(parent);
    }
    return false;
  }

  private boolean isContainedInGoogDefineClass(Node n) {
    while (n != null) {
      n = n.getParent();
      if (n.isCall()) {
        if (isGoogDefineClass(n)) {
          return true;
        }
      } else if (!n.isObjectLit() && !n.isStringKey()) {
        break;
      }
    }
    return false;
  }

  private void maybeRewriteClassDefinition(Node n) {
    if (n.isVar()) {
      Node target = n.getFirstChild();
      Node value = target.getFirstChild();
      maybeRewriteClassDefinition(n, target, value);
    } else if (NodeUtil.isExprAssign(n)) {
      Node assign = n.getFirstChild();
      Node target = assign.getFirstChild();
      Node value = assign.getLastChild();
      maybeRewriteClassDefinition(n, target, value);
    }
  }

  private void maybeRewriteClassDefinition(Node n, Node target, Node value) {
    if (isGoogDefineClass(value)) {
      if (!target.isQualifiedName()) {
        compiler.report(JSError.make(n, GOOG_CLASS_TARGET_INVALID));
      }
      ClassDefinition def = extractClassDefinition(target, value);
      if (def != null) {
        value.detachFromParent();
        target.detachFromParent();
        rewriteGoogDefineClass(n, def);
      }
    }
  }

  private static class MemberDefinition {
    final JSDocInfo info;
    final Node name;
    final Node value;

    MemberDefinition(JSDocInfo info, Node name, Node value) {
      this.info = info;
      this.name = name;
      this.value = value;
    }
  }

  private static final class ClassDefinition {
    final Node name;
    final JSDocInfo classInfo;
    final Node superClass;
    final MemberDefinition constructor;
    final List<MemberDefinition> staticProps;
    final List<MemberDefinition> props;
    final Node classModifier;

    ClassDefinition(
        Node name,
        JSDocInfo classInfo,
        Node superClass,
        MemberDefinition constructor,
        List<MemberDefinition> staticProps,
        List<MemberDefinition> props,
        Node classModifier) {
      this.name = name;
      this.classInfo = classInfo;
      this.superClass = superClass;
      this.constructor = constructor;
      this.staticProps = staticProps;
      this.props = props;
      this.classModifier = classModifier;
    }
  }

  /**
   * Validates the class definition and if valid, destructively extracts the class definition from
   * the AST.
   */
  private ClassDefinition extractClassDefinition(Node targetName, Node callNode) {

    JSDocInfo classInfo = NodeUtil.getBestJSDocInfo(targetName);

    // name = goog.defineClass(superClass, {...}, [modifier, ...])
    Node superClass = NodeUtil.getArgumentForCallOrNew(callNode, 0);
    if (superClass == null || (!superClass.isNull() && !superClass.isQualifiedName())) {
      compiler.report(JSError.make(callNode, GOOG_CLASS_SUPER_CLASS_NOT_VALID));
      return null;
    }

    if (NodeUtil.isNullOrUndefined(superClass) || superClass.matchesQualifiedName("Object")) {
      superClass = null;
    }

    Node description = NodeUtil.getArgumentForCallOrNew(callNode, 1);
    if (description == null || !description.isObjectLit() || !validateObjLit(description)) {
      // report bad class definition
      compiler.report(JSError.make(callNode, GOOG_CLASS_DESCRIPTOR_NOT_VALID));
      return null;
    }

    int paramCount = callNode.getChildCount() - 1;
    if (paramCount > 2) {
      compiler.report(JSError.make(callNode, GOOG_CLASS_UNEXPECTED_PARAMS));
      return null;
    }

    Node constructor = extractProperty(description, "constructor");
    if (classInfo != null && classInfo.isInterface()) {
      if (constructor != null) {
        compiler.report(JSError.make(description, GOOG_CLASS_CONSTRUCTOR_ON_INTERFACE));
        return null;
      }
    } else if (constructor == null) {
      // report missing constructor
      compiler.report(JSError.make(description, GOOG_CLASS_CONSTRUCTOR_MISSING));
      return null;
    }
    if (constructor == null) {
      constructor =
          IR.function(
              IR.name("").srcref(callNode),
              IR.paramList().srcref(callNode),
              IR.block().srcref(callNode));
      constructor.srcref(callNode);
    }

    JSDocInfo info = NodeUtil.getBestJSDocInfo(constructor);

    Node classModifier = null;
    Node statics = null;
    Node staticsProp = extractProperty(description, "statics");
    if (staticsProp != null) {
      if (staticsProp.isObjectLit() && validateObjLit(staticsProp)) {
        statics = staticsProp;
      } else if (staticsProp.isFunction()) {
        classModifier = staticsProp;
      } else {
        compiler.report(JSError.make(staticsProp, GOOG_CLASS_STATICS_NOT_VALID));
        return null;
      }
    }

    if (statics == null) {
      statics = IR.objectlit();
    }

    // Ok, now rip apart the definition into its component pieces.
    // Remove the "special" property key nodes.
    maybeDetach(constructor.getParent());
    maybeDetach(statics.getParent());
    if (classModifier != null) {
      maybeDetach(classModifier.getParent());
    }
    ClassDefinition def =
        new ClassDefinition(
            targetName,
            classInfo,
            maybeDetach(superClass),
            new MemberDefinition(info, null, maybeDetach(constructor)),
            objectLitToList(maybeDetach(statics)),
            objectLitToList(description),
            maybeDetach(classModifier));
    return def;
  }

  private static Node maybeDetach(Node node) {
    if (node != null && node.getParent() != null) {
      node.detachFromParent();
    }
    return node;
  }

  // Only unquoted plain properties are currently supported.
  private static boolean validateObjLit(Node objlit) {
    for (Node key : objlit.children()) {
      if (!key.isStringKey() || key.isQuotedString()) {
        return false;
      }
    }
    return true;
  }

  /** @return The first property in the objlit that matches the key. */
  private static Node extractProperty(Node objlit, String keyName) {
    for (Node keyNode : objlit.children()) {
      if (keyNode.getString().equals(keyName)) {
        return keyNode.isStringKey() ? keyNode.getFirstChild() : null;
      }
    }
    return null;
  }

  private static List<MemberDefinition> objectLitToList(Node objlit) {
    List<MemberDefinition> result = Lists.newArrayList();
    for (Node keyNode : objlit.children()) {
      result.add(
          new MemberDefinition(
              NodeUtil.getBestJSDocInfo(keyNode), keyNode, keyNode.removeFirstChild()));
    }
    objlit.detachChildren();
    return result;
  }

  private void rewriteGoogDefineClass(Node exprRoot, final ClassDefinition cls) {
    // For simplicity add everything into a block, before adding it to the AST.
    Node block = IR.block();

    // remove the original jsdoc info if it was attached to the value.
    cls.constructor.value.setJSDocInfo(null);
    if (exprRoot.isVar()) {
      // example: var ctr = function(){}
      Node var = IR.var(cls.name.cloneTree(), cls.constructor.value).srcref(exprRoot);
      JSDocInfo mergedClassInfo = mergeJsDocFor(cls, var);
      var.setJSDocInfo(mergedClassInfo);
      block.addChildToBack(var);
    } else {
      // example: ns.ctr = function(){}
      Node assign =
          IR.assign(cls.name.cloneTree(), cls.constructor.value)
              .srcref(exprRoot)
              .setJSDocInfo(cls.constructor.info);

      JSDocInfo mergedClassInfo = mergeJsDocFor(cls, assign);
      assign.setJSDocInfo(mergedClassInfo);

      Node expr = IR.exprResult(assign).srcref(exprRoot);
      block.addChildToBack(expr);
    }

    if (cls.superClass != null) {
      // example: goog.inherits(ctr, superClass)
      block.addChildToBack(
          fixupSrcref(
              IR.exprResult(
                  IR.call(
                          NodeUtil.newQName(compiler, "goog.inherits").srcrefTree(cls.superClass),
                          cls.name.cloneTree(),
                          cls.superClass.cloneTree())
                      .srcref(cls.superClass))));
    }

    for (MemberDefinition def : cls.staticProps) {
      // remove the original jsdoc info if it was attached to the value.
      def.value.setJSDocInfo(null);

      // example: ctr.prop = value
      block.addChildToBack(
          fixupSrcref(
              IR.exprResult(
                  fixupSrcref(
                          IR.assign(
                              IR.getprop(
                                      cls.name.cloneTree(),
                                      IR.string(def.name.getString()).srcref(def.name))
                                  .srcref(def.name),
                              def.value))
                      .setJSDocInfo(def.info))));
      // Handle inner class definitions.
      maybeRewriteClassDefinition(block.getLastChild());
    }

    for (MemberDefinition def : cls.props) {
      // remove the original jsdoc info if it was attached to the value.
      def.value.setJSDocInfo(null);

      // example: ctr.prototype.prop = value
      block.addChildToBack(
          fixupSrcref(
              IR.exprResult(
                  fixupSrcref(
                          IR.assign(
                              IR.getprop(
                                      fixupSrcref(
                                          IR.getprop(
                                              cls.name.cloneTree(),
                                              IR.string("prototype").srcref(def.name))),
                                      IR.string(def.name.getString()).srcref(def.name))
                                  .srcref(def.name),
                              def.value))
                      .setJSDocInfo(def.info))));
      // Handle inner class definitions.
      maybeRewriteClassDefinition(block.getLastChild());
    }

    if (cls.classModifier != null) {
      // Inside the modifier function, replace references to the argument
      // with the class name.
      //   function(cls) { cls.Foo = bar; }
      // becomes
      //   function(cls) { theClassName.Foo = bar; }
      // The cls parameter is unused, but leave it there so that it
      // matches the JsDoc.
      // TODO(tbreisacher): Add a warning if the param is shadowed or reassigned.
      Node argList = cls.classModifier.getFirstChild().getNext();
      Node arg = argList.getFirstChild();
      final String argName = arg.getString();
      NodeTraversal.traverse(
          compiler,
          cls.classModifier.getLastChild(),
          new AbstractPostOrderCallback() {
            @Override
            public void visit(NodeTraversal t, Node n, Node parent) {
              if (n.isName() && n.getString().equals(argName)) {
                parent.replaceChild(n, cls.name.cloneTree());
              }
            }
          });

      block.addChildToBack(
          IR.exprResult(
                  fixupFreeCall(
                      IR.call(cls.classModifier, cls.name.cloneTree()).srcref(cls.classModifier)))
              .srcref(cls.classModifier));
    }

    Node parent = exprRoot.getParent();
    Node stmts = block.removeChildren();
    parent.addChildrenAfter(stmts, exprRoot);
    parent.removeChild(exprRoot);

    compiler.reportCodeChange();
  }

  private static Node fixupSrcref(Node node) {
    node.srcref(node.getFirstChild());
    return node;
  }

  private static Node fixupFreeCall(Node call) {
    Preconditions.checkState(call.isCall());
    call.putBooleanProp(Node.FREE_CALL, true);
    return call;
  }

  /** @return Whether the call represents a class definition. */
  private static boolean isGoogDefineClass(Node value) {
    if (value != null && value.isCall()) {
      return value.getFirstChild().matchesQualifiedName("goog.defineClass");
    }
    return false;
  }

  static final String VIRTUAL_FILE = "<ClosureRewriteClass.java>";

  private JSDocInfo mergeJsDocFor(ClassDefinition cls, Node associatedNode) {
    // avoid null checks
    JSDocInfo classInfo = (cls.classInfo != null) ? cls.classInfo : new JSDocInfo(true);

    JSDocInfo ctorInfo =
        (cls.constructor.info != null) ? cls.constructor.info : new JSDocInfo(true);

    Node superNode = cls.superClass;

    // Start with a clone of the constructor info if there is one.
    JSDocInfoBuilder mergedInfo =
        cls.constructor.info != null
            ? JSDocInfoBuilder.copyFrom(ctorInfo)
            : new JSDocInfoBuilder(true);

    // merge block description
    String blockDescription =
        Joiner.on("\n")
            .skipNulls()
            .join(classInfo.getBlockDescription(), ctorInfo.getBlockDescription());
    if (!blockDescription.isEmpty()) {
      mergedInfo.recordBlockDescription(blockDescription);
    }

    // merge suppressions
    Set<String> suppressions = Sets.newHashSet();
    suppressions.addAll(classInfo.getSuppressions());
    suppressions.addAll(ctorInfo.getSuppressions());
    if (!suppressions.isEmpty()) {
      mergedInfo.recordSuppressions(suppressions);
    }

    // Use class deprecation if set.
    if (classInfo.isDeprecated()) {
      mergedInfo.recordDeprecated();
    }

    String deprecationReason = null;
    if (classInfo.getDeprecationReason() != null) {
      deprecationReason = classInfo.getDeprecationReason();
      mergedInfo.recordDeprecationReason(deprecationReason);
    }

    // Use class visibility if specifically set
    Visibility visibility = classInfo.getVisibility();
    if (visibility != null && visibility != JSDocInfo.Visibility.INHERITED) {
      mergedInfo.recordVisibility(classInfo.getVisibility());
    }

    if (classInfo.isConstant()) {
      mergedInfo.recordConstancy();
    }

    if (classInfo.isExport()) {
      mergedInfo.recordExport();
    }

    // If @ngInject is on the ctor, it's already been copied above.
    if (classInfo.isNgInject()) {
      compiler.report(JSError.make(associatedNode, GOOG_CLASS_NG_INJECT_ON_CLASS));
      mergedInfo.recordNgInject(true);
    }

    // @constructor is implied, @interface must be explicit
    boolean isInterface = classInfo.isInterface() || ctorInfo.isInterface();
    if (isInterface) {
      mergedInfo.recordInterface();
      List<JSTypeExpression> extendedInterfaces = null;
      if (classInfo.getExtendedInterfacesCount() > 0) {
        extendedInterfaces = classInfo.getExtendedInterfaces();
      } else if (ctorInfo.getExtendedInterfacesCount() == 0 && superNode != null) {
        extendedInterfaces =
            ImmutableList.of(
                new JSTypeExpression(
                    new Node(Token.BANG, IR.string(superNode.getQualifiedName())), VIRTUAL_FILE));
      }
      if (extendedInterfaces != null) {
        for (JSTypeExpression extend : extendedInterfaces) {
          mergedInfo.recordExtendedInterface(extend);
        }
      }
    } else {
      // @constructor by default
      mergedInfo.recordConstructor();
      if (classInfo.makesUnrestricted() || ctorInfo.makesUnrestricted()) {
        mergedInfo.recordUnrestricted();
      } else if (classInfo.makesDicts() || ctorInfo.makesDicts()) {
        mergedInfo.recordDict();
      } else {
        // @struct by default
        mergedInfo.recordStruct();
      }

      if (classInfo.getBaseType() != null) {
        mergedInfo.recordBaseType(classInfo.getBaseType());
      } else if (superNode != null) {
        // a "super" implies @extends, build a default.
        JSTypeExpression baseType =
            new JSTypeExpression(
                new Node(Token.BANG, IR.string(superNode.getQualifiedName())), VIRTUAL_FILE);
        mergedInfo.recordBaseType(baseType);
      }

      // @implements from the class if they exist
      List<JSTypeExpression> interfaces = classInfo.getImplementedInterfaces();
      for (JSTypeExpression implemented : interfaces) {
        mergedInfo.recordImplementedInterface(implemented);
      }
    }

    // merge @template types if they exist
    List<String> templateNames = new ArrayList<>();
    templateNames.addAll(classInfo.getTemplateTypeNames());
    templateNames.addAll(ctorInfo.getTemplateTypeNames());
    for (String typeName : templateNames) {
      mergedInfo.recordTemplateTypeName(typeName);
    }
    return mergedInfo.build(associatedNode);
  }
}
/** Rewrites all the library polyfills. */
public class RewritePolyfills implements HotSwapCompilerPass {

  static final DiagnosticType INSUFFICIENT_OUTPUT_VERSION_ERROR =
      DiagnosticType.warning(
          "JSC_INSUFFICIENT_OUTPUT_VERSION",
          "Built-in ''{0}'' not supported in output version {1}: set --language_out to at least {2}");

  // Also polyfill references to e.g. goog.global.Map or window.Map.
  private static final String GLOBAL = "goog.global.";
  private static final String WINDOW = "window.";

  /**
   * Represents a single polyfill: specifically, a native symbol (either a qualified name or a
   * property name) that can be rewritten and/or installed to provide the functionality to a lower
   * version. This is a simple value type.
   */
  private static class Polyfill {
    /**
     * The language version at (or above) which the native symbol is available and sufficient. If
     * the language out flag is at least as high as {@code nativeVersion} then no rewriting will
     * happen.
     */
    final FeatureSet nativeVersion;

    /**
     * The required language version for the polyfill to work. This should not be higher than {@code
     * nativeVersion}, but may be the same in cases where there is no polyfill provided. This is
     * used to emit a warning if the language out flag is too low.
     */
    final FeatureSet polyfillVersion;

    /**
     * Optional qualified name to drop-in replace for the native symbol. May be empty if no direct
     * rewriting is to take place.
     */
    final String rewrite;

    /**
     * Optional "installer" to insert (once) at the top of a source file. If present, this should be
     * a JavaScript statement, or empty if no installer should be inserted.
     */
    final String installer;

    Polyfill(
        FeatureSet nativeVersion, FeatureSet polyfillVersion, String rewrite, String installer) {
      this.nativeVersion = nativeVersion;
      this.polyfillVersion = polyfillVersion;
      this.rewrite = rewrite;
      this.installer = installer;
    }
  }

  /**
   * Describes all the available polyfills, including native and required versions, and how to use
   * them.
   */
  static class Polyfills {
    // Map of method polyfills, keyed by native method name.
    private final ImmutableMultimap<String, Polyfill> methods;
    // Map of static polyfills, keyed by fully-qualified native name.
    private final ImmutableMap<String, Polyfill> statics;

    private Polyfills(Builder builder) {
      this.methods = builder.methodsBuilder.build();
      this.statics = builder.staticsBuilder.build();
    }

    /**
     * Provides a DSL for building a {@link Polyfills} object by calling {@link #addStatics}, {@link
     * #addMethods}, and {@link #addClasses} to register the various polyfills and provide
     * information about the native and polyfilled versions, and how to use the polyfills.
     */
    static class Builder {
      private final ImmutableMultimap.Builder<String, Polyfill> methodsBuilder =
          ImmutableMultimap.builder();
      private final ImmutableMap.Builder<String, Polyfill> staticsBuilder = ImmutableMap.builder();

      /**
       * Registers one or more prototype method in a single namespace. The pass is agnostic with
       * regard to the class whose prototype is being augmented. The {@code base} parameter
       * specifies the qualified namespace where all the {@code methods} reside. Each method is
       * expected to have a sibling named with the {@code $install} suffix. The method calls
       * themselves are not rewritten, but whenever one is detected, its installer(s) will be added
       * to the top of the source file whenever the output version is less than {@code
       * nativeVersion}. For example, defining {@code addMethods(ES6, ES5, "$jscomp.string",
       * "startsWith", "endsWith")} will cause {@code $jscomp.string.startsWith$install();} to be
       * added to any source file that calls, e.g. {@code foo.startsWith}.
       *
       * <p>If {@code base} is blank, then no polyfills will be installed. This is useful for
       * documenting unimplemented polyfills.
       */
      Builder addMethods(
          FeatureSet nativeVersion, FeatureSet polyfillVersion, String base, String... methods) {
        if (!base.isEmpty()) {
          for (String method : methods) {
            methodsBuilder.put(
                method,
                new Polyfill(
                    nativeVersion, polyfillVersion, "", base + "." + method + "$install();"));
          }
        }
        // TODO(sdh): If base.isEmpty() then it means no polyfill is implemented.  Is there
        // any way we can warn if the output language is too low?  It's not likely, since
        // there's no good way to determine if it's actually intended as an ES6 method or
        // else is defined elsewhere.
        return this;
      }

      /**
       * Registers one or more static rewrite polyfill, which is a simple rewrite of one qualified
       * name to another. For each {@code name} in {@code statics}, {@code nativeBase + '.' + name}
       * will be replaced with {@code polyfillBase + '.' + name} whenever the output version is less
       * than {@code nativeVersion}. For eaxmple, defining {@code addStatics(ES6, ES5,
       * "$jscomp.math", "Math", "clz32", "imul")} will cause {@code Math.clz32} to be rewritten as
       * {@code $jscomp.math.clz32}.
       *
       * <p>If {@code polyfillBase} is blank, then no polyfills will be installed. This is useful
       * for documenting unimplemented polyfills, and will trigger a warning if the language output
       * mode is less than the native version.
       */
      Builder addStatics(
          FeatureSet nativeVersion,
          FeatureSet polyfillVersion,
          String polyfillBase,
          String nativeBase,
          String... statics) {
        for (String item : statics) {
          String nativeName = nativeBase + "." + item;
          String polyfillName = !polyfillBase.isEmpty() ? polyfillBase + "." + item : "";
          Polyfill polyfill = new Polyfill(nativeVersion, polyfillVersion, polyfillName, "");
          staticsBuilder.put(nativeName, polyfill);
          staticsBuilder.put(GLOBAL + nativeName, polyfill);
          staticsBuilder.put(WINDOW + nativeName, polyfill);
        }
        return this;
      }

      /**
       * Registers one or more class polyfill. Class polyfills are both rewritten in place and also
       * installed (so that faster native versions may be preferred if available). The {@code base}
       * parameter is a qualified name prefix added to the class name to get the polyfill's name. A
       * sibling method with the {@code $install} suffix should also be present. For example,
       * defining {@code addClasses(ES6, ES5, "$jscomp", "Map", "Set")} will cause {@code new Map()}
       * to be rewritten as {@code new $jscomp.Map()} and will insert {@code $jscomp.Map$install();}
       * at the top of the source file whenever the output version is less than {@code
       * nativeVersion}.
       *
       * <p>If {@code base} is blank, then no polyfills will be installed. This is useful for
       * documenting unimplemented polyfills, and will trigger a warning if the language output mode
       * is less than the native version.
       */
      Builder addClasses(
          FeatureSet nativeVersion, FeatureSet polyfillVersion, String base, String... classes) {
        for (String className : classes) {
          String polyfillName = base + "." + className;
          Polyfill polyfill =
              !base.isEmpty()
                  ? new Polyfill(
                      nativeVersion, polyfillVersion, polyfillName, polyfillName + "$install();")
                  : new Polyfill(nativeVersion, polyfillVersion, "", "");
          staticsBuilder.put(className, polyfill);
          staticsBuilder.put(GLOBAL + className, polyfill);
          staticsBuilder.put(WINDOW + className, polyfill);
        }
        return this;
      }

      /** Builds the {@link Polyfills}. */
      Polyfills build() {
        return new Polyfills(this);
      }
    }
  }

  // TODO(sdh): ES6 output is still incomplete, so it's reasonable to use
  // --language_out=ES5 even if targetting ES6 browsers - we need to find a way
  // to distinguish this case and not give warnings for implemented features.

  private static final Polyfills POLYFILLS =
      new Polyfills.Builder()
          // Polyfills not (yet) implemented.
          .addClasses(ES6, ES6, "", "Proxy", "Reflect")
          .addClasses(ES6_IMPL, ES6_IMPL, "", "WeakMap", "WeakSet")
          // TODO(sdh): typed arrays??? these are implemented everywhere except in IE9,
          //            and introducing warnings would be problematic.
          .addStatics(ES6_IMPL, ES6_IMPL, "", "Object", "getOwnPropertySymbols", "setPrototypeOf")
          .addStatics(ES6_IMPL, ES6_IMPL, "", "String", "raw")
          .addMethods(ES6_IMPL, ES6_IMPL, "", "normalize")

          // Implemented elsewhere (so no rewrite here)
          .addClasses(ES6_IMPL, ES3, "", "Symbol")

          // NOTE: The following polyfills will be implemented ASAP.  Once each is implemented,
          // its output language will be changed from ES6 to ES3 and the polyfill namespace
          // ($jscomp or $jscomp.*) will replace the empty string argument indicating that the
          // polyfill should actually be used.

          // Implemented classes.
          .addClasses(ES6_IMPL, ES3, "$jscomp", "Map", "Set")
          // Math methods.
          .addStatics(
              ES6_IMPL,
              ES3,
              "$jscomp.math",
              "Math",
              "clz32",
              "imul",
              "sign",
              "log2",
              "log10",
              "log1p",
              "expm1",
              "cosh",
              "sinh",
              "tanh",
              "acosh",
              "asinh",
              "atanh",
              "hypot",
              "trunc",
              "cbrt")
          // Number methods.
          .addStatics(
              ES6_IMPL,
              ES3,
              "$jscomp.number",
              "Number",
              "isFinite",
              "isInteger",
              "isNaN",
              "isSafeInteger",
              "EPSILON",
              "MAX_SAFE_INTEGER",
              "MIN_SAFE_INTEGER")
          // Object methods.
          .addStatics(ES6_IMPL, ES3, "$jscomp.object", "Object", "assign", "is")
          // (Soon-to-be implemented) String methods.
          .addStatics(ES6_IMPL, ES6_IMPL, "", "String", "fromCodePoint")
          .addMethods(
              ES6_IMPL, ES6_IMPL, "", "repeat", "codePointAt", "includes", "startsWith", "endsWith")
          // Array methods.
          .addStatics(ES6_IMPL, ES3, "$jscomp.array", "Array", "from", "of")
          .addMethods(
              ES6_IMPL,
              ES3,
              "$jscomp.array",
              "entries",
              "keys",
              "values",
              "copyWithin",
              "fill",
              "find",
              "findIndex")
          .build();

  private final AbstractCompiler compiler;
  private final Polyfills polyfills;
  private GlobalNamespace globals;

  public RewritePolyfills(AbstractCompiler compiler) {
    this(compiler, POLYFILLS);
  }

  // Visible for testing
  RewritePolyfills(AbstractCompiler compiler, Polyfills polyfills) {
    this.compiler = compiler;
    this.polyfills = polyfills;
  }

  @Override
  public void hotSwapScript(Node scriptRoot, Node originalRoot) {
    Traverser traverser = new Traverser();
    NodeTraversal.traverseEs6(compiler, scriptRoot, traverser);
    if (traverser.changed) {
      compiler.needsEs6Runtime = true;
      compiler.reportCodeChange();
    }
  }

  @Override
  public void process(Node externs, Node root) {
    if (languageOutIsAtLeast(ES6) || !compiler.getOptions().rewritePolyfills) {
      return; // no rewriting in this case.
    }
    this.globals = new GlobalNamespace(compiler, externs, root);
    hotSwapScript(root, null);
  }

  private static class InjectedInstaller {
    final JSModule module;
    final String installer;

    InjectedInstaller(JSModule module, String installer) {
      this.module = module;
      this.installer = installer;
    }

    @Override
    public int hashCode() {
      return Objects.hash(module, installer);
    }

    @Override
    public boolean equals(@Nullable Object other) {
      return other instanceof InjectedInstaller
          && ((InjectedInstaller) other).installer.equals(installer)
          && Objects.equals(((InjectedInstaller) other).module, module);
    }
  }

  private class Traverser extends AbstractPostOrderCallback {

    Set<InjectedInstaller> installers = new HashSet<>();
    boolean changed = false;

    @Override
    public void visit(NodeTraversal traversal, Node node, Node parent) {

      // Fix types in JSDoc.
      JSDocInfo doc = node.getJSDocInfo();
      if (doc != null) {
        fixJsdoc(traversal.getScope(), doc);
      }

      // Find qualified names that match static calls
      if (node.isQualifiedName()) {
        String name = node.getQualifiedName();
        Polyfill polyfill = null;

        if (polyfills.statics.containsKey(name)) {
          polyfill = polyfills.statics.get(name);
        }

        if (polyfill != null) {
          // Check the scope to make sure it's a global name.
          if (isRootInScope(node, traversal) || NodeUtil.isVarOrSimpleAssignLhs(node, parent)) {
            return;
          }

          if (!languageOutIsAtLeast(polyfill.polyfillVersion)) {
            traversal.report(
                node,
                INSUFFICIENT_OUTPUT_VERSION_ERROR,
                name,
                compiler.getOptions().getLanguageOut().toString(),
                polyfill.polyfillVersion.toString());
          }
          if (!languageOutIsAtLeast(polyfill.nativeVersion)) {

            if (!polyfill.installer.isEmpty()) {
              // Note: add the installer *before* replacing the node!
              addInstaller(node, polyfill.installer);
            }

            if (!polyfill.rewrite.isEmpty()) {
              changed = true;
              Node replacement = NodeUtil.newQName(compiler, polyfill.rewrite);
              replacement.useSourceInfoIfMissingFromForTree(node);
              parent.replaceChild(node, replacement);
            }
          }

          // TODO(sdh): consider warning if language_in is too low?  it's not really any
          // harm, and we can't do it consistently for the prototype methods, so maybe
          // it's not worth doing here, either.

          return; // isGetProp (below) overlaps, so just bail out now
        }
      }

      // Add any requires that *might* match method calls (but don't rewrite anything)
      if (node.isGetProp() && node.getLastChild().isString()) {
        for (Polyfill polyfill : polyfills.methods.get(node.getLastChild().getString())) {
          if (!languageOutIsAtLeast(polyfill.nativeVersion) && !polyfill.installer.isEmpty()) {
            // Check if this is a global function.
            if (!isStaticFunction(node, traversal)) {
              addInstaller(node, polyfill.installer);
            }
          }
        }
      }
    }

    private boolean isStaticFunction(Node node, NodeTraversal traversal) {
      if (!node.isQualifiedName()) {
        return false;
      }
      String root = NodeUtil.getRootOfQualifiedName(node).getQualifiedName();
      if (globals == null) {
        return false;
      }
      GlobalNamespace.Name fullName = globals.getOwnSlot(node.getQualifiedName());
      GlobalNamespace.Name rootName = globals.getOwnSlot(root);
      if (fullName == null || rootName == null) {
        return false;
      }
      GlobalNamespace.Ref rootDecl = rootName.getDeclaration();
      if (rootDecl == null) {
        return false;
      }
      Node globalDeclNode = rootDecl.getNode();
      if (globalDeclNode == null) {
        return false; // don't know where the root came from so assume it could be anything
      }
      Var rootScope = traversal.getScope().getVar(root);
      if (rootScope == null) {
        return true; // root is not in the current scope, so it's a static function
      }
      Node scopeDeclNode = rootScope.getNode();
      return scopeDeclNode == globalDeclNode; // is the global name currently in scope?
    }

    // Fix all polyfill type references in any JSDoc.
    private void fixJsdoc(Scope scope, JSDocInfo doc) {
      for (Node node : doc.getTypeNodes()) {
        fixJsdocType(scope, node);
      }
    }

    private void fixJsdocType(Scope scope, Node node) {
      if (node.isString()) {
        Polyfill polyfill = polyfills.statics.get(node.getString());
        // Note: all classes are unqualified names, so we don't need to deal with dots
        if (polyfill != null
            && scope.getVar(node.getString()) == null
            && !languageOutIsAtLeast(polyfill.nativeVersion)) {
          node.setString(polyfill.rewrite);
        }
      }
      for (Node child = node.getFirstChild(); child != null; child = child.getNext()) {
        fixJsdocType(scope, child);
      }
    }

    private void addInstaller(Node sourceNode, String function) {
      // Find the module
      InputId inputId = sourceNode.getInputId();
      CompilerInput input = inputId != null ? compiler.getInput(inputId) : null;
      JSModule module = input != null ? input.getModule() : null;
      InjectedInstaller injected = new InjectedInstaller(module, function);
      if (installers.add(injected)) {
        changed = true;
        Node installer = compiler.parseSyntheticCode(function).removeChildren();
        installer.useSourceInfoIfMissingFromForTree(sourceNode);
        Node enclosingScript = NodeUtil.getEnclosingScript(sourceNode);
        enclosingScript.addChildrenToFront(installer);
      }
    }
  }

  private boolean languageOutIsAtLeast(LanguageMode mode) {
    return compiler.getOptions().getLanguageOut().compareTo(mode) >= 0;
  }

  private boolean languageOutIsAtLeast(FeatureSet features) {
    switch (features.version()) {
      case "ts":
        return languageOutIsAtLeast(LanguageMode.ECMASCRIPT6_TYPED);
      case "es6":
      case "es6-impl": // TODO(sdh): support a separate language mode for es6-impl?
        return languageOutIsAtLeast(LanguageMode.ECMASCRIPT6);
      case "es5":
        return languageOutIsAtLeast(LanguageMode.ECMASCRIPT5);
      case "es3":
        return languageOutIsAtLeast(LanguageMode.ECMASCRIPT3);
      default:
        return false;
    }
  }

  private static boolean isRootInScope(Node node, NodeTraversal traversal) {
    String rootName = NodeUtil.getRootOfQualifiedName(node).getQualifiedName();
    return traversal.getScope().getVar(rootName) != null;
  }
}