com.google.errorprone.bugpatterns.ModifyingCollectionWithItself.java Source code

Java tutorial

Introduction

Here is the source code for com.google.errorprone.bugpatterns.ModifyingCollectionWithItself.java

Source

/*
 * Copyright 2012 Google Inc. All Rights Reserved.
 *
 * 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.errorprone.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.instanceMethod;
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
import static com.google.errorprone.matchers.Matchers.receiverSameAsArgument;
import static com.google.errorprone.matchers.Matchers.variableType;
import static com.sun.source.tree.Tree.Kind.IDENTIFIER;
import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.google.errorprone.util.EditDistance;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.JCTree.JCIdent;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import java.util.Collections;
import java.util.List;
import javax.lang.model.element.ElementKind;

/** @author scottjohnson@google.com (Scott Johnson) */
@BugPattern(name = "ModifyingCollectionWithItself", summary = "Using a collection function with itself as the argument.", explanation = "Invoking a collection method with the same collection as the argument is likely"
        + " incorrect.\n\n"
        + "* `collection.addAll(collection)` may cause an infinite loop, duplicate the elements,"
        + " or do nothing, depending on the type of Collection and implementation class.\n"
        + "* `collection.retainAll(collection)` is a no-op.\n"
        + "* `collection.removeAll(collection)` is the same as `collection.clear()`.\n"
        + "* `collection.containsAll(collection)` is always true.", category = JDK, severity = ERROR)
public class ModifyingCollectionWithItself extends BugChecker implements MethodInvocationTreeMatcher {

    private static final Matcher<MethodInvocationTree> IS_COLLECTION_MODIFIED_WITH_ITSELF = buildMatcher();

    private static Matcher<MethodInvocationTree> buildMatcher() {
        return anyOf(
                allOf(anyOf(instanceMethod().onDescendantOf("java.util.Collection").named("addAll"),
                        instanceMethod().onDescendantOf("java.util.Collection").named("removeAll"),
                        instanceMethod().onDescendantOf("java.util.Collection").named("containsAll"),
                        instanceMethod().onDescendantOf("java.util.Collection").named("retainAll")),
                        receiverSameAsArgument(0)),
                allOf(instanceMethod().onDescendantOf("java.util.Collection").named("addAll"),
                        receiverSameAsArgument(1)));
    }

    /**
     * Matches calls to addAll, containsAll, removeAll, and retainAll on itself
     */
    @Override
    public Description matchMethodInvocation(MethodInvocationTree t, VisitorState state) {
        if (IS_COLLECTION_MODIFIED_WITH_ITSELF.matches(t, state)) {
            return describe(t, state);
        }
        return Description.NO_MATCH;
    }

    /**
     * We expect that the lhs is a field and the rhs is an identifier, specifically a parameter to the
     * method. We base our suggested fixes on this expectation.
     *
     * <p>Case 1: If lhs is a field and rhs is an identifier, find a method parameter of the same type
     * and similar name and suggest it as the rhs. (Guess that they have misspelled the identifier.)
     *
     * <p>Case 2: If lhs is a field and rhs is not an identifier, find a method parameter of the same
     * type and similar name and suggest it as the rhs.
     *
     * <p>Case 3: If lhs is not a field and rhs is an identifier, find a class field of the same type
     * and similar name and suggest it as the lhs.
     *
     * <p>Case 4: Otherwise replace with literal meaning of functionality
     */
    private Description describe(MethodInvocationTree methodInvocationTree, VisitorState state) {
        ExpressionTree receiver = ASTHelpers.getReceiver(methodInvocationTree);

        List<? extends ExpressionTree> arguments = methodInvocationTree.getArguments();
        ExpressionTree argument;
        // .addAll(int, Collection); for the true case
        argument = arguments.size() == 2 ? arguments.get(1) : arguments.get(0);

        Description.Builder builder = buildDescription(methodInvocationTree);
        for (Fix fix : buildFixes(methodInvocationTree, state, receiver, argument)) {
            builder.addFix(fix);
        }
        return builder.build();
    }

    private List<Fix> buildFixes(MethodInvocationTree methodInvocationTree, VisitorState state,
            ExpressionTree receiver, ExpressionTree argument) {
        List<Fix> fixes;
        // this.a.addAll(...);
        if (receiver.getKind() == MEMBER_SELECT) {
            // Only inspect method parameters, unlikely to want to this.a.addAll(b), where b is another
            // field.
            fixes = fixesFromMethodParameters(state, argument);
        } else {
            // a.addAll(...)
            assert (receiver.getKind() == IDENTIFIER);

            boolean lhsIsField = ASTHelpers.getSymbol(receiver).getKind() == ElementKind.FIELD;
            fixes = lhsIsField ? fixesFromMethodParameters(state, argument) : fixesFromFields(state, receiver);
        }

        if (fixes.isEmpty()) {
            fixes = literalReplacement(methodInvocationTree, state, receiver);
        }
        return fixes;
    }

