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

Java tutorial

Introduction

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

Source

/*
 * 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;
    }
}