/* * Copyright 2014 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.argument; import static com.google.errorprone.matchers.Matchers.hasArguments; import static com.google.errorprone.matchers.Matchers.instanceMethod; import static com.google.errorprone.matchers.Matchers.staticMethod; import static com.google.errorprone.predicates.TypePredicates.isArray; import com.google.common.base.Preconditions; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.ChildMultiMatcher.MatchType; import com.google.errorprone.matchers.Description; import com.google.errorprone.matchers.Matcher; import com.google.errorprone.matchers.Matchers; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Types; import com.sun.tools.javac.tree.JCTree.JCFieldAccess; /** @author eaftan@google.com (Eddie Aftandilian) */ @BugPattern( name = "ArrayHashCode", summary = "hashcode method on array does not hash array contents", category = JDK, severity = ERROR ) public class ArrayHashCode extends BugChecker implements MethodInvocationTreeMatcher { /** * Matches calls to varargs hashcode methods {@link com.google.common.base.Objects#hashCode} and * {@link java.util.Objects#hash} in which at least one argument is of array type. */ private static final Matcher<MethodInvocationTree> varargsHashCodeMethodMatcher = allOf( Matchers.anyOf( staticMethod().onClass("com.google.common.base.Objects").named("hashCode"), staticMethod().onClass("java.util.Objects").named("hash")), hasArguments(MatchType.AT_LEAST_ONE, Matchers.<ExpressionTree>isArrayType())); /** * Matches calls to the JDK7 method {@link java.util.Objects#hashCode} with an argument of array * type. This method is not varargs, so we don't need to check the number of arguments. */ @SuppressWarnings({"unchecked"}) private static final Matcher<MethodInvocationTree> jdk7HashCodeMethodMatcher = allOf( staticMethod().onClass("java.util.Objects").named("hashCode"), argument(0, Matchers.<ExpressionTree>isArrayType())); /** * Matches calls to the hashCode instance method on an array. */ private static final Matcher<ExpressionTree> instanceHashCodeMethodMatcher = instanceMethod().onClass(isArray()).named("hashCode"); /** * Wraps identity hashcode computations in calls to {@link java.util.Arrays#hashCode} if the * array is single dimensional or {@link java.util.Arrays#deepHashCode} if the array is * multidimensional. * * <p>If there is only one argument to the hashcode method or the instance hashcode method is * used, replaces the whole method invocation. If there are multiple arguments, wraps any that * are of array type with the appropriate {@link java.util.Arrays} hashcode method. */ @Override public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { SuggestedFix.Builder fix = null; Types types = state.getTypes(); if (jdk7HashCodeMethodMatcher.matches(tree, state)) { // java.util.Objects#hashCode takes a single argument, so rewrite the whole method call // to use Arrays.hashCode/deepHashCode instead. fix = SuggestedFix.builder() .replace(tree, rewriteArrayArgument(tree.getArguments().get(0), types)); } else if (instanceHashCodeMethodMatcher.matches(tree, state)) { // Rewrite call to instance hashCode method to use Arrays.hashCode/deepHashCode instead. fix = SuggestedFix.builder() .replace(tree, rewriteArrayArgument( ((JCFieldAccess) tree.getMethodSelect()).getExpression(), types)); } else if (varargsHashCodeMethodMatcher.matches(tree, state)) { // Varargs hash code methods, java.util.Objects#hash and // com.google.common.base.Objects#hashCode if (tree.getArguments().size() == 1) { // If only one argument, type must be either primitive array or multidimensional array. // Types like Object[], String[], etc. are not an error because they don't get boxed // in this single-argument varargs call. ExpressionTree arg = tree.getArguments().get(0); Type elemType = types.elemtype(ASTHelpers.getType(arg)); if (elemType.isPrimitive() || types.isArray(elemType)) { fix = SuggestedFix.builder() .replace(tree, rewriteArrayArgument(arg, types)); } } else { // If more than one argument, wrap each argument in a call to Arrays#hashCode/deepHashCode. fix = SuggestedFix.builder(); for (ExpressionTree arg : tree.getArguments()) { if (types.isArray(ASTHelpers.getType(arg))) { fix.replace(arg, rewriteArrayArgument(arg, types)); } } } } if (fix != null) { fix.addImport("java.util.Arrays"); return describeMatch(tree, fix.build()); } return Description.NO_MATCH; } /** * Given an {@link ExpressionTree} that represents an argument of array type, rewrites it to wrap * it in a call to either {@link java.util.Arrays#hashCode} if it is single dimensional, or * {@link java.util.Arrays#deepHashCode} if it is multidimensional. */ private static String rewriteArrayArgument(ExpressionTree arg, Types types) { Type argType = ASTHelpers.getType(arg); Preconditions.checkState(types.isArray(argType), "arg must be of array type"); if (types.isArray(types.elemtype(argType))) { return "Arrays.deepHashCode(" + arg + ")"; } else { return "Arrays.hashCode(" + arg + ")"; } } }