    private List<Fix> fixesFromFields(VisitorState state, final ExpressionTree receiver) {
        FluentIterable<JCVariableDecl> collectionFields = FluentIterable
                .from(ASTHelpers.findEnclosingNode(state.getPath(), JCClassDecl.class).getMembers())
                .filter(JCVariableDecl.class).filter(isCollectionVariable(state));

        Multimap<Integer, JCVariableDecl> potentialReplacements = partitionByEditDistance(
                simpleNameOfIdentifierOrMemberAccess(receiver), collectionFields);

        return buildValidReplacements(potentialReplacements, new Function<JCVariableDecl, Fix>() {
            @Override
            public Fix apply(JCVariableDecl var) {
                return SuggestedFix.replace(receiver, "this." + var.sym.toString());
            }
        });
    }

    private List<Fix> buildValidReplacements(Multimap<Integer, JCVariableDecl> potentialReplacements,
            Function<JCVariableDecl, Fix> replacementFunction) {
        if (potentialReplacements.isEmpty()) {
            return ImmutableList.of();
        }

        // Take all of the potential edit-distance replacements with the same minimum distance,
        // then suggest them as individual fixes.
        return FluentIterable.from(potentialReplacements.get(Collections.min(potentialReplacements.keySet())))
                .transform(replacementFunction).toList();
    }

    private Predicate<JCVariableDecl> isCollectionVariable(final VisitorState state) {
        return new Predicate<JCVariableDecl>() {
            @Override
            public boolean apply(JCVariableDecl var) {
                return variableType(isSubtypeOf("java.util.Collection")).matches(var, state);
            }
        };
    }

    private List<Fix> fixesFromMethodParameters(VisitorState state, final ExpressionTree argument) {
        // find a method parameter of the same type and similar name and suggest it
        // as the new argument

        assert (argument.getKind() == IDENTIFIER || argument.getKind() == MEMBER_SELECT);

        FluentIterable<JCVariableDecl> collectionParams = FluentIterable
                .from(ASTHelpers.findEnclosingNode(state.getPath(), JCMethodDecl.class).getParameters())
                .filter(isCollectionVariable(state));

        Multimap<Integer, JCVariableDecl> potentialReplacements = partitionByEditDistance(
                simpleNameOfIdentifierOrMemberAccess(argument), collectionParams);

        return buildValidReplacements(potentialReplacements, new Function<JCVariableDecl, Fix>() {
            @Override
            public Fix apply(JCVariableDecl var) {
                return SuggestedFix.replace(argument, var.sym.toString());
            }
        });
    }

    private Multimap<Integer, JCVariableDecl> partitionByEditDistance(final String baseName,
            Iterable<JCVariableDecl> candidates) {
        return Multimaps.index(candidates, new Function<JCVariableDecl, Integer>() {
            @Override
            public Integer apply(JCVariableDecl jcVariableDecl) {
                return EditDistance.getEditDistance(baseName, jcVariableDecl.name.toString());
            }
        });
    }

    private String simpleNameOfIdentifierOrMemberAccess(ExpressionTree tree) {
        String name = null;
        if (tree.getKind() == IDENTIFIER) {
            name = ((JCIdent) tree).name.toString();
        } else if (tree.getKind() == MEMBER_SELECT) {
            name = ((JCFieldAccess) tree).name.toString();
        }
        return name;
    }

    private List<Fix> literalReplacement(MethodInvocationTree methodInvocationTree, VisitorState state,
            ExpressionTree lhs) {

        Tree parent = state.getPath().getParentPath().getLeaf();

        // If the parent is an ExpressionStatement, the expression value is ignored, so we can delete
        // the call entirely (or replace removeAll with .clear()). Otherwise, we can't provide a good
        // replacement.
        if (parent instanceof ExpressionStatementTree) {
            Fix fix;
            if (instanceMethod().anyClass().named("removeAll").matches(methodInvocationTree, state)) {
                fix = SuggestedFix.replace(methodInvocationTree, lhs + ".clear()");
            } else {
                fix = SuggestedFix.delete(parent);
            }
            return ImmutableList.of(fix);
        }

        return ImmutableList.of();
    }
}