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

src.main.java.com.mebigfatguy.fbcontrib.detect.ExceptionSoftening 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
 *
 * 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.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nullable;

import org.apache.bcel.Constants;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.CodeException;
import org.apache.bcel.classfile.ConstantClass;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.ExceptionTable;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.LocalVariable;
import org.apache.bcel.classfile.LocalVariableTable;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.Type;

import com.mebigfatguy.fbcontrib.utils.BugType;
import com.mebigfatguy.fbcontrib.utils.OpcodeUtils;
import com.mebigfatguy.fbcontrib.utils.RegisterUtils;
import com.mebigfatguy.fbcontrib.utils.SignatureUtils;
import com.mebigfatguy.fbcontrib.utils.ToString;
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.ba.ClassContext;

/**
 * looks for methods that catch checked exceptions, and throw unchecked exceptions in their place. There are several levels of concern. Least important are
 * methods constrained by interface or super class contracts not to throw checked exceptions but appear owned by the same author. Next are methods constrained
 * by interface or super class contracts and throw other types of checked exceptions. Lastly are method not constrained by any interface or superclass contract.
 */
public class ExceptionSoftening extends BytecodeScanningDetector {

    private final BugReporter bugReporter;
    private JavaClass runtimeClass;
    private OpcodeStack stack;
    private Map catchHandlerPCs;
    private List catchInfos;
    private LocalVariableTable lvt;
    private Map> constrainingInfo;
    private boolean isBooleanMethod;
    private boolean hasValidFalseReturn;
    private int catchFalseReturnPC;

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

        try {
            runtimeClass = Repository.lookupClass(Values.SLASHED_JAVA_LANG_RUNTIMEEXCEPTION);
        } catch (ClassNotFoundException cnfe) {
            bugReporter.reportMissingClass(cnfe);
        }
    }

    /**
     * overrides the visitor to reset the stack
     *
     * @param classContext
     *            the context object of the currently parsed class
     */
    @Override
    public void visitClassContext(ClassContext classContext) {
        try {
            if (runtimeClass != null) {
                stack = new OpcodeStack();
                super.visitClassContext(classContext);
            }
        } finally {
            stack = null;
        }
    }

    /**
     * overrides the visitor to look for methods that catch checked exceptions and rethrow runtime exceptions
     *
     * @param obj
     *            the context object of the currently parsed code block
     */
    @Override
    public void visitCode(Code obj) {
        try {
            Method method = getMethod();
            if (method.isSynthetic()) {
                return;
            }

            isBooleanMethod = Type.BOOLEAN.equals(method.getReturnType());
            if (isBooleanMethod || prescreen(method)) {
                catchHandlerPCs = collectExceptions(obj.getExceptionTable());
                if (!catchHandlerPCs.isEmpty()) {
                    stack.resetForMethodEntry(this);
                    catchInfos = new ArrayList<>();
                    lvt = method.getLocalVariableTable();
                    constrainingInfo = null;
                    hasValidFalseReturn = false;
                    catchFalseReturnPC = -1;
                    super.visitCode(obj);

                    if (!hasValidFalseReturn && (catchFalseReturnPC >= 0) && !method.getName().startsWith("is")) {
                        bugReporter.reportBug(new BugInstance(this, BugType.EXS_EXCEPTION_SOFTENING_RETURN_FALSE.name(), NORMAL_PRIORITY).addClass(this)
                                .addMethod(this).addSourceLine(this, catchFalseReturnPC));
                    }
                }
            }
        } finally {
            catchInfos = null;
            catchHandlerPCs = null;
            lvt = null;
            constrainingInfo = null;
        }
    }

    /**
     * overrides the visitor to find catch blocks that throw runtime exceptions
     *
     * @param seen
     *            the opcode of the currently parsed instruction
     */
    @Override
    public void sawOpcode(int seen) {
        try {
            stack.precomputation(this);

            int pc = getPC();
            CodeException ex = catchHandlerPCs.get(Integer.valueOf(pc));
            if (ex != null) {
                int endPC;
                if ((seen == GOTO) || (seen == GOTO_W)) {
                    endPC = this.getBranchTarget();
                } else {
                    endPC = Integer.MAX_VALUE;
                }
                ConstantPool pool = getConstantPool();
                ConstantClass ccls = (ConstantClass) pool.getConstant(ex.getCatchType());
                String catchSig = ccls.getBytes(pool);
                CatchInfo ci = new CatchInfo(ex.getHandlerPC(), endPC, catchSig);
                catchInfos.add(ci);
            }

            updateEndPCsOnCatchRegScope(catchInfos, pc, seen);
            removeFinishedCatchBlocks(catchInfos, pc);

            if (seen == ATHROW) {
                processThrow();
            } else if ((seen == IRETURN) && isBooleanMethod && !hasValidFalseReturn && (stack.getStackDepth() > 0)) {
                processBooleanReturn();
            }

        } finally {
            stack.sawOpcode(this, seen);
        }
    }

    private void processThrow() {
        try {
            if (!catchInfos.isEmpty()) {
                if (stack.getStackDepth() > 0) {
                    OpcodeStack.Item itm = stack.getStackItem(0);
                    JavaClass exClass = itm.getJavaClass();
                    if ((exClass != null) && exClass.instanceOf(runtimeClass)) {
                        Set possibleCatchSignatures = findPossibleCatchSignatures(catchInfos, getPC());
                        if (!possibleCatchSignatures.contains(exClass.getClassName())) {
                            boolean anyRuntimes = false;
                            for (String possibleCatches : possibleCatchSignatures) {
                                exClass = Repository.lookupClass(possibleCatches);
                                if (exClass.instanceOf(runtimeClass)) {
                                    anyRuntimes = true;
                                    break;
                                }
                            }

                            if (!anyRuntimes) {

                                if (constrainingInfo == null) {
                                    constrainingInfo = getConstrainingInfo(getClassContext().getJavaClass(), getMethod());
                                }

                                BugType bug = null;
                                int priority = NORMAL_PRIORITY;

                                if (constrainingInfo == null) {
                                    bug = BugType.EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS;
                                    priority = HIGH_PRIORITY;
                                } else if (!constrainingInfo.values().iterator().next().isEmpty()) {
                                    bug = BugType.EXS_EXCEPTION_SOFTENING_HAS_CHECKED;
                                    priority = NORMAL_PRIORITY;
                                } else {
                                    String pack1 = constrainingInfo.keySet().iterator().next();
                                    String pack2 = getClassContext().getJavaClass().getClassName();
                                    int dotPos = pack1.lastIndexOf('.');
                                    if (dotPos >= 0) {
                                        pack1 = pack1.substring(0, dotPos);
                                    } else {
                                        pack1 = "";
                                    }
                                    dotPos = pack2.lastIndexOf('.');
                                    if (dotPos >= 0) {
                                        pack2 = pack2.substring(0, dotPos);
                                    } else {
                                        pack2 = "";
                                    }
                                    if (SignatureUtils.similarPackages(pack1, pack2, 2)) {
                                        bug = BugType.EXS_EXCEPTION_SOFTENING_NO_CHECKED;
                                        priority = NORMAL_PRIORITY;
                                    }
                                }

                                if (bug != null) {
                                    bugReporter.reportBug(new BugInstance(this, bug.name(), priority).addClass(this).addMethod(this).addSourceLine(this));
                                }
                            }
                        }
                    }
                }
            }
        } catch (ClassNotFoundException cnfe) {
            bugReporter.reportMissingClass(cnfe);
        }
    }

    private void processBooleanReturn() {
        OpcodeStack.Item item = stack.getStackItem(0);
        Integer returnVal = (Integer) item.getConstant();
        if (returnVal == null) {
            hasValidFalseReturn = true;
        } else if ((catchFalseReturnPC < 0) && (returnVal.intValue() == 0)) {
            int pc = getPC();
            Set sigs = findPossibleCatchSignatures(catchInfos, pc);
            for (String sig : sigs) {
                if (!sig.isEmpty()) {
                    catchFalseReturnPC = pc;
                    break;
                }
            }
            if (catchFalseReturnPC < 0) {
                hasValidFalseReturn = true;
            }
        }
    }

    /**
     * collects all the valid exception objects (ones where start and finish are before the target) and with a catch type
     *
     * @param exceptions
     *            the exceptions from the class file
     * @return the filtered exceptions keyed by catch end pc
     */
    private static LinkedHashMap collectExceptions(CodeException... exceptions) {
        List filteredEx = new ArrayList<>();
        for (CodeException ce : exceptions) {
            if ((ce.getCatchType() != 0) && (ce.getStartPC() < ce.getEndPC()) && (ce.getEndPC() <= ce.getHandlerPC())) {
                filteredEx.add(ce);
            }
        }

        LinkedHashMap handlers = new LinkedHashMap<>();

        for (CodeException ex : filteredEx) {
            handlers.put(Integer.valueOf(ex.getEndPC()), ex);
        }

        return handlers;
    }

    /**
     * remove catchinfo blocks from the map where the handler end is before the current pc
     *
     * @param infos
     *            the exception handlers installed
     * @param pc
     *            the current pc
     */
    private static void removeFinishedCatchBlocks(List infos, int pc) {
        Iterator it = infos.iterator();
        while (it.hasNext()) {
            if (it.next().getFinish() < pc) {
                it.remove();
            }
        }
    }

    /**
     * reduces the end pc based on the optional LocalVariableTable's exception register scope
     *
     * @param infos
     *            the list of active catch blocks
     * @param pc
     *            the current pc
     * @param seen
     *            the currently parsed opcode
     */
    private void updateEndPCsOnCatchRegScope(List infos, int pc, int seen) {
        if (lvt != null) {
            for (CatchInfo ci : infos) {
                if ((ci.getStart() == pc) && OpcodeUtils.isAStore(seen)) {
                    int exReg = RegisterUtils.getAStoreReg(this, seen);
                    LocalVariable lv = lvt.getLocalVariable(exReg, pc + 1);
                    if (lv != null) {
                        ci.setFinish(lv.getStartPC() + lv.getLength());
                    }
                    break;
                }
            }
        }
    }

    /**
     * returns an array of catch types that the current pc is in
     *
     * @param infos
     *            the list of catch infos for this method
     * @param pc
     *            the current pc
     * @return an set of catch exception types that the pc is currently in
     */
    private static Set findPossibleCatchSignatures(List infos, int pc) {
        Set catchTypes = new HashSet<>(6);
        ListIterator it = infos.listIterator(infos.size());
        while (it.hasPrevious()) {
            CatchInfo ci = it.previous();
            if ((pc >= ci.getStart()) && (pc < ci.getFinish())) {
                catchTypes.add(ci.getSignature());
            } else {
                break;
            }
        }

        return catchTypes;
    }

    /**
     * finds the super class or interface that constrains the types of exceptions that can be thrown from the given method
     *
     * @param cls
     *            the currently parsed class
     * @param m
     *            the method to check
     * @return a map containing the class name to a set of exceptions that constrain this method
     *
     * @throws ClassNotFoundException
     *             if a super class or super interface can't be loaded from the repository
     */
    @Nullable
    private Map> getConstrainingInfo(JavaClass cls, Method m) throws ClassNotFoundException {
        String methodName = m.getName();
        String methodSig = m.getSignature();

        {
            // First look for the method in interfaces of the class
            JavaClass[] infClasses = cls.getInterfaces();

            for (JavaClass infCls : infClasses) {
                Method infMethod = findMethod(infCls, methodName, methodSig);
                if (infMethod != null) {
                    return buildConstrainingInfo(infCls, infMethod);
                }

                Map> constrainingExs = getConstrainingInfo(infCls, m);
                if (constrainingExs != null) {
                    return constrainingExs;
                }
            }
        }

        {
            // Next look at the superclass
            JavaClass superCls = cls.getSuperClass();
            if (superCls == null) {
                return null;
            }

            Method superMethod = findMethod(superCls, methodName, methodSig);
            if (superMethod != null) {
                return buildConstrainingInfo(superCls, superMethod);
            }

            // Otherwise recursively call this on the super class
            return getConstrainingInfo(superCls, m);

        }
    }

    /**
     * finds a method that matches the name and signature in the given class
     *
     * @param cls
     *            the class to look in
     * @param methodName
     *            the name to look for
     * @param methodSig
     *            the signature to look for
     *
     * @return the method or null
     */
    @Nullable
    private static Method findMethod(JavaClass cls, String methodName, String methodSig) {
        Method[] methods = cls.getMethods();
        for (Method method : methods) {
            if (method.getName().equals(methodName) && method.getSignature().equals(methodSig)) {
                return method;
            }
        }
        return null;
    }

    /**
     * returns exception names describing what exceptions are allowed to be thrown
     *
     * @param cls
     *            the cls to find the exceptions in
     * @param m
     *            the method to add exceptions from
     * @return a map with one entry of a class name to a set of exceptions that constrain what can be thrown.
     *
     * @throws ClassNotFoundException
     *             if an exception class can't be loaded from the repository
     */
    private Map> buildConstrainingInfo(JavaClass cls, Method m) throws ClassNotFoundException {
        Map> constraintInfo = new HashMap<>();
        Set exs = new HashSet<>();
        ExceptionTable et = m.getExceptionTable();
        if (et != null) {
            int[] indexTable = et.getExceptionIndexTable();
            ConstantPool pool = cls.getConstantPool();
            for (int index : indexTable) {
                if (index != 0) {
                    ConstantClass ccls = (ConstantClass) pool.getConstant(index);
                    String exName = ccls.getBytes(pool);
                    JavaClass exClass = Repository.lookupClass(exName);
                    if (!exClass.instanceOf(runtimeClass)) {
                        exs.add(ccls.getBytes(pool));
                    }
                }
            }
        }
        constraintInfo.put(cls.getClassName(), exs);
        return constraintInfo;
    }

    /**
     * returns whether a method explicitly throws an exception
     *
     * @param method
     *            the currently parsed method
     * @return if the method throws an exception
     */
    private boolean prescreen(Method method) {
        BitSet bytecodeSet = getClassContext().getBytecodeSet(method);
        return (bytecodeSet != null) && (bytecodeSet.get(Constants.ATHROW));
    }

    /**
     * holds information about a catch block the start and end pcs, as well as the exception signature. you can't always determine the end of a catch block, and
     * in this case the value will be Integer.MAX_VALUE
     */
    private static class CatchInfo {
        private final int catchStart;
        private int catchFinish;
        private final String catchSignature;

        public CatchInfo(int start, int finish, String signature) {
            catchStart = start;
            catchFinish = finish;
            catchSignature = signature;
        }

        public int getStart() {
            return catchStart;
        }

        public void setFinish(int finish) {
            catchFinish = finish;
        }

        public int getFinish() {
            return catchFinish;
        }

        public String getSignature() {
            return catchSignature;
        }

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy