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

src.main.java.com.mebigfatguy.fbcontrib.detect.AnnotationIssues 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.9
Show newest version
/*
 * fb-contrib - Auxiliary detectors for Java programs
 * Copyright (C) 2005-2019 Dave Brosius
 *
 * 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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.bcel.Constants;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;

import com.mebigfatguy.fbcontrib.collect.MethodInfo;
import com.mebigfatguy.fbcontrib.collect.Statistics;
import com.mebigfatguy.fbcontrib.utils.BugType;
import com.mebigfatguy.fbcontrib.utils.CodeByteUtils;
import com.mebigfatguy.fbcontrib.utils.FQMethod;
import com.mebigfatguy.fbcontrib.utils.OpcodeUtils;
import com.mebigfatguy.fbcontrib.utils.SignatureBuilder;
import com.mebigfatguy.fbcontrib.utils.StopOpcodeParsingException;
import com.mebigfatguy.fbcontrib.utils.ToString;
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.XMethod;
import edu.umd.cs.findbugs.internalAnnotations.SlashedClassName;

/**
 * looks for common problems with the application of annotations.
 * 
 *   - missing @Nullable annotations
 */
@CustomUserValue
public class AnnotationIssues extends BytecodeScanningDetector {

    private static final String USER_NULLABLE_ANNOTATIONS = "fb-contrib.ai.annotations";

    private static final Set IS_EMPTY_SIGNATURES = UnmodifiableSet.create(
            // @formatter:off
            new SignatureBuilder().withParamTypes(Collection.class).withReturnType(boolean.class).build(),
            new SignatureBuilder().withParamTypes(Map.class).withReturnType(boolean.class).build()
    // @formatter:on
    );

    private static final Set NULLABLE_ANNOTATIONS = new HashSet<>();

    static {
        NULLABLE_ANNOTATIONS.add("Lorg/jetbrains/annotations/Nullable;");
        NULLABLE_ANNOTATIONS.add("Ljavax/annotation/Nullable;");
        NULLABLE_ANNOTATIONS.add("Ljavax/annotation/CheckForNull;");
        NULLABLE_ANNOTATIONS.add("Lcom/sun/istack/Nullable;");
        NULLABLE_ANNOTATIONS.add("Ledu/umd/cs/findbugs/annotations/Nullable;");
        NULLABLE_ANNOTATIONS.add("Lorg/springframework/lang/Nullable;");
        NULLABLE_ANNOTATIONS.add("Landroid/support/annotation/Nullable");

        String userAnnotations = System.getProperty(USER_NULLABLE_ANNOTATIONS);
        if ((userAnnotations != null) && !userAnnotations.isEmpty()) {
            String[] annotations = userAnnotations.split(Values.WHITESPACE_COMMA_SPLIT);
            for (String annotation : annotations) {
                NULLABLE_ANNOTATIONS.add("L" + annotation.replace('.', '/') + ";");
            }
        }
    }

    private static final Set NOTABLE_EXCEPTIONS = UnmodifiableSet.create(
            // @formatter:off
            new FQMethod(Values.SLASHED_JAVA_LANG_CLASS, "newInstance", SignatureBuilder.SIG_VOID_TO_OBJECT)
    // @formatter:on
    );

    public static class AIUserValue {

        int reg;

        public AIUserValue(int reg) {
            this.reg = reg;
        }

        @Override
        public String toString() {
            return ToString.build(this);
        }
    };

    private BugReporter bugReporter;
    private Map assumedNullTill;
    private Map assumedNonNullTill;
    private Set noAssumptionsPossible;
    private List branchTargets;
    private OpcodeStack stack;
    private boolean methodIsNullable;

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

    public boolean isCollecting() {
        return false;
    }

    @Override
    public void visitClassContext(ClassContext classContext) {
        try {
            JavaClass cls = classContext.getJavaClass();
            if (cls.getMajor() >= Constants.MAJOR_1_5) {
                if (isCollecting() || !cls.isAnonymous()) {
                    stack = new OpcodeStack();
                    assumedNullTill = new HashMap<>();
                    assumedNonNullTill = new HashMap<>();
                    noAssumptionsPossible = new HashSet<>();
                    branchTargets = new ArrayList<>();
                    super.visitClassContext(classContext);
                }
            }
        } finally {
            stack = null;
            assumedNullTill = null;
            assumedNonNullTill = null;
            noAssumptionsPossible = null;
            branchTargets = null;
        }
    }

