com.google.javascript.jscomp.ExploitAssigns Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler-linter Show documentation
Show all versions of closure-compiler-linter 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.
This binary checks for style issues such as incorrect or missing JSDoc
usage, and missing goog.require() statements. It does not do more advanced
checks such as typechecking.
/*
* Copyright 2006 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.javascript.rhino.Node;
/**
* Tries to chain assignments together.
*
* @author [email protected] (Nick Santos)
*
*/
class ExploitAssigns extends AbstractPeepholeOptimization {
@Override
Node optimizeSubtree(Node subtree) {
for (Node child = subtree.getFirstChild(); child != null;) {
Node next = child.getNext();
if (NodeUtil.isExprAssign(child)) {
collapseAssign(child.getFirstChild(), child, subtree);
}
child = next;
}
return subtree;
}
/** Try to collapse the given assign into subsequent expressions. */
private void collapseAssign(Node assign, Node expr, Node exprParent) {
Node leftValue = assign.getFirstChild();
Node rightValue = leftValue.getNext();
if (leftValue.isDestructuringPattern()) {
// We don't collapse expressions containing these because they can have side effects:
// - Reassigning RHS names. (e.g. `() => { ({c: a} = a); return a; }`).
// - Calling a getter defined on the RHS.
// - Evaluating a default value.
// TODO(b/123102446): Check for these issues and optimize when they aren't present.
return;
} else if (isCollapsibleValue(leftValue, true)
&& collapseAssignEqualTo(expr, exprParent, leftValue)) {
// Condition has side-effects.
} else if (isCollapsibleValue(rightValue, false)
&& collapseAssignEqualTo(expr, exprParent, rightValue)) {
// Condition has side-effects.
} else if (rightValue.isAssign()) {
// Recursively deal with nested assigns.
collapseAssign(rightValue, expr, exprParent);
}
}
/**
* Determines whether we know enough about the given value to be able
* to collapse it into subsequent expressions.
*
* For example, we can collapse booleans and variable names:
*
* x = 3; y = x; // y = x = 3;
* a = true; b = true; // b = a = true;
*
* But we won't try to collapse complex expressions.
*
* @param value The value node.
* @param isLValue Whether it's on the left-hand side of an expr.
*/
private static boolean isCollapsibleValue(Node value, boolean isLValue) {
switch (value.getToken()) {
case GETPROP:
// Do not collapse GETPROPs on arbitrary objects, because
// they may be implemented setter functions, and oftentimes
// setter functions fail on native objects. This is OK for "THIS"
// objects, because we assume that they are non-native.
return !isLValue || value.getFirstChild().isThis();
case NAME:
return true;
default:
return NodeUtil.isImmutableValue(value);
}
}
/**
* Collapse the given assign expression into the expression directly following it, if possible.
*
* @param expr The expression that may be moved.
* @param exprParent The parent of {@code expr}.
* @param value The value of this expression, expressed as a node. Each expression may have
* multiple values, so this function may be called multiple times for the same expression. For
* example,
* a = true;
*
is equal to the name "a" and the boolean "true".
* @return Whether the expression was collapsed successfully.
*/
private boolean collapseAssignEqualTo(Node expr, Node exprParent, Node value) {
Node assign = expr.getFirstChild();
Node parent = exprParent;
Node next = expr.getNext();
while (next != null) {
switch (next.getToken()) {
case AND:
case OR:
case HOOK:
case IF:
case RETURN:
case EXPR_RESULT:
// Dive down the left side
parent = next;
next = next.getFirstChild();
break;
case CONST:
case LET:
case VAR:
if (next.getFirstChild().hasChildren()) {
parent = next.getFirstChild();
next = parent.getFirstChild();
break;
}
return false;
case GETPROP:
case NAME:
if (next.isQualifiedName()) {
if (value.isQualifiedName() &&
next.matchesQualifiedName(value)) {
// If the previous expression evaluates to value of a
// qualified name, and that qualified name is used again
// shortly, then we can exploit the assign here.
// Verify the assignment doesn't change its own value.
if (!isSafeReplacement(next, assign)) {
return false;
}
exprParent.removeChild(expr);
expr.removeChild(assign);
parent.replaceChild(next, assign);
reportChangeToEnclosingScope(parent);
return true;
}
}
return false;
case ASSIGN:
// Assigns are really tricky. In lots of cases, we want to inline
// into the right side of the assign. But the left side of the
// assign is evaluated first, and it may have convoluted logic:
// a = null;
// (a = b).c = null;
// We don't want to exploit the first assign. Similarly:
// a.b = null;
// a.b.c = null;
// We don't want to exploit the first assign either.
//
// To protect against this, we simply only inline when the left side
// is guaranteed to evaluate to the same L-value no matter what.
Node leftSide = next.getFirstChild();
if (leftSide.isName() || (leftSide.isGetProp() && leftSide.getFirstChild().isThis())) {
// Dive down the right side of the assign.
parent = next;
next = leftSide.getNext();
break;
} else {
return false;
}
default:
if (NodeUtil.isImmutableValue(next)
&& next.isEquivalentTo(value)) {
// If the r-value of the expr assign is an immutable value,
// and the value is used again shortly, then we can exploit
// the assign here.
exprParent.removeChild(expr);
expr.removeChild(assign);
parent.replaceChild(next, assign);
reportChangeToEnclosingScope(parent);
return true;
}
// Return without inlining a thing
return false;
}
}
return false;
}
/**
* Checks name referenced in node to determine if it might have
* changed.
* @return Whether the replacement can be made.
*/
private boolean isSafeReplacement(Node node, Node replacement) {
// No checks are needed for simple names.
if (node.isName()) {
return true;
}
checkArgument(node.isGetProp());
while (node.isGetProp()) {
node = node.getFirstChild();
}
return !(node.isName() && isNameAssignedTo(node.getString(), replacement));
}
/**
* @return Whether name is assigned in the expression rooted at node.
*/
private static boolean isNameAssignedTo(String name, Node node) {
for (Node c = node.getFirstChild(); c != null; c = c.getNext()) {
if (isNameAssignedTo(name, c)) {
return true;
}
}
if (node.isName()) {
Node parent = node.getParent();
if (parent.isAssign() && parent.getFirstChild() == node) {
if (name.equals(node.getString())) {
return true;
}
}
}
return false;
}
}