
com.google.javascript.jscomp.RemoveUnusedVars Maven / Gradle / Ivy
/*
* Copyright 2008 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 static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.javascript.jscomp.CodingConvention.SubclassRelationship;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Garbage collection for variable and function definitions. Basically performs
* a mark-and-sweep type algorithm over the JavaScript parse tree.
*
* For each scope:
* (1) Scan the variable/function declarations at that scope.
* (2) Traverse the scope for references, marking all referenced variables.
* Unlike other compiler passes, this is a pre-order traversal, not a
* post-order traversal.
* (3) If the traversal encounters an assign without other side-effects,
* create a continuation. Continue the continuation iff the assigned
* variable is referenced.
* (4) When the traversal completes, remove all unreferenced variables.
*
* If it makes it easier, you can think of the continuations of the traversal
* as a reference graph. Each continuation represents a set of edges, where the
* source node is a known variable, and the destination nodes are lazily
* evaluated when the continuation is executed.
*
* This algorithm is similar to the algorithm used by {@code SmartNameRemoval}.
* {@code SmartNameRemoval} maintains an explicit graph of dependencies
* between global symbols. However, {@code SmartNameRemoval} cannot handle
* non-trivial edges in the reference graph ("A is referenced iff both B and C
* are referenced"), or local variables. {@code SmartNameRemoval} is also
* substantially more complicated because it tries to handle namespaces
* (which is largely unnecessary in the presence of {@code CollapseProperties}.
*
* This pass also uses a more complex analysis of assignments, where
* an assignment to a variable or a property of that variable does not
* necessarily count as a reference to that variable, unless we can prove
* that it modifies external state. This is similar to
* {@code FlowSensitiveInlineVariables}, except that it works for variables
* used across scopes.
*
* Multiple datastructures are used to accumulate nodes, some of which are
* later removed. Since some nodes encompass a subtree of nodes, the removal
* can sometimes pre-remove other nodes which are also referenced in these
* datastructures for later removal. Attempting double-removal violates scope
* change notification constraints so there is a desire to excise
* already-removed subtree nodes from these datastructures. But not all of the
* datastructures are conducive to flexible removal and the ones that are
* conducive don't necessarily track all flavors of nodes. So instead of
* updating datastructures on the fly a pre-check is performed to skip
* already-removed nodes right before the moment an attempt to remove them
* would otherwise be made.
*
* @author [email protected] (Nick Santos)
*/
class RemoveUnusedVars implements CompilerPass {
// Properties that are implicitly used as part of the JS language.
private static final ImmutableSet IMPLICITLY_USED_PROPERTIES =
ImmutableSet.of("length", "toString", "valueOf", "constructor");
private final AbstractCompiler compiler;
private final CodingConvention codingConvention;
private final boolean removeGlobals;
private final boolean preserveFunctionExpressionNames;
/**
* Used to hold continuations that need to be invoked.
*
* When we find a subtree of the AST that may not need to be traversed, we create a Continuation
* for it. If we later discover that we do need to traverse it, we add it to this worklist
* rather than traversing it immediately. If we invoked the traversal immediately, we could
* end up modifying a data structure in the traversal as we're iterating over it.
*/
private final Deque worklist = new ArrayDeque<>();
private final Map varInfoMap = new HashMap<>();
private final Set referencedPropertyNames = new HashSet<>(IMPLICITLY_USED_PROPERTIES);
/** Stores Removable objects for each property name that is currently considered removable. */
private final Multimap removablesForPropertyNames = HashMultimap.create();
/** Single value to use for all vars for which we cannot remove anything at all. */
private final VarInfo canonicalTotallyUnremovableVarInfo;
/**
* Keep track of scopes that we've traversed.
*/
private final List allFunctionParamScopes = new ArrayList<>();
private final ScopeCreator scopeCreator;
private final boolean removeUnusedProperties;
RemoveUnusedVars(
AbstractCompiler compiler,
boolean removeGlobals,
boolean preserveFunctionExpressionNames,
boolean removeUnusedProperties) {
this.compiler = compiler;
this.codingConvention = compiler.getCodingConvention();
this.removeGlobals = removeGlobals;
this.preserveFunctionExpressionNames = preserveFunctionExpressionNames;
this.removeUnusedProperties = removeUnusedProperties;
this.scopeCreator = new Es6SyntacticScopeCreator(compiler);
// All Vars that are completely unremovable will share this VarInfo instance.
canonicalTotallyUnremovableVarInfo = new VarInfo();
canonicalTotallyUnremovableVarInfo.setIsExplicitlyNotRemovable();
}
/**
* Traverses the root, removing all unused variables. Multiple traversals
* may occur to ensure all unused variables are removed.
*/
@Override
public void process(Node externs, Node root) {
checkState(compiler.getLifeCycleStage().isNormalized());
if (removeUnusedProperties) {
referencedPropertyNames.addAll(compiler.getExternProperties());
}
traverseAndRemoveUnusedReferences(root);
}
/**
* Traverses a node recursively. Call this once per pass.
*/
private void traverseAndRemoveUnusedReferences(Node root) {
// TODO(bradfordcsmith): Include externs in the scope.
// Since we don't do this now, scope.getVar(someExtern) returns null.
Scope scope = scopeCreator.createScope(root, null);
worklist.add(new Continuation(root, scope));
while (!worklist.isEmpty()) {
Continuation continuation = worklist.remove();
continuation.apply();
}
removeUnreferencedVars();
if (removeUnusedProperties) {
removeUnreferencedProperties();
}
for (Scope fparamScope : allFunctionParamScopes) {
removeUnreferencedFunctionArgs(fparamScope);
}
}
private void removeUnreferencedProperties() {
for (Removable removable : removablesForPropertyNames.values()) {
removable.remove(compiler);
}
}
/**
* Traverses everything in the current scope and marks variables that
* are referenced.
*
* During traversal, we identify subtrees that will only be
* referenced if their enclosing variables are referenced. Instead of
* traversing those subtrees, we create a continuation for them,
* and traverse them lazily.
*/
private void traverseNode(Node n, Scope scope) {
Node parent = n.getParent();
Token type = n.getToken();
Var var = null;
switch (type) {
case CATCH:
traverseCatch(n, scope);
break;
case FUNCTION:
{
VarInfo varInfo = null;
// If this function is a removable var, then create a continuation
// for it instead of traversing immediately.
if (NodeUtil.isFunctionDeclaration(n)) {
varInfo = traverseVar(scope.getVar(n.getFirstChild().getString()));
FunctionDeclaration functionDeclaration =
new RemovableBuilder()
.addContinuation(new Continuation(n, scope))
.buildFunctionDeclaration(n);
varInfo.addRemovable(functionDeclaration);
if (parent.isExport()) {
varInfo.markAsReferenced();
}
} else {
traverseFunction(n, scope);
}
}
break;
case ASSIGN:
traverseAssign(n, scope);
break;
case CALL:
traverseCall(n, scope);
break;
case BLOCK:
// This case if for if there are let and const variables in block scopes.
// Otherwise other variables will be hoisted up into the global scope and already be
// handled.
traverseChildren(
n, NodeUtil.createsBlockScope(n) ? scopeCreator.createScope(n, scope) : scope);
break;
case MODULE_BODY:
traverseChildren(n, scopeCreator.createScope(n, scope));
break;
case CLASS:
traverseClass(n, scope);
break;
case CLASS_MEMBERS:
traverseClassMembers(n, scope);
break;
case DEFAULT_VALUE:
traverseDefaultValue(n, scope);
break;
case REST:
traverseRest(n, scope);
break;
case ARRAY_PATTERN:
traverseArrayPattern(n, scope);
break;
case OBJECT_PATTERN:
traverseObjectPattern(n, scope);
break;
case OBJECTLIT:
traverseObjectLiteral(n, scope);
break;
case FOR:
traverseVanillaFor(n, scope);
break;
case FOR_IN:
case FOR_OF:
traverseEnhancedFor(n, scope);
break;
case LET:
case CONST:
case VAR:
// for-loop cases are handled by custom traversal methods.
checkState(NodeUtil.isStatement(n));
traverseDeclarationStatement(n, scope);
break;
case NAME:
// The only cases that should reach this point are parameter declarations and references
// to names. The name node does not have children in these cases.
checkState(!n.hasChildren());
// the parameter declaration is not a read of the name
if (!parent.isParamList()) {
// var|let|const name;
// are handled at a higher level.
checkState(!NodeUtil.isNameDeclaration(parent));
// function name() {}
// class name() {}
// handled at a higher level
checkState(!((parent.isFunction() || parent.isClass()) && parent.getFirstChild() == n));
var = scope.getVar(n.getString());
if (var != null) {
// All name references that aren't handled elsewhere are references to vars.
traverseVar(var).markAsReferenced();
}
}
break;
case GETPROP:
Node objectNode = n.getFirstChild();
Node propertyNameNode = objectNode.getNext();
String propertyName = propertyNameNode.getString();
markPropertyNameReferenced(propertyName);
traverseNode(objectNode, scope);
break;
default:
traverseChildren(n, scope);
break;
}
}
private void traverseCall(Node callNode, Scope scope) {
Node parent = callNode.getParent();
String classVarName = null;
// A call that is a statement unto itself or the left side of a comma expression might be
// a call to a known method for doing class setup
// e.g. $jscomp.inherits(Class, BaseClass) or goog.addSingletonGetter(Class)
// Such methods never have meaningful return values, so we won't look for them in other
// contexts
if (parent.isExprResult() || (parent.isComma() && parent.getFirstChild() == callNode)) {
SubclassRelationship subclassRelationship =
codingConvention.getClassesDefinedByCall(callNode);
if (subclassRelationship != null) {
// e.g. goog.inherits(DerivedClass, BaseClass);
// NOTE: DerivedClass and BaseClass must be QNames. Otherwise getClassesDefinedByCall() will
// return null.
classVarName = subclassRelationship.subclassName;
} else {
// Look for calls to addSingletonGetter calls.
classVarName = codingConvention.getSingletonGetterClassName(callNode);
}
}
Var classVar = (classVarName == null) ? null : scope.getVar(classVarName);
if (classVar == null || !classVar.isGlobal()) {
// This isn't one of the special call types, or it isn't acting on a global class name.
// It would be more correct to only not track when the class name does not
// reference a constructor, but checking that it is a global is easier and mostly the same.
traverseChildren(callNode, scope);
} else {
VarInfo classVarInfo = traverseVar(classVar);
RemovableBuilder builder = new RemovableBuilder();
for (Node child = callNode.getFirstChild(); child != null; child = child.getNext()) {
builder.addContinuation(new Continuation(child, scope));
}
classVarInfo.addRemovable(builder.buildClassSetupCall(callNode));
}
}
private void traverseRest(Node restNode, Scope scope) {
Node target = restNode.getOnlyChild();
if (!target.isName()) {
traverseNode(target, scope);
} else {
Var var = scope.getVar(target.getString());
if (var != null) {
VarInfo varInfo = traverseVar(var);
// NOTE: DestructuringAssign is currently used for both actual destructuring and
// default or rest parameters.
// TODO(bradfordcsmith): Maybe distinguish between these 2 cases.
varInfo.addRemovable(new RemovableBuilder().buildDestructuringAssign(restNode, target));
}
}
}
private void traverseObjectLiteral(Node objectLiteral, Scope scope) {
for (Node propertyNode = objectLiteral.getFirstChild();
propertyNode != null;
propertyNode = propertyNode.getNext()) {
if (propertyNode.isStringKey() && !propertyNode.isQuotedString()) {
// An unquoted property name in an object literal counts as a reference to that property
// name, because of some reflection patterns.
// TODO(bradfordcsmith): Handle this better for `Foo.prototype = {a: 1, b: 2}`
markPropertyNameReferenced(propertyNode.getString());
traverseNode(propertyNode.getFirstChild(), scope);
} else {
traverseNode(propertyNode, scope);
}
}
}
private void traverseCatch(Node catchNode, Scope scope) {
Node exceptionNameNode = catchNode.getFirstChild();
Node block = exceptionNameNode.getNext();
VarInfo exceptionVarInfo =
traverseVar(scope.getVar(exceptionNameNode.getString()));
exceptionVarInfo.setIsExplicitlyNotRemovable();
traverseNode(block, scope);
}
private void traverseEnhancedFor(Node enhancedFor, Scope scope) {
Scope forScope = scopeCreator.createScope(enhancedFor, scope);
// for (iterationTarget in|of collection) body;
Node iterationTarget = enhancedFor.getFirstChild();
Node collection = iterationTarget.getNext();
Node body = collection.getNext();
if (iterationTarget.isName()) {
// using previously-declared loop variable. e.g.
// `for (varName of collection) {}`
Var var = forScope.getVar(iterationTarget.getString());
// NOTE: var will be null if it was declared in externs
if (var != null) {
VarInfo varInfo = traverseVar(var);
varInfo.setIsExplicitlyNotRemovable();
}
} else if (NodeUtil.isNameDeclaration(iterationTarget)) {
// loop has const/var/let declaration
Node declNode = iterationTarget.getOnlyChild();
if (declNode.isDestructuringLhs()) {
// e.g.
// `for (const [a, b] of pairList) {}`
// destructuring is handled at a lower level
// Note that destructuring assignments are always considered to set an unknown value
// equivalent to what we set for the var name case above and below.
// It isn't necessary to set the variable names as not removable, though, because the
// thing that isn't removable is the destructuring pattern itself, which we never remove.
// TODO(bradfordcsmith): The need to explain all the above shows this should be reworked.
traverseNode(declNode, forScope);
} else {
// e.g.
// `for (const varName of collection) {}`
checkState(declNode.isName());
checkState(!declNode.hasChildren());
// We can never remove the loop variable of a for-in or for-of loop, because it's
// essential to loop syntax.
VarInfo varInfo = traverseVar(forScope.getVar(declNode.getString()));
varInfo.setIsExplicitlyNotRemovable();
}
} else {
// using some general LHS value e.g.
// `for ([a, b] of collection) {}` destructuring with existing vars
// `for (a.x of collection) {}` using a property as the loop var
// TODO(bradfordcsmith): This should be considered a write if it's a property reference.
traverseNode(iterationTarget, forScope);
}
traverseNode(collection, forScope);
traverseNode(body, forScope);
}
private void traverseVanillaFor(Node forNode, Scope scope) {
Scope forScope = scopeCreator.createScope(forNode, scope);
Node initialization = forNode.getFirstChild();
Node condition = initialization.getNext();
Node update = condition.getNext();
Node block = update.getNext();
if (NodeUtil.isNameDeclaration(initialization)) {
traverseVanillaForNameDeclarations(initialization, forScope);
} else {
traverseNode(initialization, forScope);
}
traverseNode(condition, forScope);
traverseNode(update, forScope);
traverseNode(block, forScope);
}
private void traverseVanillaForNameDeclarations(Node nameDeclaration, Scope scope) {
for (Node child = nameDeclaration.getFirstChild(); child != null; child = child.getNext()) {
if (!child.isName()) {
// TODO(bradfordcsmith): Customize handling of destructuring
traverseNode(child, scope);
} else {
Node nameNode = child;
@Nullable Node valueNode = child.getFirstChild();
VarInfo varInfo = traverseVar(scope.getVar(nameNode.getString()));
if (valueNode == null) {
varInfo.addRemovable(new RemovableBuilder().buildVanillaForNameDeclaration(nameNode));
} else if (NodeUtil.mayHaveSideEffects(valueNode)) {
// TODO(bradfordcsmith): Actually allow for removing the variable while keeping the
// valueNode for its side-effects.
varInfo.setIsExplicitlyNotRemovable();
traverseNode(valueNode, scope);
} else {
VanillaForNameDeclaration vanillaForNameDeclaration =
new RemovableBuilder()
.setAssignedValue(valueNode)
.addContinuation(new Continuation(valueNode, scope))
.buildVanillaForNameDeclaration(nameNode);
varInfo.addRemovable(vanillaForNameDeclaration);
}
}
}
}
private void traverseDeclarationStatement(Node declarationStatement, Scope scope) {
// Normalization should ensure that declaration statements always have just one child.
Node nameNode = declarationStatement.getOnlyChild();
if (!nameNode.isName()) {
// Destructuring declarations are handled elsewhere.
traverseNode(nameNode, scope);
} else {
Node valueNode = nameNode.getFirstChild();
VarInfo varInfo =
traverseVar(checkNotNull(scope.getVar(nameNode.getString())));
RemovableBuilder builder = new RemovableBuilder();
if (valueNode == null) {
varInfo.addRemovable(builder.buildNameDeclarationStatement(declarationStatement));
} else {
if (NodeUtil.mayHaveSideEffects(valueNode)) {
traverseNode(valueNode, scope);
} else {
builder.addContinuation(new Continuation(valueNode, scope));
}
NameDeclarationStatement removable =
builder.setAssignedValue(valueNode).buildNameDeclarationStatement(declarationStatement);
varInfo.addRemovable(removable);
}
}
}
private void traverseAssign(Node assignNode, Scope scope) {
checkState(NodeUtil.isAssignmentOp(assignNode));
Node lhs = assignNode.getFirstChild();
Node nameNode = null;
Node propertyNode = null;
boolean isVariableAssign = false;
boolean isComputedPropertyAssign = false;
boolean isNamedPropertyAssign = false;
boolean isPrototypeObjectPropertyAssignment = false;
if (lhs.isName()) {
isVariableAssign = true;
nameNode = lhs;
} else if (NodeUtil.isGet(lhs)) {
propertyNode = lhs.getLastChild();
Node possibleNameNode = lhs.getFirstChild();
// Handle assignments to properties of a variable or its prototype property.
// However, don't handle any longer qualified names, because it gets hard to track
// properties of properties.
if (possibleNameNode.isGetProp()
&& possibleNameNode.getSecondChild().getString().equals("prototype")) {
isPrototypeObjectPropertyAssignment = true;
possibleNameNode = possibleNameNode.getFirstChild();
}
if (possibleNameNode.isName()) {
nameNode = possibleNameNode;
if (lhs.isGetProp()) {
isNamedPropertyAssign = true;
} else {
checkState(lhs.isGetElem());
isComputedPropertyAssign = true;
}
}
}
// else LHS is something else, like a destructuring pattern, which will be handled by
// traverseChildren() below
// TODO(bradfordcsmith): Handle destructuring at this level for better clarity and so we can
// do a better job with removal.
// If we successfully identified a name node & there is a corresponding Var,
// then we have a removable assignment.
Var var = (nameNode == null) ? null : scope.getVar(nameNode.getString());
if (var == null) {
traverseChildren(assignNode, scope);
} else {
Node valueNode = assignNode.getLastChild();
RemovableBuilder builder =
new RemovableBuilder()
.setAssignedValue(valueNode)
.setIsPrototypeObjectPropertyAssignment(isPrototypeObjectPropertyAssignment);
if (NodeUtil.isExpressionResultUsed(assignNode) || NodeUtil.mayHaveSideEffects(valueNode)) {
traverseNode(valueNode, scope);
} else {
builder.addContinuation(new Continuation(valueNode, scope));
}
VarInfo varInfo = traverseVar(var);
if (isNamedPropertyAssign) {
varInfo.addRemovable(builder.buildNamedPropertyAssign(assignNode, nameNode, propertyNode));
} else if (isVariableAssign) {
varInfo.addRemovable(builder.buildVariableAssign(assignNode, nameNode));
} else {
checkState(isComputedPropertyAssign);
if (NodeUtil.mayHaveSideEffects(propertyNode)) {
traverseNode(propertyNode, scope);
} else {
builder.addContinuation(new Continuation(propertyNode, scope));
}
varInfo.addRemovable(
builder.buildComputedPropertyAssign(assignNode, nameNode, propertyNode));
}
}
}
private void traverseDefaultValue(Node defaultValueNode, Scope scope) {
Var var;
Node target = defaultValueNode.getFirstChild();
Node value = target.getNext();
if (!target.isName()) {
traverseNode(target, scope);
traverseNode(value, scope);
} else {
var = scope.getVar(target.getString());
if (var == null) {
traverseNode(value, scope);
} else {
VarInfo varInfo = traverseVar(var);
if (NodeUtil.mayHaveSideEffects(value)) {
// TODO(johnlenz): we don't really need to retain all uses of the variable, just
// enough to host the default value assignment.
varInfo.markAsReferenced();
traverseNode(value, scope);
} else {
DestructuringAssign assign =
new RemovableBuilder()
.addContinuation(new Continuation(value, scope))
.buildDestructuringAssign(defaultValueNode, target);
varInfo.addRemovable(assign);
}
}
}
}
private void traverseArrayPattern(Node arrayPattern, Scope scope) {
for (Node c = arrayPattern.getFirstChild(); c != null; c = c.getNext()) {
if (!c.isName()) {
// TODO(bradfordcsmith): Treat destructuring assignments to properties as removable writes.
traverseNode(c, scope);
} else {
Var var = scope.getVar(c.getString());
if (var != null) {
VarInfo varInfo = traverseVar(var);
varInfo.addRemovable(new RemovableBuilder().buildDestructuringAssign(c, c));
}
}
}
}
private void traverseObjectPattern(Node objectPattern, Scope scope) {
for (Node propertyNode = objectPattern.getFirstChild();
propertyNode != null;
propertyNode = propertyNode.getNext()) {
traverseObjectPatternElement(propertyNode, scope);
}
}
private void traverseObjectPatternElement(Node elm, Scope scope) {
// non-null for computed properties
// `{[propertyExpression]: target} = ...`
Node propertyExpression = null;
// non-null for named properties
// `{propertyName: target} = ...`
String propertyName = null;
Node target = null;
Node defaultValue = null;
// Get correct values for all the variables above.
if (elm.isComputedProp()) {
propertyExpression = elm.getFirstChild();
target = elm.getLastChild();
} else {
checkState(elm.isStringKey());
target = elm.getOnlyChild();
// Treat `{'a': x} = ...` like `{['a']: x} = ...`, but it never has side-effects and we
// have no propertyExpression to traverse.
// NOTE: The parser will convert `{1: x} = ...` to `{'1': x} = ...`
if (!elm.isQuotedString()) {
propertyName = elm.getString();
}
}
if (target.isDefaultValue()) {
target = target.getFirstChild();
defaultValue = checkNotNull(target.getNext());
}
// TODO(bradfordcsmith): Handle property assignments also
Var var = target.isName() ? scope.getVar(target.getString()) : null;
// TODO(bradfordcsmith): Arrange to safely remove side-effect cases.
boolean cannotRemove =
var == null
|| (propertyExpression != null && NodeUtil.mayHaveSideEffects(propertyExpression))
|| (defaultValue != null && NodeUtil.mayHaveSideEffects(defaultValue));
if (cannotRemove) {
if (propertyExpression != null) {
traverseNode(propertyExpression, scope);
}
if (propertyName != null) {
markPropertyNameReferenced(propertyName);
}
traverseNode(target, scope);
if (defaultValue != null) {
traverseNode(defaultValue, scope);
}
if (var != null) {
// Since we cannot remove it, we must now treat this usage as a reference.
traverseVar(var).markAsReferenced();
}
} else {
RemovableBuilder builder = new RemovableBuilder();
if (propertyName != null) {
// TODO(bradfordcsmith): Use a continuation here.
markPropertyNameReferenced(propertyName);
}
if (propertyExpression != null) {
builder.addContinuation(new Continuation(propertyExpression, scope));
}
if (defaultValue != null) {
builder.addContinuation(new Continuation(defaultValue, scope));
}
traverseVar(var)
.addRemovable(builder.buildDestructuringAssign(elm, target));
}
}
private void traverseChildren(Node n, Scope scope) {
for (Node c = n.getFirstChild(); c != null; c = c.getNext()) {
traverseNode(c, scope);
}
}
/**
* Handle a class that is not the RHS child of an assignment or a variable declaration
* initializer.
*
* For
* @param classNode
* @param scope
*/
private void traverseClass(Node classNode, Scope scope) {
checkArgument(classNode.isClass());
if (NodeUtil.isClassDeclaration(classNode)) {
traverseClassDeclaration(classNode, scope);
} else {
traverseClassExpression(classNode, scope);
}
}
private void traverseClassDeclaration(Node classNode, Scope scope) {
checkArgument(classNode.isClass());
Node classNameNode = classNode.getFirstChild();
Node baseClassExpression = classNameNode.getNext();
Node classBodyNode = baseClassExpression.getNext();
Scope classScope = scopeCreator.createScope(classNode, scope);
VarInfo varInfo = traverseVar(scope.getVar(classNameNode.getString()));
if (classNode.getParent().isExport()) {
// Cannot remove an exported class.
varInfo.setIsExplicitlyNotRemovable();
traverseNode(baseClassExpression, scope);
// Use traverseChildren() here, because we should not consider any properties on the exported
// class to be removable.
traverseChildren(classBodyNode, classScope);
} else if (NodeUtil.mayHaveSideEffects(baseClassExpression)) {
// TODO(bradfordcsmith): implement removal without losing side-effects for this case
varInfo.setIsExplicitlyNotRemovable();
traverseNode(baseClassExpression, scope);
traverseClassMembers(classBodyNode, classScope);
} else {
RemovableBuilder builder =
new RemovableBuilder()
.addContinuation(new Continuation(baseClassExpression, classScope))
.addContinuation(new Continuation(classBodyNode, classScope));
varInfo.addRemovable(builder.buildClassDeclaration(classNode));
}
}
private void traverseClassExpression(Node classNode, Scope scope) {
checkArgument(classNode.isClass());
Node classNameNode = classNode.getFirstChild();
Node baseClassExpression = classNameNode.getNext();
Node classBodyNode = baseClassExpression.getNext();
Scope classScope = scopeCreator.createScope(classNode, scope);
if (classNameNode.isName()) {
// We may be able to remove the name node if nothing ends up referring to it.
VarInfo varInfo = traverseVar(classScope.getVar(classNameNode.getString()));
varInfo.addRemovable(new RemovableBuilder().buildNamedClassExpression(classNode));
}
// If we're traversing the class expression, we've already decided we cannot remove it.
traverseNode(baseClassExpression, scope);
traverseClassMembers(classBodyNode, classScope);
}
private void traverseClassMembers(Node node, Scope scope) {
checkArgument(node.isClassMembers(), node);
if (removeUnusedProperties) {
for (Node member = node.getFirstChild(); member != null; member = member.getNext()) {
if (member.isMemberFunctionDef() || NodeUtil.isGetOrSetKey(member)) {
// If we get as far as traversing the members of a class, we've already decided that
// we cannot remove the class itself, so just consider individual members for removal.
considerForIndependentRemoval(
new RemovableBuilder()
.addContinuation(new Continuation(member, scope))
.buildMethodDefinition(member));
} else {
checkState(member.isComputedProp());
traverseChildren(member, scope);
}
}
} else {
traverseChildren(node, scope);
}
}
/**
* Traverses a function
*
* ES6 scopes of a function include the parameter scope and the body scope
* of the function.
*
* Note that CATCH blocks also create a new scope, but only for the
* catch variable. Declarations within the block actually belong to the
* enclosing scope. Because we don't remove catch variables, there's
* no need to treat CATCH blocks differently like we do functions.
*/
private void traverseFunction(Node function, Scope parentScope) {
checkState(function.getChildCount() == 3, function);
checkState(function.isFunction(), function);
final Node paramlist = NodeUtil.getFunctionParameters(function);
final Node body = function.getLastChild();
checkState(body.getNext() == null && body.isNormalBlock(), body);
// Checking the parameters
Scope fparamScope = scopeCreator.createScope(function, parentScope);
// Checking the function body
Scope fbodyScope = scopeCreator.createScope(body, fparamScope);
String name = function.getFirstChild().getString();
if (!name.isEmpty()) {
// var x = function funcName() {};
Var var = checkNotNull(fparamScope.getVar(name));
// make sure funcName gets into the varInfoMap so it will be considered for removal.
traverseVar(var);
}
traverseChildren(paramlist, fparamScope);
traverseChildren(body, fbodyScope);
allFunctionParamScopes.add(fparamScope);
}
private boolean canRemoveParameters(Node parameterList) {
checkState(parameterList.isParamList());
Node function = parameterList.getParent();
return removeGlobals && !NodeUtil.isGetOrSetKey(function.getParent());
}
/**
* Removes unreferenced arguments from a function declaration and when
* possible the function's callSites.
*
* @param fparamScope The function parameter
*/
private void removeUnreferencedFunctionArgs(Scope fparamScope) {
// Notice that removing unreferenced function args breaks
// Function.prototype.length. In advanced mode, we don't really care
// about this: we consider "length" the equivalent of reflecting on
// the function's lexical source.
//
// Rather than create a new option for this, we assume that if the user
// is removing globals, then it's OK to remove unused function args.
//
// See http://blickly.github.io/closure-compiler-issues/#253
if (!removeGlobals) {
return;
}
Node function = fparamScope.getRootNode();
checkState(function.isFunction());
if (NodeUtil.isGetOrSetKey(function.getParent())) {
// The parameters object literal setters can not be removed.
return;
}
Node argList = NodeUtil.getFunctionParameters(function);
// Strip as many unreferenced args off the end of the function declaration as possible.
maybeRemoveUnusedTrailingParameters(argList, fparamScope);
// Mark any remaining unused parameters are unused to OptimizeParameters can try to remove
// them.
markUnusedParameters(argList, fparamScope);
}
private void markPropertyNameReferenced(String propertyName) {
if (referencedPropertyNames.add(propertyName)) {
// Continue traversal of all of the property name's values and no longer consider them for
// removal.
for (Removable removable : removablesForPropertyNames.removeAll(propertyName)) {
removable.applyContinuations();
}
}
}
private void considerForIndependentRemoval(Removable removable) {
if (removeUnusedProperties && removable.isNamedProperty()) {
String propertyName = removable.getPropertyName();
if (referencedPropertyNames.contains(propertyName)
|| codingConvention.isExported(propertyName)) {
// Referenced, so not removable.
removable.applyContinuations();
} else if (removable.isIndependentlyRemovableNamedProperty()) {
// Store for possible removal later.
removablesForPropertyNames.put(removable.getPropertyName(), removable);
} else {
// TODO(bradfordcsmith): Maybe allow removal of non-prototype property assignments if we
// can be sure the variable's value is defined as a literal value that does not escape.
removable.applyContinuations();
// This assignment counts as a reference, since we won't be removing it.
// This is necessary in order to preserve getters and setters for the property.
markPropertyNameReferenced(propertyName);
}
} else {
removable.applyContinuations();
}
}
/**
* Mark any remaining unused parameters as being unused so it can be used elsewhere.
*
* @param paramList list of function's parameters
* @param fparamScope
*/
private void markUnusedParameters(Node paramList, Scope fparamScope) {
for (Node param = paramList.getFirstChild(); param != null; param = param.getNext()) {
if (!param.isUnusedParameter()) {
Node lValue = param;
if (lValue.isDefaultValue()) {
lValue = lValue.getFirstChild();
}
if (lValue.isRest()) {
lValue = lValue.getOnlyChild();
}
if (lValue.isDestructuringPattern()) {
continue;
}
Var var = fparamScope.getVar(lValue.getString());
VarInfo varInfo = getVarInfo(var);
if (varInfo.isRemovable()) {
param.setUnusedParameter(true);
compiler.reportChangeToEnclosingScope(paramList);
}
}
}
}
/**
* Strip as many unreferenced args off the end of the function declaration as possible. We start
* from the end of the function declaration because removing parameters from the middle of the
* param list could mess up the interpretation of parameters being sent over by any function
* calls.
*
* @param argList list of function's arguments
* @param fparamScope
*/
private void maybeRemoveUnusedTrailingParameters(Node argList, Scope fparamScope) {
Node lastArg;
while ((lastArg = argList.getLastChild()) != null) {
Node lValue = lastArg;
if (lastArg.isDefaultValue()) {
lValue = lastArg.getFirstChild();
if (NodeUtil.mayHaveSideEffects(lastArg.getLastChild())) {
break;
}
}
if (lValue.isRest()) {
lValue = lValue.getFirstChild();
}
if (lValue.isDestructuringPattern()) {
if (lValue.hasChildren()) {
// TODO(johnlenz): handle the case where there are no assignments.
break;
} else {
// Remove empty destructuring patterns and their associated object literal assignment
// if it exists and if the right hand side does not have side effects. Note, a
// destructuring pattern with a "leftover" property key as in {a:{}} is not considered
// empty in this case!
NodeUtil.deleteNode(lastArg, compiler);
continue;
}
}
Var var = fparamScope.getVar(lValue.getString());
VarInfo varInfo = getVarInfo(var);
if (varInfo.isRemovable()) {
NodeUtil.deleteNode(lastArg, compiler);
} else {
break;
}
}
}
/**
* Handles a variable reference seen during traversal and returns a {@link VarInfo} object
* appropriate for the given {@link Var}.
*
*
This is a wrapper for {@link #getVarInfo} that handles additional logic needed when we're
* getting the {@link VarInfo} during traversal.
*/
private VarInfo traverseVar(Var var) {
checkNotNull(var);
if (var.isArguments()) {
// If `arguments` is used in a function we must consider all parameters to be referenced.
Scope functionScope = var.getScope().getClosestHoistScope();
Node paramList = NodeUtil.getFunctionParameters(functionScope.getRootNode());
for (Node param = paramList.getFirstChild(); param != null; param = param.getNext()) {
Node lValue = param;
if (lValue.isDefaultValue()) {
lValue = lValue.getFirstChild();
}
if (lValue.isRest()) {
lValue = lValue.getOnlyChild();
}
if (lValue.isDestructuringPattern()) {
continue;
}
Var paramVar = functionScope.getVar(lValue.getString());
getVarInfo(paramVar).markAsReferenced();
}
// `arguments` is never removable.
return canonicalTotallyUnremovableVarInfo;
} else {
return getVarInfo(var);
}
}
/**
* Get the right {@link VarInfo} object to use for the given {@link Var}.
*
*
This method is responsible for managing the entries in {@link #varInfoMap}.
*
Note: Several {@link Var}s may share the same {@link VarInfo} when they should be treated
* the same way.
*/
private VarInfo getVarInfo(Var var) {
checkNotNull(var);
VarInfo varInfo = varInfoMap.get(var);
if (varInfo == null) {
boolean isGlobal = var.isGlobal();
if (isGlobal && !removeGlobals && !removeUnusedProperties) {
varInfo = canonicalTotallyUnremovableVarInfo;
} else if (codingConvention.isExported(var.getName(), !isGlobal)) {
varInfo = canonicalTotallyUnremovableVarInfo;
} else if (var.isArguments()) {
varInfo = canonicalTotallyUnremovableVarInfo;
} else {
varInfo = new VarInfo();
if (isGlobal && !removeGlobals) {
varInfo.setIsExplicitlyNotRemovable();
} else if (var.getParentNode().isParamList()) {
varInfo.propertyAssignmentsWillPreventRemoval = true;
}
varInfoMap.put(var, varInfo);
}
}
return varInfo;
}
/**
* Removes any vars in the scope that were not referenced. Removes any assignments to those
* variables as well.
*/
private void removeUnreferencedVars() {
for (Entryentry : varInfoMap.entrySet()) {
Var var = entry.getKey();
VarInfo varInfo = entry.getValue();
if (!varInfo.isRemovable()) {
continue;
}
// Regardless of what happens to the original declaration,
// we need to remove all assigns, because they may contain references
// to other unreferenced variables.
varInfo.removeAllRemovables();
compiler.addToDebugLog("Unreferenced var: ", var.name);
Node nameNode = var.nameNode;
Node toRemove = nameNode.getParent();
if (toRemove == null || alreadyRemoved(toRemove)) {
// assignedVarInfo.removeAllRemovables () already removed it
} else if (NodeUtil.isFunctionExpression(toRemove)) {
// TODO(bradfordcsmith): Add a Removable for this case.
if (!preserveFunctionExpressionNames) {
Node fnNameNode = toRemove.getFirstChild();
compiler.reportChangeToEnclosingScope(fnNameNode);
fnNameNode.setString("");
}
} else if (toRemove.isParamList()) {
// TODO(bradfordcsmith): handle parameter declarations with removables
// Don't remove function arguments here. That's a special case
// that's taken care of in removeUnreferencedFunctionArgs.
} else {
throw new IllegalStateException("unremoved code: " + toRemove.toStringTree());
}
}
}
/**
* Our progress in a traversal can be expressed completely as the
* current node and scope. The continuation lets us save that
* information so that we can continue the traversal later.
*/
private class Continuation {
private final Node node;
private final Scope scope;
Continuation(Node node, Scope scope) {
this.node = node;
this.scope = scope;
}
void apply() {
if (node.isFunction()) {
// Calling traverseNode here would create infinite recursion for a function declaration
traverseFunction(node, scope);
} else {
traverseNode(node, scope);
}
}
}
/** Represents a portion of the AST that can be removed. */
private abstract class Removable {
private final List continuations;
@Nullable private final String propertyName;
@Nullable private final Node assignedValue;
private final boolean isPrototypeObjectPropertyAssignment;
private boolean continuationsAreApplied = false;
private boolean isRemoved = false;
Removable(RemovableBuilder builder) {
continuations = builder.continuations;
propertyName = builder.propertyName;
assignedValue = builder.assignedValue;
isPrototypeObjectPropertyAssignment = builder.isPrototypeObjectPropertyAssignment;
}
String getPropertyName() {
return checkNotNull(propertyName);
}
/** Remove the associated nodes from the AST. */
abstract void removeInternal(AbstractCompiler compiler);
/** Remove the associated nodes from the AST, unless they've already been removed. */
void remove(AbstractCompiler compiler) {
if (!isRemoved) {
isRemoved = true;
removeInternal(compiler);
}
}
public void applyContinuations() {
if (!continuationsAreApplied) {
continuationsAreApplied = true;
for (Continuation c : continuations) {
// Enqueue the continuation for processing.
// Don't invoke the continuation immediately, because that can lead to concurrent
// modification of data structures.
worklist.add(c);
}
continuations.clear();
}
}
boolean isLiteralValueAssignment() {
// An assigned value of null occurs for a name declaration when no initializer is given.
// It is the same as assigning `undefined`, so it is a literal value.
return assignedValue == null
|| NodeUtil.isLiteralValue(assignedValue, /* includeFunctions */ true);
}
/** True if this object represents assignment to a variable. */
boolean isVariableAssignment() {
return false;
}
/** True if this object represents assignment of a value to a property. */
boolean isPropertyAssignment() {
return false;
}
/** True if this object represents a named property, either assignment or declaration. */
boolean isNamedProperty() {
return propertyName != null;
}
/**
* True if this object represents assignment to a named property.
*
* This does not include class or object literal member declarations.
*/
boolean isNamedPropertyAssignment() {
return false;
}
/**
* True if this object has an assigned value that may escape to another context through aliasing
* or some other means.
*/
boolean assignedValueMayEscape() {
return false;
}
/** Is this a direct assignment to `varName.prototype`? */
boolean isPrototypeAssignment() {
return isNamedPropertyAssignment() && propertyName.equals("prototype");
}
/** Is this an assignment to a property on a prototype object? */
boolean isPrototypeObjectNamedPropertyAssignment() {
return isPrototypeObjectPropertyAssignment && isNamedPropertyAssignment();
}
boolean isMethodDeclaration() {
return false;
}
boolean isIndependentlyRemovableNamedProperty() {
return isPrototypeObjectNamedPropertyAssignment() || isMethodDeclaration();
}
}
private class RemovableBuilder {
final List continuations = new ArrayList<>();
@Nullable String propertyName = null;
@Nullable public Node assignedValue = null;
boolean isPrototypeObjectPropertyAssignment = false;
RemovableBuilder addContinuation(Continuation continuation) {
continuations.add(continuation);
return this;
}
RemovableBuilder setAssignedValue(@Nullable Node assignedValue) {
this.assignedValue = assignedValue;
return this;
}
RemovableBuilder setIsPrototypeObjectPropertyAssignment(
boolean isPrototypeObjectPropertyAssignment) {
this.isPrototypeObjectPropertyAssignment = isPrototypeObjectPropertyAssignment;
return this;
}
DestructuringAssign buildDestructuringAssign(Node removableNode, Node nameNode) {
return new DestructuringAssign(this, removableNode, nameNode);
}
ClassDeclaration buildClassDeclaration(Node classNode) {
return new ClassDeclaration(this, classNode);
}
NamedClassExpression buildNamedClassExpression(Node classNode) {
return new NamedClassExpression(this, classNode);
}
MethodDefinition buildMethodDefinition(Node methodNode) {
checkArgument(methodNode.isMemberFunctionDef() || NodeUtil.isGetOrSetKey(methodNode));
this.propertyName = methodNode.getString();
return new MethodDefinition(this, methodNode);
}
FunctionDeclaration buildFunctionDeclaration(Node functionNode) {
return new FunctionDeclaration(this, functionNode);
}
NameDeclarationStatement buildNameDeclarationStatement(Node declarationStatement) {
return new NameDeclarationStatement(this, declarationStatement);
}
Assign buildNamedPropertyAssign(Node assignNode, Node nameNode, Node propertyNode) {
this.propertyName = propertyNode.getString();
checkNotNull(assignedValue);
return new Assign(this, assignNode, nameNode, Kind.NAMED_PROPERTY, propertyNode);
}
Assign buildComputedPropertyAssign(Node assignNode, Node nameNode, Node propertyNode) {
checkNotNull(assignedValue);
return new Assign(this, assignNode, nameNode, Kind.COMPUTED_PROPERTY, propertyNode);
}
Assign buildVariableAssign(Node assignNode, Node nameNode) {
return new Assign(this, assignNode, nameNode, Kind.VARIABLE, /* propertyNode */ null);
}
ClassSetupCall buildClassSetupCall(Node callNode) {
return new ClassSetupCall(this, callNode);
}
VanillaForNameDeclaration buildVanillaForNameDeclaration(Node nameNode) {
return new VanillaForNameDeclaration(this, nameNode);
}
}
private class DestructuringAssign extends Removable {
final Node removableNode;
final Node nameNode;
DestructuringAssign(RemovableBuilder builder, Node removableNode, Node nameNode) {
super(builder);
checkState(nameNode.isName());
this.removableNode = removableNode;
this.nameNode = nameNode;
Node parent = nameNode.getParent();
if (parent.isDefaultValue()) {
checkState(!NodeUtil.mayHaveSideEffects(parent.getLastChild()));
}
}
@Override
boolean isVariableAssignment() {
// TODO(bradfordcsmith): Handle destructuring assignments to properties.
return true;
}
@Override
boolean isLiteralValueAssignment() {
// TODO(bradfordcsmith): Determine assigned value when possible.
// We don't look at the rhs of destructuring assignments at all right now,
// so assume they always assign some non-literal value.
return false;
}
@Override
public void removeInternal(AbstractCompiler compiler) {
if (alreadyRemoved(removableNode)) {
return;
}
Node removableParent = removableNode.getParent();
if (removableParent.isArrayPattern()) {
// [a, removableName, b] = something;
// [a, ...removableName] = something;
// [a, removableName = removableValue, b] = something;
// [a, ...removableName = removableValue] = something;
compiler.reportChangeToEnclosingScope(removableParent);
if (removableNode == removableParent.getLastChild()) {
removableNode.detach();
} else {
removableNode.replaceWith(IR.empty().srcref(removableNode));
}
// We prefer `[a, b]` to `[a, b, , , , ]`
// So remove any trailing empty nodes.
for (Node maybeEmpty = removableParent.getLastChild();
maybeEmpty != null && maybeEmpty.isEmpty();
maybeEmpty = removableParent.getLastChild()) {
maybeEmpty.detach();
}
NodeUtil.markFunctionsDeleted(removableNode, compiler);
} else if (removableParent.isParamList() && removableNode.isDefaultValue()) {
// function(removableName = removableValue)
compiler.reportChangeToEnclosingScope(removableNode);
// preserve the slot in the parameter list
Node name = removableNode.getFirstChild();
checkState(name.isName());
if (removableNode == removableParent.getLastChild()
&& removeGlobals
&& canRemoveParameters(removableParent)) {
// function(p1, removableName = removableDefault)
// and we're allowed to remove the parameter entirely
removableNode.detach();
} else {
// function(removableName = removableDefault, otherParam)
// or removableName is at the end, but cannot be completely removed.
removableNode.replaceWith(name.detach());
}
NodeUtil.markFunctionsDeleted(removableNode, compiler);
} else if (removableNode.isDefaultValue()) {
// { a: removableName = removableValue }
// { [removableExpression]: removableName = removableValue }
checkState(
removableParent.isStringKey()
|| (removableParent.isComputedProp()
&& !NodeUtil.mayHaveSideEffects(removableParent.getFirstChild())));
// Remove the whole property, not just its default value part.
NodeUtil.deleteNode(removableParent, compiler);
} else {
// { removableStringKey: removableName }
// function(...removableName) {}
// function(...removableName = default)
checkState(
removableParent.isObjectPattern()
|| (removableParent.isParamList() && removableNode.isRest()));
NodeUtil.deleteNode(removableNode, compiler);
}
}
}
private class ClassDeclaration extends Removable {
final Node classDeclarationNode;
ClassDeclaration(RemovableBuilder builder, Node classDeclarationNode) {
super(builder);
this.classDeclarationNode = classDeclarationNode;
}
@Override
public void removeInternal(AbstractCompiler compiler) {
NodeUtil.deleteNode(classDeclarationNode, compiler);
}
}
private class NamedClassExpression extends Removable {
final Node classNode;
NamedClassExpression(RemovableBuilder builder, Node classNode) {
super(builder);
this.classNode = classNode;
}
@Override
public void removeInternal(AbstractCompiler compiler) {
if (!alreadyRemoved(classNode)) {
Node nameNode = classNode.getFirstChild();
if (!nameNode.isEmpty()) {
// Just empty the class's name. If the expression is assigned to an unused variable,
// then the whole class might still be removed as part of that assignment.
classNode.replaceChild(nameNode, IR.empty().useSourceInfoFrom(nameNode));
compiler.reportChangeToEnclosingScope(classNode);
}
}
}
}
private class MethodDefinition extends Removable {
final Node methodNode;
MethodDefinition(RemovableBuilder builder, Node methodNode) {
super(builder);
this.methodNode = methodNode;
}
@Override
boolean isMethodDeclaration() {
return true;
}
@Override
void removeInternal(AbstractCompiler compiler) {
NodeUtil.deleteNode(methodNode, compiler);
}
}
private class FunctionDeclaration extends Removable {
final Node functionDeclarationNode;
FunctionDeclaration(RemovableBuilder builder, Node functionDeclarationNode) {
super(builder);
this.functionDeclarationNode = functionDeclarationNode;
}
@Override
public void removeInternal(AbstractCompiler compiler) {
NodeUtil.deleteNode(functionDeclarationNode, compiler);
}
}
private class NameDeclarationStatement extends Removable {
private final Node declarationStatement;
public NameDeclarationStatement(RemovableBuilder builder, Node declarationStatement) {
super(builder);
this.declarationStatement = declarationStatement;
}
@Override
void removeInternal(AbstractCompiler compiler) {
Node nameNode = declarationStatement.getOnlyChild();
Node valueNode = nameNode.getFirstChild();
if (valueNode != null && NodeUtil.mayHaveSideEffects(valueNode)) {
compiler.reportChangeToEnclosingScope(declarationStatement);
valueNode.detach();
declarationStatement.replaceWith(IR.exprResult(valueNode).useSourceInfoFrom(valueNode));
} else {
NodeUtil.deleteNode(declarationStatement, compiler);
}
}
@Override
boolean isVariableAssignment() {
return true;
}
}
enum Kind {
// X = something;
VARIABLE,
// X.propertyName = something;
// X.prototype.propertyName = something;
NAMED_PROPERTY,
// X[expression] = something;
// X.prototype[expression] = something;
COMPUTED_PROPERTY;
}
private class Assign extends Removable {
final Node assignNode;
final Node nameNode;
final Kind kind;
@Nullable final Node propertyNode;
// If true, the value may have escaped and any modification is a use.
final boolean maybeAliased;
Assign(
RemovableBuilder builder,
Node assignNode,
Node nameNode,
Kind kind,
@Nullable Node propertyNode) {
super(builder);
checkState(NodeUtil.isAssignmentOp(assignNode));
if (kind == Kind.VARIABLE) {
checkArgument(
propertyNode == null,
"got property node for simple variable assignment: %s",
propertyNode);
} else {
checkArgument(propertyNode != null, "missing property node");
if (kind == Kind.NAMED_PROPERTY) {
checkArgument(propertyNode.isString(), "property name is not a string: %s", propertyNode);
}
}
this.assignNode = assignNode;
this.nameNode = nameNode;
this.kind = kind;
this.propertyNode = propertyNode;
this.maybeAliased = NodeUtil.isExpressionResultUsed(assignNode);
}
@Override
boolean assignedValueMayEscape() {
return maybeAliased;
}
/** True for `varName = value` assignments. */
@Override
boolean isVariableAssignment() {
return kind == Kind.VARIABLE;
}
@Override
boolean isPropertyAssignment() {
return isNamedPropertyAssignment() || isComputedPropertyAssignment();
}
/** True for `varName.propName = value` and `varName.prototype.propName = value` assignments. */
@Override
boolean isNamedPropertyAssignment() {
return kind == Kind.NAMED_PROPERTY;
}
/** True for `varName[expr] = value` and `varName.prototype[expr] = value` assignments. */
boolean isComputedPropertyAssignment() {
return kind == Kind.COMPUTED_PROPERTY;
}
/** Replace the current assign with its right hand side. */
@Override
public void removeInternal(AbstractCompiler compiler) {
if (alreadyRemoved(assignNode)) {
return;
}
Node parent = assignNode.getParent();
compiler.reportChangeToEnclosingScope(parent);
Node lhs = assignNode.getFirstChild();
Node rhs = assignNode.getSecondChild();
boolean mustPreserveRhs =
NodeUtil.mayHaveSideEffects(rhs) || NodeUtil.isExpressionResultUsed(assignNode);
boolean mustPreserveGetElmExpr =
lhs.isGetElem() && NodeUtil.mayHaveSideEffects(lhs.getLastChild());
if (mustPreserveRhs && mustPreserveGetElmExpr) {
Node replacement =
IR.comma(lhs.getLastChild().detach(), rhs.detach()).useSourceInfoFrom(assignNode);
assignNode.replaceWith(replacement);
} else if (mustPreserveGetElmExpr) {
assignNode.replaceWith(lhs.getLastChild().detach());
NodeUtil.markFunctionsDeleted(rhs, compiler);
} else if (mustPreserveRhs) {
assignNode.replaceWith(rhs.detach());
NodeUtil.markFunctionsDeleted(lhs, compiler);
} else if (parent.isExprResult()) {
parent.detach();
NodeUtil.markFunctionsDeleted(parent, compiler);
} else {
// value isn't needed, but we need to keep the AST valid.
assignNode.replaceWith(IR.number(0).useSourceInfoFrom(assignNode));
NodeUtil.markFunctionsDeleted(assignNode, compiler);
}
}
}
/**
* Represents a call to a class setup method such as `goog.inherits()` or
* `goog.addSingletonGetter()`.
*/
private class ClassSetupCall extends Removable {
final Node callNode;
ClassSetupCall(RemovableBuilder builder, Node callNode) {
super(builder);
this.callNode = callNode;
}
@Override
public void removeInternal(AbstractCompiler compiler) {
Node parent = callNode.getParent();
// NOTE: The call must either be its own statement or the LHS of a comma expression,
// because it doesn't have a meaningful return value.
if (parent.isExprResult()) {
NodeUtil.deleteNode(parent, compiler);
} else {
// `(goog.inherits(A, B), something)` -> `something`
checkState(parent.isComma());
Node rhs = checkNotNull(callNode.getNext());
compiler.reportChangeToEnclosingScope(parent);
parent.replaceWith(rhs.detach());
}
}
}
private static boolean alreadyRemoved(Node n) {
Node parent = n.getParent();
if (parent == null) {
return true;
}
if (parent.isRoot()) {
return false;
}
return alreadyRemoved(parent);
}
private class VarInfo {
/**
* Objects that represent variable declarations, assignments, or class setup calls that can
* be removed.
*
* NOTE: Once we realize that we cannot remove the variable, this list will be cleared and
* no more will be added.
*/
final List removables = new ArrayList<>();
boolean isEntirelyRemovable = true;
/**
* Used along with hasPropertyAssignments to handle cases where property assignment may have
* an unknown side-effect, and so make it unsafe to remove the variable.
*
* If we're unsure where the variable's value comes from, then setting a property on it may
* have a side-effect we cannot easily detect.
*/
boolean propertyAssignmentsWillPreventRemoval = false;
/**
* Records whether any properties are set on the variable.
*
* This includes both named properties (`x.propName =`) and computed ones (`x[expr] = `).
* It is used in combination with propertyAssignmentsWillPreventRemoval.
*/
boolean hasPropertyAssignments = false;
void addRemovable(Removable removable) {
// determine how this removable affects removability
if (removable.isPropertyAssignment()) {
hasPropertyAssignments = true;
if (removable.isPrototypeAssignment() && removable.assignedValueMayEscape()) {
// Assignment to properties could have unexpected side-effects.
// x = varName.prototype = {};
// foo(varName.prototype = {});
// NOTE: Arguably we should also check for literal value assignment, but that would
// prevent us from removing cases like this one.
// Foo.prototype = {
// constructor: Foo, // not considered a literal value.
// ...
// };
propertyAssignmentsWillPreventRemoval = true;
}
if (propertyAssignmentsWillPreventRemoval) {
setIsExplicitlyNotRemovable();
}
} else if (removable.isVariableAssignment()
&& (removable.assignedValueMayEscape() || !removable.isLiteralValueAssignment())) {
// Assignment to properties could have unexpected side-effects.
// x = varName = {};
// foo(varName = {});
// varName = foo();
propertyAssignmentsWillPreventRemoval = true;
if (hasPropertyAssignments) {
setIsExplicitlyNotRemovable();
}
}
if (isEntirelyRemovable) {
// Store for possible removal later.
removables.add(removable);
} else {
considerForIndependentRemoval(removable);
}
}
/**
* Marks this variable as referenced and evaluates any continuations if not previously marked as
* referenced.
*
* @return true if the variable was not already marked as referenced
*/
boolean markAsReferenced() {
return setIsExplicitlyNotRemovable();
}
boolean isRemovable() {
return isEntirelyRemovable;
}
boolean setIsExplicitlyNotRemovable() {
if (isEntirelyRemovable) {
isEntirelyRemovable = false;
for (Removable r : removables) {
considerForIndependentRemoval(r);
}
removables.clear();
return true;
} else {
return false;
}
}
void removeAllRemovables() {
checkState(isEntirelyRemovable);
for (Removable removable : removables) {
removable.remove(compiler);
}
removables.clear();
}
}
/**
* Represents declarations in the standard for-loop initialization.
*
* e.g. the `let i = 0` part of `for (let i = 0; i < 10; ++i) {...}`.
* These must be handled differently from declaration statements because:
*
*
* -
* For-loop declarations may declare more than one variable.
* The normalization doesn't break them up as it does for declaration statements.
*
* -
* Removal must be handled differently.
*
* -
* We don't currently preserve initializers with side effects here.
* Instead, we just consider such cases non-removable.
*
*
*/
private class VanillaForNameDeclaration extends Removable {
private final Node nameNode;
private VanillaForNameDeclaration(RemovableBuilder builder, Node nameNode) {
super(builder);
this.nameNode = nameNode;
}
@Override
void removeInternal(AbstractCompiler compiler) {
Node declaration = checkNotNull(nameNode.getParent());
compiler.reportChangeToEnclosingScope(declaration);
// NOTE: We don't need to preserve the initializer value, because we currently do not remove
// for-loop vars whose initializing values have side effects.
if (nameNode.getPrevious() == null && nameNode.getNext() == null) {
// only child, so we can remove the whole declaration
declaration.replaceWith(IR.empty().useSourceInfoFrom(declaration));
} else {
declaration.removeChild(nameNode);
}
NodeUtil.markFunctionsDeleted(nameNode, compiler);
}
}
}