Skip to content

Commit

Permalink
Implement more prototype property removal features in RemoveUnusedVars.
Browse files Browse the repository at this point in the history
1. Remove class methods, getters and setters.
2. Avoid removing properties that are exported by naming convention.
3. Enable passing tests in RemoveUnusedVarsPrototypePropertiesTest.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=177653714
  • Loading branch information
brad4d authored and blickly committed Dec 4, 2017
1 parent ff836ef commit fd77355
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 101 deletions.
188 changes: 147 additions & 41 deletions src/com/google/javascript/jscomp/RemoveUnusedVars.java
Expand Up @@ -90,7 +90,7 @@ class RemoveUnusedVars implements CompilerPass {


// Properties that are implicitly used as part of the JS language. // Properties that are implicitly used as part of the JS language.
private static final ImmutableSet<String> IMPLICITLY_USED_PROPERTIES = private static final ImmutableSet<String> IMPLICITLY_USED_PROPERTIES =
ImmutableSet.of("length", "toString", "valueOf"); ImmutableSet.of("length", "toString", "valueOf", "constructor");


private final AbstractCompiler compiler; private final AbstractCompiler compiler;


Expand Down Expand Up @@ -211,8 +211,7 @@ private void traverseNode(Node n, Scope scope) {
// If this function is a removable var, then create a continuation // If this function is a removable var, then create a continuation
// for it instead of traversing immediately. // for it instead of traversing immediately.
if (NodeUtil.isFunctionDeclaration(n)) { if (NodeUtil.isFunctionDeclaration(n)) {
varInfo = varInfo = traverseVar(scope.getVar(n.getFirstChild().getString()));
traverseVar(scope.getVar(n.getFirstChild().getString()));
FunctionDeclaration functionDeclaration = FunctionDeclaration functionDeclaration =
new RemovableBuilder() new RemovableBuilder()
.addContinuation(new Continuation(n, scope)) .addContinuation(new Continuation(n, scope))
Expand Down Expand Up @@ -251,6 +250,10 @@ private void traverseNode(Node n, Scope scope) {
traverseClass(n, scope); traverseClass(n, scope);
break; break;


case CLASS_MEMBERS:
traverseClassMembers(n, scope);
break;

case DEFAULT_VALUE: case DEFAULT_VALUE:
traverseDefaultValue(n, scope); traverseDefaultValue(n, scope);
break; break;
Expand Down Expand Up @@ -501,7 +504,7 @@ private void traverseDeclarationStatement(Node declarationStatement, Scope scope
// Normalization should ensure that declaration statements always have just one child. // Normalization should ensure that declaration statements always have just one child.
Node nameNode = declarationStatement.getOnlyChild(); Node nameNode = declarationStatement.getOnlyChild();
if (!nameNode.isName()) { if (!nameNode.isName()) {
// TODO(bradfordcsmith): Customize handling of destructuring // Destructuring declarations are handled elsewhere.
traverseNode(nameNode, scope); traverseNode(nameNode, scope);
} else { } else {
Node valueNode = nameNode.getFirstChild(); Node valueNode = nameNode.getFirstChild();
Expand Down Expand Up @@ -727,33 +730,87 @@ private void traverseChildren(Node n, Scope scope) {
} }
} }


/**
* Handle a class that is not the RHS child of an assignment or a variable declaration
* initializer.
*
* <p>For
* @param classNode
* @param scope
*/
private void traverseClass(Node classNode, Scope 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()); checkArgument(classNode.isClass());
Node classNameNode = classNode.getFirstChild(); Node classNameNode = classNode.getFirstChild();
Node baseClassExpression = classNameNode.getNext(); Node baseClassExpression = classNameNode.getNext();
Node classBodyNode = baseClassExpression.getNext(); Node classBodyNode = baseClassExpression.getNext();
Scope classScope = scopeCreator.createScope(classNode, scope); Scope classScope = scopeCreator.createScope(classNode, scope);
if (!NodeUtil.isNamedClass(classNode) || classNode.getParent().isExport()) {
// If this isn't a named class, there's no var to consider here. VarInfo varInfo = traverseVar(scope.getVar(classNameNode.getString()));
// If it is exported, it definitely cannot be removed. if (classNode.getParent().isExport()) {
traverseNode(baseClassExpression, classScope); // Cannot remove an exported class.
traverseNode(classBodyNode, classScope); 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)) { } else if (NodeUtil.mayHaveSideEffects(baseClassExpression)) {
// TODO(bradfordcsmith): implement removal without losing side-effects for this case // TODO(bradfordcsmith): implement removal without losing side-effects for this case
traverseNode(baseClassExpression, classScope); varInfo.setIsExplicitlyNotRemovable();
traverseNode(classBodyNode, classScope); traverseNode(baseClassExpression, scope);
traverseClassMembers(classBodyNode, classScope);
} else { } else {
RemovableBuilder builder = RemovableBuilder builder =
new RemovableBuilder() new RemovableBuilder()
.addContinuation(new Continuation(baseClassExpression, classScope)) .addContinuation(new Continuation(baseClassExpression, classScope))
.addContinuation(new Continuation(classBodyNode, classScope)); .addContinuation(new Continuation(classBodyNode, classScope));
VarInfo varInfo = varInfo.addRemovable(builder.buildClassDeclaration(classNode));
traverseVar(classScope.getVar(classNameNode.getString())); }
if (NodeUtil.isClassDeclaration(classNode)) { }
varInfo.addRemovable(builder.buildClassDeclaration(classNode));
} else { private void traverseClassExpression(Node classNode, Scope scope) {
varInfo.addRemovable(builder.buildNamedClassExpression(classNode)); 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);
} }
} }


Expand Down Expand Up @@ -848,6 +905,30 @@ private void markPropertyNameReferenced(String propertyName) {
} }
} }


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. * Mark any remaining unused parameters as being unused so it can be used elsewhere.
* *
Expand Down Expand Up @@ -1012,7 +1093,7 @@ private void removeUnreferencedVars() {
Node nameNode = var.nameNode; Node nameNode = var.nameNode;
Node toRemove = nameNode.getParent(); Node toRemove = nameNode.getParent();
if (toRemove == null || alreadyRemoved(toRemove)) { if (toRemove == null || alreadyRemoved(toRemove)) {
// varInfo.removeAllRemovables () already removed it // assignedVarInfo.removeAllRemovables () already removed it
} else if (NodeUtil.isFunctionExpression(toRemove)) { } else if (NodeUtil.isFunctionExpression(toRemove)) {
// TODO(bradfordcsmith): Add a Removable for this case. // TODO(bradfordcsmith): Add a Removable for this case.
if (!preserveFunctionExpressionNames) { if (!preserveFunctionExpressionNames) {
Expand All @@ -1025,7 +1106,7 @@ private void removeUnreferencedVars() {
// Don't remove function arguments here. That's a special case // Don't remove function arguments here. That's a special case
// that's taken care of in removeUnreferencedFunctionArgs. // that's taken care of in removeUnreferencedFunctionArgs.
} else { } else {
throw new IllegalStateException("unremoved code"); throw new IllegalStateException("unremoved code: " + toRemove.toStringTree());
} }
} }
} }
Expand Down Expand Up @@ -1073,7 +1154,6 @@ private abstract class Removable {
} }


String getPropertyName() { String getPropertyName() {
checkState(isNamedPropertyAssignment());
return checkNotNull(propertyName); return checkNotNull(propertyName);
} }


Expand Down Expand Up @@ -1118,11 +1198,20 @@ boolean isPropertyAssignment() {
return false; return false;
} }


/** True if this object represents assignment to a named property. */ /** True if this object represents a named property, either assignment or declaration. */
boolean isNamedPropertyAssignment() { boolean isNamedProperty() {
return propertyName != null; return propertyName != null;
} }


/**
* True if this object represents assignment to a named property.
*
* <p>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 * True if this object has an assigned value that may escape to another context through aliasing
* or some other means. * or some other means.
Expand All @@ -1140,6 +1229,14 @@ boolean isPrototypeAssignment() {
boolean isPrototypeObjectNamedPropertyAssignment() { boolean isPrototypeObjectNamedPropertyAssignment() {
return isPrototypeObjectPropertyAssignment && isNamedPropertyAssignment(); return isPrototypeObjectPropertyAssignment && isNamedPropertyAssignment();
} }

boolean isMethodDeclaration() {
return false;
}

boolean isIndependentlyRemovableNamedProperty() {
return isPrototypeObjectNamedPropertyAssignment() || isMethodDeclaration();
}
} }


private class RemovableBuilder { private class RemovableBuilder {
Expand Down Expand Up @@ -1177,6 +1274,12 @@ NamedClassExpression buildNamedClassExpression(Node classNode) {
return new NamedClassExpression(this, 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) { FunctionDeclaration buildFunctionDeclaration(Node functionNode) {
return new FunctionDeclaration(this, functionNode); return new FunctionDeclaration(this, functionNode);
} }
Expand Down Expand Up @@ -1339,6 +1442,25 @@ public void removeInternal(AbstractCompiler compiler) {
} }
} }


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 { private class FunctionDeclaration extends Removable {
final Node functionDeclarationNode; final Node functionDeclarationNode;


Expand Down Expand Up @@ -1595,16 +1717,10 @@ void addRemovable(Removable removable) {
} }


if (isEntirelyRemovable) { if (isEntirelyRemovable) {
// Store for possible removal later.
removables.add(removable); removables.add(removable);
} else if (removeUnusedProperties
&& removable.isPrototypeObjectNamedPropertyAssignment()
&& !referencedPropertyNames.contains(removable.getPropertyName())) {
// prototype properties may be removed even if the variable itself isn't.
// TODO(bradfordcsmith): maybe allow removal of non-prototype property assignments when we
// can be sure the variable's value is defined as a literal value that does not escape.
removablesForPropertyNames.put(removable.getPropertyName(), removable);
} else { } else {
removable.applyContinuations(); considerForIndependentRemoval(removable);
} }
} }


Expand All @@ -1626,17 +1742,7 @@ boolean setIsExplicitlyNotRemovable() {
if (isEntirelyRemovable) { if (isEntirelyRemovable) {
isEntirelyRemovable = false; isEntirelyRemovable = false;
for (Removable r : removables) { for (Removable r : removables) {
if (removeUnusedProperties considerForIndependentRemoval(r);
&& r.isPrototypeObjectNamedPropertyAssignment()
&& !referencedPropertyNames.contains(r.getPropertyName())) {
// we may yet remove individual prototype properties
// TODO(bradfordcsmith): maybe allow removal of non-prototype property assignments when
// we can be sure the variable's value is defined as a literal value that does not
// escape.
removablesForPropertyNames.put(r.getPropertyName(), r);
} else {
r.applyContinuations();
}
} }
removables.clear(); removables.clear();
return true; return true;
Expand Down

0 comments on commit fd77355

Please sign in to comment.