com.google.javascript.jscomp.ExpandJqueryAliases Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler-unshaded Show documentation
Show all versions of closure-compiler-unshaded Show documentation
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.
/*
* 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;
this.peepholePasses = new PeepholeOptimizationsPass(compiler,
new PeepholeMinimizeConditions(late),
new PeepholeSubstituteAlternateSyntax(late),
new PeepholeReplaceKnownMethods(late),
new PeepholeRemoveDeadCode(),
new PeepholeFoldConstants(late, compiler.getOptions().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.addChildrenToFront(IR.exprResult(assign).srcref(n));
}
while (extendArg.hasChildren()) {
Node currentProp = extendArg.removeFirstChild();
Node propValue;
if (currentProp.hasChildren()) {
propValue = currentProp.getLastChild().detachFromParent();
} 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.setType(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.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
*
* 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(), 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.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_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.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 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, 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 - 2025 Weber Informatics LLC | Privacy Policy