    @Override
    public void visitCode(Code obj) {

        Method method = getMethod();
        String sig = method.getSignature();
        String returnType = sig.substring(sig.indexOf(')') + 1);
        char returnTypeChar = returnType.charAt(0);
        if ((returnTypeChar != 'L') && (returnTypeChar != '[')) {
            return;
        }

        if (method.isSynthetic() && !isCollecting()) {
            return;
        }

        if (Values.SIG_JAVA_LANG_VOID.equals(returnType)) {
            return;
        }

        if (NOTABLE_EXCEPTIONS.contains(new FQMethod(getClassName(), method.getName(), sig))) {
            MethodInfo methodInfo = Statistics.getStatistics().getMethodStatistics(getClassName(), method.getName(),
                    method.getSignature());
            methodInfo.setCanReturnNull(false);
            return;
        }

        if (methodHasNullableAnnotation(method)) {
            if (isCollecting()) {
                MethodInfo methodInfo = Statistics.getStatistics().getMethodStatistics(getClassName(), method.getName(),
                        method.getSignature());
                methodInfo.setCanReturnNull(true);
            }
            return;
        }

        MethodInfo methodInfo = Statistics.getStatistics().getMethodStatistics(getClassName(), method.getName(),
                method.getSignature());
        if (!isCollecting() && methodInfo.getCanReturnNull() && !methodInfo.isDerived()) {
            bugReporter
                    .reportBug(new BugInstance(this, BugType.AI_ANNOTATION_ISSUES_NEEDS_NULLABLE.name(), LOW_PRIORITY)
                            .addClass(this).addMethod(this));
        } else {

            methodIsNullable = false;
            stack.resetForMethodEntry(this);
            assumedNullTill.clear();
            assumedNonNullTill.clear();
            noAssumptionsPossible.clear();
            branchTargets.clear();

            try {
                super.visitCode(obj);
            } catch (StopOpcodeParsingException e) {
            }

            if (methodIsNullable) {
                if (isCollecting()) {
                    methodInfo.setCanReturnNull(true);
                } else if (!methodInfo.isDerived()) {
                    bugReporter.reportBug(
                            new BugInstance(this, BugType.AI_ANNOTATION_ISSUES_NEEDS_NULLABLE.name(), LOW_PRIORITY)
                                    .addClass(this).addMethod(this));
                }
            }
        }
    }

    @Override
    public void sawOpcode(int seen) {
        AIUserValue userValue = null;

        if (OpcodeUtils.isBranch(seen) && (getBranchOffset() > 0)) {
            branchTargets.add(getBranchTarget());
            Collections.sort(branchTargets);
        }

        clearBranchTargets(getPC());
        convertNullToNonNull(getPC());
        clearAssumptions(assumedNullTill, getPC());
        clearAssumptions(assumedNonNullTill, getPC());

        if (OpcodeUtils.isBranch(seen) && (getBranchOffset() > 0)) {
            branchTargets.add(getBranchTarget());
            Collections.sort(branchTargets);
        }

        try {
            switch (seen) {
            case ARETURN: {
                if (!methodIsNullable && (stack.getStackDepth() > 0)) {
                    OpcodeStack.Item itm = stack.getStackItem(0);
                    Integer reg = Integer.valueOf(itm.getRegisterNumber());
                    methodIsNullable = !assumedNonNullTill.containsKey(reg)
                            && (!noAssumptionsPossible.contains(reg) && ((assumedNullTill.containsKey(reg))
                                    || isStackElementNullable(getClassName(), getMethod(), itm)));
                    if (methodIsNullable) {
                        throw new StopOpcodeParsingException();
                    }
                }
                break;
            }

            case IFNONNULL:
                if (getBranchOffset() > 0) {
                    if (stack.getStackDepth() > 0) {
                        OpcodeStack.Item itm = stack.getStackItem(0);
                        int reg = itm.getRegisterNumber();
                        if (reg >= 0) {
                            assumedNullTill.put(reg, getBranchTarget());
                        }
                    }
                }
                break;

            case IFNULL:
                if (getBranchOffset() > 0) {
                    if (stack.getStackDepth() > 0) {
                        OpcodeStack.Item itm = stack.getStackItem(0);
                        int reg = itm.getRegisterNumber();
                        if (reg >= 0) {
                            assumedNonNullTill.put(reg, getBranchTarget());
                        }
                    }
                }
                break;

            case IFEQ:
                if ((getBranchOffset() > 0) && (stack.getStackDepth() > 0)) {
                    OpcodeStack.Item itm = stack.getStackItem(0);
                    AIUserValue uv = (AIUserValue) itm.getUserValue();
                    if ((uv != null) && (uv.reg >= 0)) {
                        assumedNullTill.put(uv.reg, getBranchTarget());
                    }
                }
                break;

            case INVOKESTATIC:
                if (stack.getStackDepth() > 0) {
                    String signature = getSigConstantOperand();
                    if (IS_EMPTY_SIGNATURES.contains(signature)) {
                        String methodName = getNameConstantOperand();
                        if ("isEmpty".equals(methodName)) {
                            OpcodeStack.Item item = stack.getStackItem(0);
                            int reg = item.getRegisterNumber();
                            if (reg >= 0) {
                                userValue = new AIUserValue(reg);
                                break;
                            }
                        }
                    }
                }

                // $FALL-THROUGH$
            case INVOKEINTERFACE:
            case INVOKEVIRTUAL: {
                boolean resultIsNullable = (isMethodNullable(getClassConstantOperand(), getNameConstantOperand(),
                        getSigConstantOperand()));
                if (resultIsNullable) {
                    userValue = new AIUserValue(-1);
                }
                break;
            }

            case ATHROW: {
                removeAssumptions(assumedNonNullTill);
                removeAssumptions(assumedNullTill);
                break;
            }

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

    public static boolean methodHasNullableAnnotation(Method m) {
        for (AnnotationEntry entry : m.getAnnotationEntries()) {
            String annotationType = entry.getAnnotationType();
            if (NULLABLE_ANNOTATIONS.contains(annotationType)) {
                return true;
            }
        }

        return false;
    }

    public static boolean isStackElementNullable(String className, Method method, OpcodeStack.Item itm) {
        if (itm.isNull() || (itm.getUserValue() != null)) {
            MethodInfo mi = Statistics.getStatistics().getMethodStatistics(className, method.getName(),
                    method.getSignature());
            if (mi != null) {
                mi.setCanReturnNull(true);
            }
            return true;
        } else {
            XMethod xm = itm.getReturnValueOf();
            if (xm != null) {
                MethodInfo mi = Statistics.getStatistics().getMethodStatistics(xm.getClassName().replace('.', '/'),
                        xm.getName(), xm.getSignature());
                if ((mi != null) && mi.getCanReturnNull()) {
                    mi = Statistics.getStatistics().getMethodStatistics(className, method.getName(),
                            method.getSignature());
                    if (mi != null) {
                        mi.setCanReturnNull(true);
                    }
                    return true;
                }
            }
        }

        return false;
    }

    public static boolean isMethodNullable(@SlashedClassName String className, String methodName,
            String methodSignature) {
        char returnTypeChar = methodSignature.charAt(methodSignature.indexOf(')') + 1);
        if ((returnTypeChar != 'L') && (returnTypeChar != '[')) {
            return false;
        }
        MethodInfo mi = Statistics.getStatistics().getMethodStatistics(className, methodName, methodSignature);
        return ((mi != null) && mi.getCanReturnNull());

        // can we check if it has @Nullable on it? hmm need to convert to Method
    }

    /**
     * the map is keyed by register, and value by when an assumption holds to a byte
     * offset if we have passed when the assumption holds, clear the item from the
     * map
     *
     * @param assumptionTill the map of assumptions
     * @param pc             // * the current pc
     */
    public static void clearAssumptions(Map assumptionTill, int pc) {
        Iterator it = assumptionTill.values().iterator();
        while (it.hasNext()) {
            if (it.next().intValue() <= pc) {
                it.remove();
            }
        }
    }

    public void convertNullToNonNull(int pc) {
        for (Map.Entry entry : assumedNullTill.entrySet()) {
            if (entry.getValue().intValue() == pc) {
                int lastOp = getPrevOpcode(1);
                if ((lastOp == ARETURN) || (lastOp == ATHROW)) {
                    int nonNullTill = getNextBranchTarget();
                    assumedNonNullTill.put(entry.getKey(), nonNullTill);
                } else if (OpcodeUtils.isBranch(lastOp)) {
                    int branchOffset = CodeByteUtils.getshort(getCode().getCode(), getPC() - 2);
                    if (branchOffset > 0) {
                        assumedNonNullTill.put(entry.getKey(), getPC() + branchOffset);
                    }
                }
            }
        }
    }

    /**
     * remove branch targets that have been passed
     *
     * @param pc the current pc
     */
    public void clearBranchTargets(int pc) {
        Iterator it = branchTargets.iterator();
        while (it.hasNext()) {
            int target = it.next().intValue();
            if (target <= pc) {
                it.remove();
            }
        }
    }

    public int getNextBranchTarget() {
        if (branchTargets.isEmpty()) {
            return Integer.MAX_VALUE;
        }

        return branchTargets.get(0);
    }

    public void removeAssumptions(Map assumptionsTill) {
        noAssumptionsPossible.addAll(assumptionsTill.keySet());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy