All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.ExpandJqueryAliases Maven / Gradle / Ivy

Go to download

Closure Compiler is a JavaScript optimizing compiler. It parses your JavaScript, analyzes it, removes dead code and rewrites and minimizes what's left. It also checks syntax, variable references, and types, and warns about common JavaScript pitfalls. It is used in many of Google's JavaScript apps, including Gmail, Google Web Search, Google Maps, and Google Docs.

There is a newer version: v20240317
Show newest version
/*
 * Copyright 2011 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.javascript.jscomp;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback;
import com.google.javascript.jscomp.NodeTraversal.ScopedCallback;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

/**
 * 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 =
      DiagnosticType.error("JSC_JQUERY_UNABLE_TO_EXPAND_INVALID_NAME",
          "jQuery.expandedEach expansion would result in an invalid property name.");

  static final DiagnosticType JQUERY_UNABLE_TO_EXPAND_INVALID_NAME_WITH_NAME =
      DiagnosticType.error("JSC_JQUERY_UNABLE_TO_EXPAND_INVALID_NAME_WITH_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 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;
    boolean useTypesForOptimization = compiler.getOptions().useTypesForLocalOptimization;
    this.peepholePasses = new PeepholeOptimizationsPass(compiler,
        new PeepholeMinimizeConditions(late, useTypesForOptimization),
        new PeepholeSubstituteAlternateSyntax(late),
        new PeepholeReplaceKnownMethods(late, useTypesForOptimization),
        new PeepholeRemoveDeadCode(),
        new PeepholeFoldConstants(late, useTypesForOptimization),
        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.addChildToFront(IR.exprResult(assign).srcref(n));
    }

    while (extendArg.hasChildren()) {
      Node currentProp = extendArg.removeFirstChild();
      Node propValue;
      if (currentProp.hasChildren()) {
        propValue = currentProp.getLastChild().detach();
      } else {
        propValue = IR.name(currentProp.getString()).srcref(currentProp);
      }

      Node newProp;
      if (currentProp.isQuotedString()) {
        newProp = IR.getelem(objectToExtend.cloneTree(),
            currentProp).srcref(currentProp);
      } else if (currentProp.isComputedProp()) {
        Node childOfcompProp = currentProp.removeFirstChild();
        newProp = IR.getelem(objectToExtend.cloneTree(),
            childOfcompProp).srcref(currentProp);
      } else {
        currentProp.setToken(Token.STRING);
        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.replaceWith(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.detach();
      }
      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.removeChild(newCallTarget.getNext());
      }
      n.addChildToBack(IR.thisNode().srcref(n));
    }
    compiler.reportCodeChange();
  }

  /**
   * Expand a jQuery.expandedEach call
   *
   * 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.getSecondChild();

    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.getSecondChild());

    // Create a reference tree
    Node nClone = n.cloneTree();

    objectToLoopOver = nClone.getSecondChild();

    // 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 keyNodeReferences = new ArrayList<>();
    List 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 keyNodes, List 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();
    Node 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();
          if (val == null) {
            val = IR.name(key.getString());
          }
        }
      }

      // Keep track of the replaced nodes so we can reset the tree
      List newKeys = new ArrayList<>();
      List newValues = new ArrayList<>();
      List origGetElems = new ArrayList<>();
      List newGetProps = new ArrayList<>();

      // Replace all of the key nodes with the prop name
      for (int j = 0; j < keyNodes.size(); j++) {
        if (key.isComputedProp()) {
          t.report(key, JQUERY_UNABLE_TO_EXPAND_INVALID_NAME);
          return null;
        }
        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.getSecondChild());
          Node prop = ancestorClone.getSecondChild();

          if (prop.isString() &&
            NodeUtil.isValidPropertyName(LanguageMode.ECMASCRIPT3, prop.getString())) {
            Node target = ancestorClone.getFirstChild();
            Node newGetProp = IR.getprop(target.detach(),
                prop.detach());
            newGetProps.add(newGetProp);
            origGetElems.add(ancestor);
            ancestor.replaceWith(newGetProp);
          } else {
            if (prop.isString() &&
                !NodeUtil.isValidPropertyName(LanguageMode.ECMASCRIPT3, prop.getString())) {
              t.report(n,
                  JQUERY_UNABLE_TO_EXPAND_INVALID_NAME_WITH_NAME,
                  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.replaceWith(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).replaceWith(origGetElems.get(j));
      }
      for (int j = 0; j < newKeys.size(); j++) {
        newKeys.get(j).replaceWith(keyNodes.get(j));
      }
      for (int j = 0; j < newValues.size(); j++) {
        newValues.get(j).replaceWith(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().detach();
        grandparent.addChildAfter(child, insertAfter);
        insertAfter = child;
      }
      grandparent.removeChild(parent);
    } else {
      // Return the original object
      Node callTarget = n.getFirstChild();
      Node objectToLoopOver = callTarget.getNext();

      objectToLoopOver.detach();
      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.removeChild(fnc.getNext());
      }
    }
    compiler.reportCodeChange();
  }

  private boolean isArrayLitValidForExpansion(Node n) {
    Iterator 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 keyReferences;
    private List valueReferences;

    FindCallbackArgumentReferences(Node functionRoot, List keyReferences,
        List valueReferences, boolean useArrayMode) {
      Preconditions.checkState(functionRoot.isFunction());

      String keyString = null;
      String 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.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) { }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy