/** @return Whether the name is used in a way that might be a candidate for inlining. */
  static boolean isCandidateUsage(Node name) {
    Node parent = name.getParent();
    Preconditions.checkState(name.isName());
    if (parent.isVar() || parent.isFunction()) {
      // This is a declaration.  Duplicate declarations are handle during
      // function candidate gathering.
      return true;
    }

    if (parent.isCall() && parent.getFirstChild() == name) {
      // This is a normal reference to the function.
      return true;
    }

    // Check for a ".call" to the named function:
    //   CALL
    //     GETPROP/GETELEM
    //       NAME
    //       STRING == "call"
    //     This-Value
    //     Function-parameter-1
    //     ...
    if (NodeUtil.isGet(parent)
        && name == parent.getFirstChild()
        && name.getNext().isString()
        && name.getNext().getString().equals("call")) {
      Node gramps = name.getAncestor(2);
      if (gramps.isCall() && gramps.getFirstChild() == parent) {
        // Yep, a ".call".
        return true;
      }
    }
    return false;
  }
  private boolean matchesNodeShape(Node template, Node ast) {
    if (isTemplateParameterNode(template)) {
      // Match the entire expression but only if it is an expression.
      return !NodeUtil.isStatement(ast);
    } else if (isTemplateLocalNameNode(template)) {
      // Match any name. Maybe match locals here.
      if (!ast.isName()) {
        return false;
      }
    } else if (template.isCall()) {
      // Loosely match CALL nodes. isEquivalentToShallow checks free calls against non-free calls,
      // but the template should ignore that distinction.
      if (ast == null || !ast.isCall() || ast.getChildCount() != template.getChildCount()) {
        return false;
      }
      // But check any children.
    } else if (!template.isEquivalentToShallow(ast)) {
      return false;
    }

    // isEquivalentToShallow guarantees the child counts match
    Node templateChild = template.getFirstChild();
    Node astChild = ast.getFirstChild();
    while (templateChild != null) {
      if (!matchesNodeShape(templateChild, astChild)) {
        return false;
      }
      templateChild = templateChild.getNext();
      astChild = astChild.getNext();
    }
    return true;
  }
Example #3
0
 /**
  * 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();
 }
  /**
   * 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();
  }
    void visitSubtree(Node n, Node parent) {
      if (n.isCall()) {
        boolean isModuleDetected = codingConvention.extractIsModuleFile(n, parent);
        if (isModuleDetected) {
          this.isModuleFile = true;
        }

        String require = codingConvention.extractClassNameIfRequire(n, parent);
        if (require != null) {
          requires.add(require);
        }

        String provide = codingConvention.extractClassNameIfProvide(n, parent);
        if (provide != null) {
          provides.add(provide);
        }
        return;
      } else if (parent != null && !parent.isExprResult() && !parent.isScript()) {
        return;
      }

      for (Node child = n.getFirstChild(); child != null; child = child.getNext()) {
        visitSubtree(child, n);
      }
    }
 @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 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;
 }
Example #8
0
 @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);
     }
   }
 }
  /** 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;
  }
  @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 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));
    }
 @Override
 public FlowScope getPreciserScopeKnowingConditionOutcome(
     Node condition, FlowScope blindScope, boolean outcome) {
   if (condition.isCall() && condition.getChildCount() == 2) {
     Node callee = condition.getFirstChild();
     Node param = condition.getLastChild();
     if (callee.isGetProp() && param.isQualifiedName()) {
       JSType paramType = getTypeIfRefinable(param, blindScope);
       Node left = callee.getFirstChild();
       Node right = callee.getLastChild();
       if (left.isName() && "goog".equals(left.getString()) && right.isString()) {
         Function<TypeRestriction, JSType> restricter = restricters.get(right.getString());
         if (restricter != null) {
           return restrictParameter(param, paramType, blindScope, restricter, outcome);
         }
       }
     }
   }
   return nextPreciserScopeKnowingConditionOutcome(condition, blindScope, outcome);
 }
Example #13
0
    /**
     * There are two types of calls we are interested in calls without explicit "this" values (what
     * we are call "free" calls) and direct call to eval.
     */
    private static void annotateCalls(Node n) {
      Preconditions.checkState(n.isCall());

      // Keep track of of the "this" context of a call.  A call without an
      // explicit "this" is a free call.
      Node first = n.getFirstChild();

      // ignore cast nodes.
      while (first.isCast()) {
        first = first.getFirstChild();
      }

      if (!NodeUtil.isGet(first)) {
        n.putBooleanProp(Node.FREE_CALL, true);
      }

      // Keep track of the context in which eval is called. It is important
      // to distinguish between "(0, eval)()" and "eval()".
      if (first.isName() && "eval".equals(first.getString())) {
        first.putBooleanProp(Node.DIRECT_EVAL, true);
      }
    }
 private void checkRequireCall(NodeTraversal t, Node callNode, Node parent) {
   Preconditions.checkState(callNode.isCall());
   switch (parent.getType()) {
     case Token.EXPR_RESULT:
       return;
     case Token.GETPROP:
       if (parent.getParent().isName()) {
         checkRequireCall(t, callNode, parent.getParent());
         return;
       }
       break;
     case Token.NAME:
     case Token.OBJECT_PATTERN:
       {
         Node declaration = parent.getParent();
         if (declaration.getChildCount() != 1) {
           t.report(declaration, ONE_REQUIRE_PER_DECLARATION);
         }
         return;
       }
   }
   t.report(callNode, REQUIRE_NOT_AT_TOP_LEVEL);
 }
 /** @return Whether the call represents a call to Polymer. */
 private static boolean isPolymerCall(Node value) {
   return value != null && value.isCall() && value.getFirstChild().matchesQualifiedName("Polymer");
 }
  /**
   * 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);
  }
 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;
 }
 public boolean isJqueryExpandedEachCall(Node call, String qName) {
   Preconditions.checkArgument(call.isCall());
   return call.getFirstChild() != null && JQUERY_EXPANDED_EACH_NAME.equals(qName);
 }
Example #20
0
  @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);
      }
    }
  }
  /**
   * Tries to optimize all the arguments array access in this scope by assigning a name to each
   * element.
   *
   * @param scope scope of the function
   * @return true if any modification has been done to the AST
   */
  private boolean tryReplaceArguments(Scope scope) {

    Node parametersList = scope.getRootNode().getSecondChild();
    Preconditions.checkState(parametersList.isParamList());

    // Keep track of rather this function modified the AST and needs to be
    // reported back to the compiler later.
    boolean changed = false;

    // Number of parameter that can be accessed without using the arguments
    // array.
    int numNamedParameter = parametersList.getChildCount();

    // We want to guess what the highest index that has been access from the
    // arguments array. We will guess that it does not use anything index higher
    // than the named parameter list first until we see other wise.
    int highestIndex = numNamedParameter - 1;

    // Iterate through all the references to arguments array in the function to
    // determine the real highestIndex.
    for (Node ref : currentArgumentsAccess) {

      Node getElem = ref.getParent();

      // Bail on anything but argument[c] access where c is a constant.
      // TODO(user): We might not need to bail out all the time, there might
      // be more cases that we can cover.
      if (!getElem.isGetElem() || ref != getElem.getFirstChild()) {
        return false;
      }

      Node index = ref.getNext();

      // We have something like arguments[x] where x is not a constant. That
      // means at least one of the access is not known.
      if (!index.isNumber() || index.getDouble() < 0) {
        // TODO(user): Its possible not to give up just yet. The type
        // inference did a 'semi value propagation'. If we know that string
        // is never a subclass of the type of the index. We'd know that
        // it is never 'callee'.
        return false; // Give up.
      }

      Node getElemParent = getElem.getParent();
      // When we have argument[0](), replacing it with a() is semantically
      // different if argument[0] is a function call that refers to 'this'
      if (getElemParent.isCall() && getElemParent.getFirstChild() == getElem) {
        // TODO(user): We can consider using .call() if aliasing that
        // argument allows shorter alias for other arguments.
        return false;
      }

      // Replace the highest index if we see an access that has a higher index
      // than all the one we saw before.
      int value = (int) index.getDouble();
      if (value > highestIndex) {
        highestIndex = value;
      }
    }

    // Number of extra arguments we need.
    // For example: function() { arguments[3] } access index 3 so
    // it will need 4 extra named arguments to changed into:
    // function(a,b,c,d) { d }.
    int numExtraArgs = highestIndex - numNamedParameter + 1;

    // Temporary holds the new names as string for quick access later.
    String[] argNames = new String[numExtraArgs];

    // Insert the formal parameter to the method's signature.
    // Example: function() --> function(r0, r1, r2)
    for (int i = 0; i < numExtraArgs; i++) {
      String name = getNewName();
      argNames[i] = name;
      parametersList.addChildToBack(IR.name(name).useSourceInfoIfMissingFrom(parametersList));
      changed = true;
    }

    // This loop performs the replacement of arguments[x] -> a if x is known.
    for (Node ref : currentArgumentsAccess) {
      Node index = ref.getNext();

      // Skip if it is unknown.
      if (!index.isNumber()) {
        continue;
      }
      int value = (int) index.getDouble();

      // Unnamed parameter.
      if (value >= numNamedParameter) {
        ref.getGrandparent()
            .replaceChild(ref.getParent(), IR.name(argNames[value - numNamedParameter]));
      } else {

        // Here, for no apparent reason, the user is accessing a named parameter
        // with arguments[idx]. We can replace it with the actual name for them.
        Node name = parametersList.getFirstChild();

        // This is a linear search for the actual name from the signature.
        // It is not necessary to make this fast because chances are the user
        // will not deliberately write code like this.
        for (int i = 0; i < value; i++) {
          name = name.getNext();
        }
        ref.getGrandparent().replaceChild(ref.getParent(), IR.name(name.getString()));
      }
      changed = true;
    }

    return changed;
  }
  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);
      }
    }
  }
 /** 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);
 }
  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();
  }
    @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);
      }
    }