/* * Copyright 2016 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.JUNIT; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import com.google.common.collect.Iterables; 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.matchers.method.MethodMatchers; 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; import java.util.ArrayList; import java.util.List; import javax.lang.model.type.TypeKind; /** * Detects floating-point assertEquals() calls that will not work in JUnit 4. * * <p>JUnit 4 bans most but not all floating-point comparisons without a delta argument. This check * will be as strict as JUnit 4, no more and no less. * * @author mwacker@google.com (Mike Wacker) */ @BugPattern( name = "JUnit3FloatingPointComparisonWithoutDelta", summary = "Floating-point comparison without error tolerance", // First sentence copied directly from JUnit 4. explanation = "Use assertEquals(expected, actual, delta) to compare floating-point numbers. " + "This call to assertEquals() will either fail or not compile in JUnit 4. " + "Use assertEquals(expected, actual, 0.0) if the delta must be 0.", category = JUNIT, severity = WARNING ) public class JUnit3FloatingPointComparisonWithoutDelta extends BugChecker implements MethodInvocationTreeMatcher { private static final Matcher<ExpressionTree> ASSERT_EQUALS_MATCHER = MethodMatchers.staticMethod().onClass("junit.framework.TestCase").named("assertEquals"); @Override public Description matchMethodInvocation( MethodInvocationTree methodInvocationTree, VisitorState state) { if (!ASSERT_EQUALS_MATCHER.matches(methodInvocationTree, state)) { return Description.NO_MATCH; } List<Type> argumentTypes = getArgumentTypesWithoutMessage(methodInvocationTree, state); if (canBeConvertedToJUnit4(state, argumentTypes)) { return Description.NO_MATCH; } Fix fix = addDeltaArgument(methodInvocationTree, state, argumentTypes); return describeMatch(methodInvocationTree, fix); } /** * Gets the argument types, excluding the message argument if present. */ private List<Type> getArgumentTypesWithoutMessage( MethodInvocationTree methodInvocationTree, VisitorState state) { List<Type> argumentTypes = new ArrayList<>(); for (ExpressionTree argument : methodInvocationTree.getArguments()) { JCTree tree = (JCTree) argument; argumentTypes.add(tree.type); } removeMessageArgumentIfPresent(state, argumentTypes); return argumentTypes; } /** * Removes the message argument if it is present. */ private void removeMessageArgumentIfPresent(VisitorState state, List<Type> argumentTypes) { if (argumentTypes.size() == 2) { return; } Types types = state.getTypes(); Type firstType = argumentTypes.get(0); if (types.isSameType(firstType, state.getSymtab().stringType)) { argumentTypes.remove(0); } } /** * Determines if the invocation can be safely converted to JUnit 4 based on its argument types. */ private boolean canBeConvertedToJUnit4(VisitorState state, List<Type> argumentTypes) { // Delta argument is used. if (argumentTypes.size() > 2) { return true; } Type firstType = argumentTypes.get(0); Type secondType = argumentTypes.get(1); // Neither argument is floating-point. if (!isFloatingPoint(state, firstType) && !isFloatingPoint(state, secondType)) { return true; } // One argument is not numeric. if (!isNumeric(state, firstType) || !isNumeric(state, secondType)) { return true; } // Neither argument is primitive. if (!firstType.isPrimitive() && !secondType.isPrimitive()) { return true; } return false; } /** * Determines if the type is a floating-point type, including reference types. */ private boolean isFloatingPoint(VisitorState state, Type type) { Type trueType = unboxedTypeOrType(state, type); return (trueType.getKind() == TypeKind.DOUBLE) || (trueType.getKind() == TypeKind.FLOAT); } /** * Determines if the type is a numeric type, including reference types. * * <p>Type.isNumeric() does not handle reference types properly. */ private boolean isNumeric(VisitorState state, Type type) { Type trueType = unboxedTypeOrType(state, type); return trueType.isNumeric(); } /** * Gets the unboxed type, or the original type if it is not unboxable. */ private Type unboxedTypeOrType(VisitorState state, Type type) { Types types = state.getTypes(); return types.unboxedTypeOrType(type); } /** * Creates the fix to add a delta argument. */ private Fix addDeltaArgument( MethodInvocationTree methodInvocationTree, VisitorState state, List<Type> argumentTypes) { int insertionIndex = getDeltaInsertionIndex(methodInvocationTree, state); String deltaArgument = getDeltaArgument(state, argumentTypes); return SuggestedFix.replace(insertionIndex, insertionIndex, deltaArgument); } /** * Gets the index of where to insert the delta argument. */ private int getDeltaInsertionIndex( MethodInvocationTree methodInvocationTree, VisitorState state) { JCTree lastArgument = (JCTree) Iterables.getLast(methodInvocationTree.getArguments()); return state.getEndPosition(lastArgument); } /** * Gets the text for the delta argument to be added. */ private String getDeltaArgument(VisitorState state, List<Type> argumentTypes) { Type firstType = argumentTypes.get(0); Type secondType = argumentTypes.get(1); boolean doublePrecisionUsed = isDouble(state, firstType) || isDouble(state, secondType); return doublePrecisionUsed ? ", 0.0" : ", 0.0f"; } /** * Determines if the type is a double, including reference types. */ private boolean isDouble(VisitorState state, Type type) { Type trueType = unboxedTypeOrType(state, type); return trueType.getKind() == TypeKind.DOUBLE; } }