All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.main.java.com.mebigfatguy.fbcontrib.detect.UnitTestAssertionOddities Maven / Gradle / Ivy

Go to download

An auxiliary findbugs.sourceforge.net plugin for java bug detectors that fall outside the narrow scope of detectors to be packaged with the product itself.

There is a newer version: 7.6.8
Show newest version
/*
 * fb-contrib - Auxiliary detectors for Java programs
 * Copyright (C) 2005-2018 Dave Brosius
 * Copyright (C) 2016-2018 Juan Martin Sotuyo Dodero
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.mebigfatguy.fbcontrib.detect;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.apache.bcel.Repository;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.ElementValuePair;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;

import com.mebigfatguy.fbcontrib.utils.BugType;
import com.mebigfatguy.fbcontrib.utils.OpcodeUtils;
import com.mebigfatguy.fbcontrib.utils.SignatureBuilder;
import com.mebigfatguy.fbcontrib.utils.SignatureUtils;
import com.mebigfatguy.fbcontrib.utils.TernaryPatcher;
import com.mebigfatguy.fbcontrib.utils.UnmodifiableSet;
import com.mebigfatguy.fbcontrib.utils.Values;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.BytecodeScanningDetector;
import edu.umd.cs.findbugs.OpcodeStack;
import edu.umd.cs.findbugs.OpcodeStack.CustomUserValue;
import edu.umd.cs.findbugs.ba.ClassContext;
import edu.umd.cs.findbugs.ba.XField;
import edu.umd.cs.findbugs.classfile.FieldDescriptor;
import edu.umd.cs.findbugs.classfile.analysis.AnnotationValue;
import edu.umd.cs.findbugs.internalAnnotations.SlashedClassName;

/** looks for odd uses of the Assert class of the JUnit and TestNG framework */
@CustomUserValue
public class UnitTestAssertionOddities extends BytecodeScanningDetector {
    private enum State {
        SAW_NOTHING, SAW_IF_ICMPNE, SAW_IF_NE, SAW_IF_ICMPEQ, SAW_ICONST_1, SAW_GOTO, SAW_ICONST_0, SAW_EQUALS
    }

    private enum TestFrameworkType {
        UNKNOWN, JUNIT, TESTNG;
    }

    private static final Set INJECTOR_ANNOTATIONS = UnmodifiableSet.create(
    // @formatter:off
        "org.mockito.Mock",
        "org.springframework.beans.factory.annotation.Autowired"
        // @formatter:on
    );

    private static final String BOOLEAN_TYPE_SIGNATURE = "Ljava/lang/Boolean;";
    private static final String LJAVA_LANG_DOUBLE = "Ljava/lang/Double;";

    private static final String TESTCASE_CLASS = "junit.framework.TestCase";
    private static final String TEST_CLASS = "org.junit.Test";
    private static final String TEST_ANNOTATION_SIGNATURE = "Lorg/junit/Test;";
    private static final String OLD_ASSERT_CLASS = "junit/framework/Assert";
    private static final String NEW_ASSERT_CLASS = "org/junit/Assert";

    private static final String TESTNG_CLASS = "org.testng.annotations.Test";
    private static final String TESTNG_ANNOTATION_SIGNATURE = "Lorg/testng/annotations/Test;";
    private static final String NG_ASSERT_CLASS = "org/testng/Assert";
    private static final String NG_JUNIT_ASSERT_CLASS = "org/testng/AssertJUnit";

    private BugReporter bugReporter;
    private JavaClass testCaseClass;
    private JavaClass testAnnotationClass;
    private JavaClass testNGAnnotationClass;
    private OpcodeStack stack;
    private boolean isTestCaseDerived;
    private boolean isAnnotationCapable;
    private @SlashedClassName String className;
    private boolean sawAssert;
    private State state;
    private boolean checkIsNegated;
    private TestFrameworkType frameworkType;
    private boolean hasAnnotation;
    private Set fieldsWithAnnotations;

