Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.google.errorprone.bugpatterns.ModifiedButNotUsed Maven / Gradle / Ivy
/*
* Copyright 2018 The Error Prone 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.errorprone.bugpatterns;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.collect.Streams.concat;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.kindIs;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import static com.google.errorprone.matchers.method.MethodMatchers.constructor;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static com.google.errorprone.predicates.TypePredicates.isDescendantOf;
import static com.google.errorprone.predicates.TypePredicates.isDescendantOfAny;
import static com.google.errorprone.util.ASTHelpers.getReceiver;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.isConsideredFinal;
import static com.google.errorprone.util.ASTHelpers.streamReceivers;
import static java.util.stream.Collectors.joining;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.ExpressionStatementTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.IsSubtypeOf;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.predicates.type.DescendantOf;
import com.google.errorprone.predicates.type.DescendantOfAny;
import com.google.errorprone.suppliers.Suppliers;
import com.google.errorprone.util.ASTHelpers;
import com.google.errorprone.util.SideEffectAnalysis;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Stream;
/**
* Matches creation of new collections/proto builders which are modified but never used.
*
* @author [email protected] (Graeme Morgan)
*/
@BugPattern(
name = "ModifiedButNotUsed",
summary = "A collection or proto builder was created, but its values were never accessed.",
severity = WARNING)
public class ModifiedButNotUsed extends BugChecker
implements ExpressionStatementTreeMatcher, VariableTreeMatcher {
private static final ImmutableSet GUAVA_IMMUTABLES =
ImmutableSet.of(
"com.google.common.collect.ImmutableCollection",
"com.google.common.collect.ImmutableMap",
"com.google.common.collect.ImmutableMultimap");
private static final ImmutableSet COLLECTIONS =
concat(
GUAVA_IMMUTABLES.stream().map(i -> i + ".Builder"),
Stream.of(
"java.util.Collection", "java.util.Map", "com.google.common.collect.Multimap"))
.collect(toImmutableSet());
private static final Matcher COLLECTION_SETTER =
instanceMethod()
.onDescendantOfAny(COLLECTIONS)
.namedAnyOf(
"add",
"addAll",
"clear",
"put",
"putAll",
"remove",
"removeAll",
"removeIf",
"replaceAll",
"retainAll",
"set",
"sort");
private static final String MESSAGE = "com.google.protobuf.MessageLite";
private static final String MESSAGE_BUILDER = MESSAGE + ".Builder";
private static final Matcher FLUENT_SETTER =
anyOf(
instanceMethod()
.onDescendantOf(MESSAGE_BUILDER)
.withNameMatching(Pattern.compile("(add|clear|remove|set).+")),
instanceMethod()
.onDescendantOfAny(
GUAVA_IMMUTABLES.stream().map(c -> c + ".Builder").collect(toImmutableSet()))
.namedAnyOf("add", "addAll", "put", "putAll"));
private static final Matcher FLUENT_CHAIN =
anyOf(
FLUENT_SETTER,
instanceMethod()
.onDescendantOf(MESSAGE_BUILDER)
.withNameMatching(Pattern.compile("get.+")));
private static final Matcher COLLECTION_TYPE =
anyOf(COLLECTIONS.stream().map(IsSubtypeOf::new).collect(toImmutableList()));
private static final Matcher PROTO_TYPE = new IsSubtypeOf<>(MESSAGE_BUILDER);
private static final Matcher FLUENT_CONSTRUCTOR =
anyOf(
allOf(
kindIs(Kind.NEW_CLASS),
constructor()
.forClass(
new DescendantOfAny(
GUAVA_IMMUTABLES.stream()
.map(i -> Suppliers.typeFromString(i + ".Builder"))
.collect(toImmutableList())))),
staticMethod()
.onClass(isDescendantOfAny(GUAVA_IMMUTABLES))
.namedAnyOf("builder", "builderWithExpectedSize"),
allOf(
kindIs(Kind.NEW_CLASS),
constructor().forClass(new DescendantOf(Suppliers.typeFromString(MESSAGE_BUILDER)))),
staticMethod().onClass(isDescendantOf(MESSAGE)).named("newBuilder"),
instanceMethod().onDescendantOf(MESSAGE).namedAnyOf("toBuilder", "newBuilderForType"));
private static final Matcher NEW_COLLECTION =
anyOf(
constructor()
.forClass(
new DescendantOfAny(
COLLECTIONS.stream()
.map(Suppliers::typeFromString)
.collect(toImmutableList()))),
staticMethod()
.onClassAny(
"com.google.common.collect.Lists",
"com.google.common.collect.Maps",
"com.google.common.collect.Sets")
.withNameMatching(Pattern.compile("new.+")));
private static final Matcher BUILD_CALL =
anyOf(
instanceMethod()
.onDescendantOf("com.google.protobuf.MessageLite.Builder")
.namedAnyOf("build", "buildPartial"),
instanceMethod()
.onDescendantOfAny(
GUAVA_IMMUTABLES.stream().map(c -> c + ".Builder").collect(toImmutableList()))
.named("build"));
@Override
public Description matchVariable(VariableTree tree, VisitorState state) {
VarSymbol symbol = getSymbol(tree);
if (!isConsideredFinal(symbol)) {
return NO_MATCH;
}
if (state.getPath().getParentPath().getLeaf() instanceof ClassTree) {
return NO_MATCH;
}
if (!COLLECTION_TYPE.matches(tree, state) && !PROTO_TYPE.matches(tree, state)) {
return NO_MATCH;
}
List initializers = new ArrayList<>();
if (tree.getInitializer() == null) {
new TreePathScanner() {
@Override
public Void visitAssignment(AssignmentTree node, Void unused) {
if (symbol.equals(getSymbol(node.getVariable()))) {
initializers.add(new TreePath(getCurrentPath(), node.getExpression()));
}
return super.visitAssignment(node, unused);
}
}.scan(state.getPath().getParentPath(), null);
} else {
initializers.add(new TreePath(state.getPath(), tree.getInitializer()));
}
if (initializers.size() != 1) {
return NO_MATCH;
}
TreePath initializerPath = getOnlyElement(initializers);
ExpressionTree initializer = (ExpressionTree) initializerPath.getLeaf();
if (!NEW_COLLECTION.matches(initializer, state) && !newFluentChain(initializer, state)) {
return NO_MATCH;
}
UnusedScanner isUnusedScanner = new UnusedScanner(symbol, state, getMatcher(tree, state));
isUnusedScanner.scan(state.getPath().getParentPath(), null);
if (isUnusedScanner.isUsed) {
return NO_MATCH;
}
ImmutableList removals =
tree.getInitializer() == null
? ImmutableList.of(state.getPath(), initializerPath)
: ImmutableList.of(initializerPath);
return buildDescription(initializer).addAllFixes(isUnusedScanner.buildFixes(removals)).build();
}
private static Matcher getMatcher(Tree tree, VisitorState state) {
return COLLECTION_TYPE.matches(tree, state)
? (t, s) -> collectionUsed(s)
: (t, s) -> fluentBuilderUsed(s);
}
private static boolean collectionUsed(VisitorState state) {
TreePath path = state.getPath();
return !(path.getParentPath().getLeaf() instanceof MemberSelectTree)
|| !(path.getParentPath().getParentPath().getLeaf() instanceof MethodInvocationTree)
|| !COLLECTION_SETTER.matches(
(MethodInvocationTree) path.getParentPath().getParentPath().getLeaf(), state)
|| ASTHelpers.targetType(state.withPath(path.getParentPath().getParentPath())) != null;
}
private static boolean fluentBuilderUsed(VisitorState state) {
for (TreePath path = state.getPath();
path != null;
path = path.getParentPath().getParentPath()) {
if (path.getParentPath().getLeaf() instanceof ExpressionStatementTree) {
return false;
}
if (!(path.getParentPath().getLeaf() instanceof MemberSelectTree
&& path.getParentPath().getParentPath().getLeaf() instanceof MethodInvocationTree
&& FLUENT_CHAIN.matches(
(MethodInvocationTree) path.getParentPath().getParentPath().getLeaf(), state))) {
return true;
}
}
return true;
}
@Override
public Description matchExpressionStatement(ExpressionStatementTree tree, VisitorState state) {
ExpressionTree expression = tree.getExpression();
if (BUILD_CALL.matches(expression, state)) {
expression = getReceiver(expression);
}
if (expression == null) {
return NO_MATCH;
}
// Make sure we actually set something.
if (!FLUENT_SETTER.matches(expression, state)) {
return NO_MATCH;
}
if (!newFluentChain(expression, state)) {
return NO_MATCH;
}
return buildDescription(tree)
.setMessage("Modifying a Builder without assigning it to anything does nothing.")
.build();
}
/**
* Whether this is a chain of method invocations terminating in a new proto or collection builder.
*/
private static boolean newFluentChain(ExpressionTree tree, VisitorState state) {
return concat(Stream.of(tree), streamReceivers(tree))
.filter(t -> !FLUENT_CHAIN.matches(t, state))
.findFirst()
.map(t -> FLUENT_CONSTRUCTOR.matches(t, state))
.orElse(false);
}
private static class UnusedScanner extends TreePathScanner {
private final Symbol symbol;
private final VisitorState state;
private final Matcher matcher;
private final List usageSites = new ArrayList<>();
private boolean isUsed = false;
private UnusedScanner(Symbol symbol, VisitorState state, Matcher matcher) {
this.symbol = symbol;
this.state = state;
this.matcher = matcher;
}
@Override
public Void visitIdentifier(IdentifierTree identifierTree, Void unused) {
if (!Objects.equals(getSymbol(identifierTree), symbol)) {
return null;
}
if (matcher.matches(identifierTree, state.withPath(getCurrentPath()))) {
isUsed = true;
return null;
}
usageSites.add(getCurrentPath());
return null;
}
@Override
public Void visitVariable(VariableTree variableTree, Void unused) {
// Don't count the declaration of the variable as a usage.
if (Objects.equals(getSymbol(variableTree), symbol)) {
return null;
}
return super.visitVariable(variableTree, null);
}
@Override
public Void visitAssignment(AssignmentTree assignmentTree, Void unused) {
// Don't count the LHS of the assignment to the variable as a usage.
if (Objects.equals(getSymbol(assignmentTree.getVariable()), symbol)) {
return scan(assignmentTree.getExpression(), null);
}
return super.visitAssignment(assignmentTree, null);
}
private ImmutableList buildFixes(List removals) {
boolean encounteredSideEffects = false;
SuggestedFix.Builder withoutSideEffects =
SuggestedFix.builder().setShortDescription("remove unused variable and any side effects");
SuggestedFix.Builder withSideEffects =
SuggestedFix.builder().setShortDescription("remove unused variable");
for (TreePath usageSite : Iterables.concat(removals, usageSites)) {
List keepingSideEffects = new ArrayList<>();
for (TreePath path = usageSite;
!(path.getLeaf() instanceof StatementTree);
path = path.getParentPath()) {
List arguments;
if (path.getLeaf() instanceof MethodInvocationTree) {
arguments = ((MethodInvocationTree) path.getLeaf()).getArguments();
} else if (path.getLeaf() instanceof NewClassTree) {
arguments = ((NewClassTree) path.getLeaf()).getArguments();
} else {
continue;
}
arguments.stream()
.filter(SideEffectAnalysis::hasSideEffect)
.map(e -> state.getSourceForNode(e) + ";")
.forEach(keepingSideEffects::add);
}
StatementTree enclosingStatement =
state.withPath(usageSite).findEnclosing(StatementTree.class);
if (!keepingSideEffects.isEmpty()) {
encounteredSideEffects = true;
withSideEffects.replace(
enclosingStatement, keepingSideEffects.stream().collect(joining("")));
} else {
withSideEffects.replace(enclosingStatement, "");
}
withoutSideEffects.replace(enclosingStatement, "");
}
return encounteredSideEffects
? ImmutableList.of(withoutSideEffects.build(), withSideEffects.build())
: ImmutableList.of(withoutSideEffects.build());
}
}
}