    /**
     * constructs a JOA detector given the reporter to report bugs on
     *
     * @param bugReporter
     *            the sync of bug reports
     */
    public UnitTestAssertionOddities(BugReporter bugReporter) {
        this.bugReporter = bugReporter;

        try {
            testCaseClass = Repository.lookupClass(TESTCASE_CLASS);
        } catch (ClassNotFoundException cnfe) {
            testCaseClass = null;
        }
        try {
            testAnnotationClass = Repository.lookupClass(TEST_CLASS);
        } catch (ClassNotFoundException cnfe) {
            testAnnotationClass = null;
        }

        try {
            testNGAnnotationClass = Repository.lookupClass(TESTNG_CLASS);
        } catch (ClassNotFoundException cnfe) {
            testNGAnnotationClass = null;
        }
    }

    /**
     * override the visitor to see if this class could be a test class
     *
     * @param classContext
     *            the context object of the currently parsed class
     */
    @Override
    public void visitClassContext(ClassContext classContext) {
        try {
            JavaClass cls = classContext.getJavaClass();
            className = cls.getClassName().replace('.', '/');
            isTestCaseDerived = (testCaseClass != null) && cls.instanceOf(testCaseClass);
            isAnnotationCapable = (cls.getMajor() >= 5) && ((testAnnotationClass != null) || (testNGAnnotationClass != null));
            if (isTestCaseDerived || isAnnotationCapable) {
                stack = new OpcodeStack();
                fieldsWithAnnotations = new HashSet<>();
                super.visitClassContext(classContext);
            }
        } catch (ClassNotFoundException cnfe) {
            bugReporter.reportMissingClass(cnfe);
        } finally {
            stack = null;
            fieldsWithAnnotations = null;
        }
    }

    @Override
    public void visitCode(Code obj) {
        detectFrameworkType();

        if (frameworkType != TestFrameworkType.UNKNOWN) {
            stack.resetForMethodEntry(this);
            state = State.SAW_NOTHING;
            sawAssert = false;
            super.visitCode(obj);

            if (!sawAssert && !hasExpects()) {
                bugReporter.reportBug(new BugInstance(this, frameworkType == TestFrameworkType.JUNIT ? BugType.UTAO_JUNIT_ASSERTION_ODDITIES_NO_ASSERT.name()
                        : BugType.UTAO_TESTNG_ASSERTION_ODDITIES_NO_ASSERT.name(), LOW_PRIORITY).addClass(this).addMethod(this));
            }
        }
    }

    /**
     * Attempt to identify whether we are dealing with JUnit or TestNG.
     */
    private void detectFrameworkType() {
        hasAnnotation = false;
        Method m = getMethod();
        if (isTestCaseDerived && m.getName().startsWith("test")) {
            frameworkType = TestFrameworkType.JUNIT;
            return;
        }

        frameworkType = TestFrameworkType.UNKNOWN;
        if (!isAnnotationCapable) {
            return;
        }

        AnnotationEntry[] annotations = m.getAnnotationEntries();
        if (annotations == null) {
            return;
        }
        for (AnnotationEntry annotation : annotations) {
            String annotationType = annotation.getAnnotationType();
            if (annotation.isRuntimeVisible()) {
                if (TEST_ANNOTATION_SIGNATURE.equals(annotationType)) {
                    frameworkType = TestFrameworkType.JUNIT;
                    hasAnnotation = true;
                    return;
                } else if (TESTNG_ANNOTATION_SIGNATURE.equals(annotationType)) {
                    frameworkType = TestFrameworkType.TESTNG;
                    hasAnnotation = true;
                    return;
                }
            }
        }
    }

    @Override
    public void visitField(Field obj) {
        if (obj.getAnnotationEntries().length > 0) {
            fieldsWithAnnotations.add(getFieldDescriptor());
        }
    }

    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "CLI_CONSTANT_LIST_INDEX", justification = "Constrained by FindBugs API")
    @Override
    public void sawOpcode(int seen) {
        String userValue = null;

        try {
            stack.precomputation(this);

            if (seen == INVOKESTATIC) {
                String clsName = getClassConstantOperand();
                if (OLD_ASSERT_CLASS.equals(clsName) || NEW_ASSERT_CLASS.equals(clsName) || NG_JUNIT_ASSERT_CLASS.equals(clsName)) {

                    sawAssert = true;

                    if (hasAnnotation && (frameworkType == TestFrameworkType.JUNIT) && OLD_ASSERT_CLASS.equals(clsName)) {
                        bugReporter.reportBug(new BugInstance(this, BugType.UTAO_JUNIT_ASSERTION_ODDITIES_USING_DEPRECATED.name(), NORMAL_PRIORITY)
                                .addClass(this).addMethod(this).addSourceLine(this));
                    }

                    String methodName = getNameConstantOperand();
                    if ("assertEquals".equals(methodName) && processAssert()) {
                        return;
                    } else if ("assertNotEquals".equals(methodName)) {
                        String signature = getSigConstantOperand();
                        int numArguments = SignatureUtils.getNumParameters(signature);
                        if (((numArguments == 2) || (numArguments == 3)) && (stack.getStackDepth() >= 2)) {
                            OpcodeStack.Item expectedItem = stack.getStackItem(1);
                            if (expectedItem.isNull()) {
                                bugReporter.reportBug(new BugInstance(this, BugType.UTAO_JUNIT_ASSERTION_ODDITIES_USE_ASSERT_NOT_NULL.name(), NORMAL_PRIORITY)
                                        .addClass(this).addMethod(this).addSourceLine(this));
                                return;
                            }
                        }
                    } else if ("assertNotNull".equals(methodName)) {
                        if ((stack.getStackDepth() > 0) && "valueOf".equals(stack.getStackItem(0).getUserValue())) {
                            bugReporter.reportBug(new BugInstance(this, BugType.UTAO_JUNIT_ASSERTION_ODDITIES_IMPOSSIBLE_NULL.name(), NORMAL_PRIORITY)
                                    .addClass(this).addMethod(this).addSourceLine(this));
                        }
                    } else if ((!checkIsNegated && "assertTrue".equals(methodName)) || (checkIsNegated && "assertFalse".equals(methodName))) {
                        if ((state == State.SAW_ICONST_0) || (state == State.SAW_EQUALS)) {
                            bugReporter.reportBug(new BugInstance(this, BugType.UTAO_JUNIT_ASSERTION_ODDITIES_USE_ASSERT_EQUALS.name(), NORMAL_PRIORITY)
                                    .addClass(this).addMethod(this).addSourceLine(this));
                        }
                    } else if (((!checkIsNegated && "assertFalse".equals(methodName)) || (checkIsNegated && "assertTrue".equals(methodName)))
                            && ((state == State.SAW_ICONST_0) || (state == State.SAW_EQUALS))) {
                        bugReporter.reportBug(new BugInstance(this, BugType.UTAO_JUNIT_ASSERTION_ODDITIES_USE_ASSERT_NOT_EQUALS.name(), NORMAL_PRIORITY)
                                .addClass(this).addMethod(this).addSourceLine(this));
                    }
                } else if (NG_ASSERT_CLASS.equals(clsName)) {
                    sawAssert = true;
                    String methodName = getNameConstantOperand();
                    if ("assertEquals".equals(methodName) && ngProcessAssertEquals()) {
                        return;
                    } else if ("assertNotEquals".equals(methodName)) {
                        String signature = getSigConstantOperand();
                        int numArgs = SignatureUtils.getNumParameters(signature);
                        OpcodeStack.Item expectedItem;
                        if ((numArgs == 2) && (stack.getStackDepth() >= 2)) {
                            expectedItem = stack.getStackItem(0);
                        } else if ((numArgs == 3) && (stack.getStackDepth() >= 3)) {
                            expectedItem = stack.getStackItem(1);
                        } else {
                            return;
                        }

                        XField fld = expectedItem.getXField();
                        if (((fld == null) || !fieldsWithAnnotations.contains(fld.getFieldDescriptor())) && (expectedItem.isNull())) {
                            bugReporter.reportBug(new BugInstance(this, BugType.UTAO_TESTNG_ASSERTION_ODDITIES_USE_ASSERT_NOT_NULL.name(), NORMAL_PRIORITY)
                                    .addClass(this).addMethod(this).addSourceLine(this));
                            return;
                        }
                    } else if ("assertNotNull".equals(methodName)) {
                        if ((stack.getStackDepth() > 0) && "valueOf".equals(stack.getStackItem(0).getUserValue())) {
                            bugReporter.reportBug(new BugInstance(this, BugType.UTAO_TESTNG_ASSERTION_ODDITIES_IMPOSSIBLE_NULL.name(), NORMAL_PRIORITY)
                                    .addClass(this).addMethod(this).addSourceLine(this));
                        }
                    } else if ((!checkIsNegated && "assertTrue".equals(methodName)) || (checkIsNegated && "assertFalse".equals(methodName))) {
                        if ((state == State.SAW_ICONST_0) || (state == State.SAW_EQUALS)) {
                            bugReporter.reportBug(new BugInstance(this, BugType.UTAO_TESTNG_ASSERTION_ODDITIES_USE_ASSERT_EQUALS.name(), NORMAL_PRIORITY)
                                    .addClass(this).addMethod(this).addSourceLine(this));
                        }
                    } else if (((!checkIsNegated && "assertFalse".equals(methodName)) || (checkIsNegated && "assertTrue".equals(methodName)))
                            && ((state == State.SAW_ICONST_0) || (state == State.SAW_EQUALS))) {
                        bugReporter.reportBug(new BugInstance(this, BugType.UTAO_TESTNG_ASSERTION_ODDITIES_USE_ASSERT_NOT_EQUALS.name(), NORMAL_PRIORITY)
                                .addClass(this).addMethod(this).addSourceLine(this));
                    }
                } else {
                    String methodName = getNameConstantOperand();
                    String sig = getSigConstantOperand();
                    if (clsName.startsWith("java/lang/") && "valueOf".equals(methodName) && (sig.indexOf(")Ljava/lang/") >= 0)) {
                        userValue = "valueOf";
                    }
                }
            } else if ((seen == ATHROW) && (stack.getStackDepth() > 0)) {
                OpcodeStack.Item item = stack.getStackItem(0);
                String throwClass = item.getSignature();
                if ("Ljava/lang/AssertionError;".equals(throwClass)) {
                    bugReporter.reportBug(new BugInstance(this,
                            frameworkType == TestFrameworkType.JUNIT ? BugType.UTAO_JUNIT_ASSERTION_ODDITIES_ASSERT_USED.name()
                                    : BugType.UTAO_TESTNG_ASSERTION_ODDITIES_ASSERT_USED.name(),
                            NORMAL_PRIORITY).addClass(this).addMethod(this).addSourceLine(this));
                    sawAssert = true;
                }
            }

            switch (state) {
                case SAW_NOTHING:
                case SAW_EQUALS:
                    // starting the chain, reset to false
                    checkIsNegated = false;
                    if (seen == IF_ICMPNE) {
                        state = State.SAW_IF_ICMPNE;
                    } else if (seen == IFNE) {
                        state = State.SAW_IF_NE;
                        checkIsNegated = true;
                    } else if (seen == IF_ICMPEQ) {
                        state = State.SAW_IF_ICMPEQ;
                        checkIsNegated = true;
                    } else {
                        state = State.SAW_NOTHING;
                    }
                break;

                case SAW_IF_ICMPEQ:
                case SAW_IF_NE:
                case SAW_IF_ICMPNE:
                    if (seen == ICONST_1) {
                        state = State.SAW_ICONST_1;
                    } else {
                        state = State.SAW_NOTHING;
                    }
                break;

                case SAW_ICONST_1:
                    if (seen == GOTO) {
                        state = State.SAW_GOTO;
                    } else {
                        state = State.SAW_NOTHING;
                    }
                break;

                case SAW_GOTO:
                    if (seen == ICONST_0) {
                        state = State.SAW_ICONST_0;
                    } else {
                        state = State.SAW_NOTHING;
                    }
                break;

                default:
                    state = State.SAW_NOTHING;
                break;
            }

            if (OpcodeUtils.isStandardInvoke(seen)) {
                String lcName = getNameConstantOperand().toLowerCase(Locale.ENGLISH);
                if (seen == INVOKEVIRTUAL) {
                    String sig = getSigConstantOperand();
                    if ("equals".equals(lcName) && SignatureBuilder.SIG_OBJECT_TO_BOOLEAN.equals(sig)) {
                        state = State.SAW_EQUALS;
                    }
                }

                // assume that if you call a method in the unit test class, or
                // call a method with assert of verify in them
                // it's possibly doing asserts for you. Yes this is a hack

                if (className.equals(getClassConstantOperand()) || lcName.contains("assert") || lcName.contains("verify")) {
                    sawAssert = true;
                }
            }

        } finally {
            TernaryPatcher.pre(stack, seen);
            stack.sawOpcode(this, seen);
            TernaryPatcher.post(stack, seen);
            if ((userValue != null) && (stack.getStackDepth() > 0)) {
                OpcodeStack.Item item = stack.getStackItem(0);
                item.setUserValue(userValue);
            }
        }
    }

    private boolean processAssert() {
        String signature = getSigConstantOperand();
        List argTypes = SignatureUtils.getParameterSignatures(signature);
        if (((argTypes.size() == 2) || (argTypes.size() == 3)) && (stack.getStackDepth() >= 2)) {
            OpcodeStack.Item item0 = stack.getStackItem(0);
            OpcodeStack.Item expectedItem = stack.getStackItem(1);
            Object cons1 = expectedItem.getConstant();
            if ((cons1 != null) && BOOLEAN_TYPE_SIGNATURE.equals(expectedItem.getSignature()) && BOOLEAN_TYPE_SIGNATURE.equals(item0.getSignature())) {
                bugReporter.reportBug(new BugInstance(this, BugType.UTAO_JUNIT_ASSERTION_ODDITIES_BOOLEAN_ASSERT.name(), NORMAL_PRIORITY).addClass(this)
                        .addMethod(this).addSourceLine(this));
                return true;
            }
            if ((cons1 == null) && (item0.getConstant() != null) && ((argTypes.size() == 2) || !isFloatingPtPrimitive(item0.getSignature()))) {
                bugReporter.reportBug(new BugInstance(this, BugType.UTAO_JUNIT_ASSERTION_ODDITIES_ACTUAL_CONSTANT.name(), NORMAL_PRIORITY).addClass(this)
                        .addMethod(this).addSourceLine(this));
                return true;
            }
            if (expectedItem.isNull() && !hasFieldInjectorAnnotation(expectedItem)) {
                bugReporter.reportBug(new BugInstance(this, BugType.UTAO_JUNIT_ASSERTION_ODDITIES_USE_ASSERT_NULL.name(), NORMAL_PRIORITY).addClass(this)
                        .addMethod(this).addSourceLine(this));
                return true;
            }
            if (Values.SIG_PRIMITIVE_DOUBLE.equals(argTypes.get(argTypes.size() - 1)) && Values.SIG_PRIMITIVE_DOUBLE.equals(argTypes.get(argTypes.size() - 2))
                    && ((argTypes.size() < 3) || !Values.SIG_PRIMITIVE_DOUBLE.equals(argTypes.get(argTypes.size() - 3)))) {
                bugReporter.reportBug(new BugInstance(this, BugType.UTAO_JUNIT_ASSERTION_ODDITIES_INEXACT_DOUBLE.name(), NORMAL_PRIORITY).addClass(this)
                        .addMethod(this).addSourceLine(this));
                return true;
            }
        }

        return false;
    }

    private boolean ngProcessAssertEquals() {
        String signature = getSigConstantOperand();
        List argTypes = SignatureUtils.getParameterSignatures(signature);
        if ((argTypes.size() == 2) || (argTypes.size() == 3)) {

            OpcodeStack.Item actualItem, expectedItem;
            if ((argTypes.size() == 2) && (stack.getStackDepth() >= 2)) {
                expectedItem = stack.getStackItem(0);
                actualItem = stack.getStackItem(1);
            } else if ((argTypes.size() == 3) && (stack.getStackDepth() >= 3)) {
                expectedItem = stack.getStackItem(1);
                actualItem = stack.getStackItem(2);
            } else {
                return true;
            }

            Object cons1 = expectedItem.getConstant();
            if ((cons1 != null) && Values.SIG_PRIMITIVE_BOOLEAN.equals(argTypes.get(0)) && Values.SIG_PRIMITIVE_BOOLEAN.equals(argTypes.get(1))) {
                bugReporter.reportBug(new BugInstance(this, BugType.UTAO_TESTNG_ASSERTION_ODDITIES_BOOLEAN_ASSERT.name(), NORMAL_PRIORITY).addClass(this)
                        .addMethod(this).addSourceLine(this));
                return true;
            }
            if ((actualItem.getConstant() != null) && (expectedItem.getConstant() == null)
                    && ((argTypes.size() == 2) || !isFloatingPtPrimitive(actualItem.getSignature()))) {
                bugReporter.reportBug(new BugInstance(this, BugType.UTAO_TESTNG_ASSERTION_ODDITIES_ACTUAL_CONSTANT.name(), NORMAL_PRIORITY).addClass(this)
                        .addMethod(this).addSourceLine(this));
                return true;
            }
            if (expectedItem.isNull() && !hasFieldInjectorAnnotation(expectedItem)) {
                bugReporter.reportBug(new BugInstance(this, BugType.UTAO_TESTNG_ASSERTION_ODDITIES_USE_ASSERT_NULL.name(), NORMAL_PRIORITY).addClass(this)
                        .addMethod(this).addSourceLine(this));
                return true;
            }
            if (Values.SIG_JAVA_LANG_OBJECT.equals(argTypes.get(0)) && Values.SIG_JAVA_LANG_OBJECT.equals(argTypes.get(1))
                    && LJAVA_LANG_DOUBLE.equals(actualItem.getSignature()) && LJAVA_LANG_DOUBLE.equals(expectedItem.getSignature())) {
                bugReporter.reportBug(new BugInstance(this, BugType.UTAO_TESTNG_ASSERTION_ODDITIES_INEXACT_DOUBLE.name(), NORMAL_PRIORITY).addClass(this)
                        .addMethod(this).addSourceLine(this));
                return true;
            }
        }

        return false;
    }

    private boolean isFloatingPtPrimitive(String signature) {
        return Values.SIG_PRIMITIVE_DOUBLE.equals(signature) || Values.SIG_PRIMITIVE_FLOAT.equals(signature);
    }

    private boolean hasExpects() {
        AnnotationEntry[] annotations = getMethod().getAnnotationEntries();
        if (annotations != null) {
            for (AnnotationEntry annotation : annotations) {
                String type = annotation.getAnnotationType();
                if ("Lorg/junit/Test;".equals(type) || "Lorg/testng/annotations/Test;".equals(type)) {
                    ElementValuePair[] evPairs = annotation.getElementValuePairs();
                    if (evPairs != null) {
                        for (ElementValuePair evPair : evPairs) {
                            String evName = evPair.getNameString();
                            if ("expected".equals(evName) || "expectedExceptions".equals(evName)) {
                                return true;
                            }
                        }
                    }
                }
            }
        }

        return false;
    }

    private boolean hasFieldInjectorAnnotation(OpcodeStack.Item item) {
        XField xf = item.getXField();
        if (xf == null) {
            return false;
        }

        Collection annotations = xf.getAnnotations();
        for (AnnotationValue value : annotations) {
            if (INJECTOR_ANNOTATIONS.contains(value.getAnnotationClass().getDottedClassName())) {
                return true;
            }
        }
        return false;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy