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

src.main.java.com.mebigfatguy.fbcontrib.detect.LostExceptionStackTrace 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.List;
import java.util.Map;
import java.util.Set;

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.JavaClass;
import org.apache.bcel.classfile.LocalVariable;
import org.apache.bcel.classfile.LocalVariableTable;
import org.apache.bcel.classfile.Method;

import com.mebigfatguy.fbcontrib.utils.BugType;
import com.mebigfatguy.fbcontrib.utils.CollectionUtils;
import com.mebigfatguy.fbcontrib.utils.OpcodeUtils;
import com.mebigfatguy.fbcontrib.utils.RegisterUtils;
import com.mebigfatguy.fbcontrib.utils.SignatureUtils;
import com.mebigfatguy.fbcontrib.utils.TernaryPatcher;
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.OpcodeStack.CustomUserValue;
import edu.umd.cs.findbugs.ba.ClassContext;

/**
 * looks for methods that catch exceptions, and rethrow another exception without encapsulating the original exception within it. Doing this loses the stack
 * history, and where the original problem occurred. This makes finding and fixing errors difficult.
 */
@CustomUserValue
public class LostExceptionStackTrace extends BytecodeScanningDetector {
    private static JavaClass throwableClass;
    private static JavaClass assertionClass;

    static {
        try {
            throwableClass = Repository.lookupClass(Values.SLASHED_JAVA_LANG_THROWABLE);
            assertionClass = Repository.lookupClass("java/lang/AssertionError");
        } catch (ClassNotFoundException cnfe) {
            throwableClass = null;
            assertionClass = null;
        }
    }

    private final BugReporter bugReporter;
    private OpcodeStack stack;
    private CodeException[] exceptions;
    private Set catchInfos;
    private Map exReg;
    private boolean lastWasExitPoint = false;

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

    /**
     * implements the visitor to make sure the jdk is 1.4 or better
     *
     * @param classContext
     *            the context object of the currently parsed class
     */
    @Override
    public void visitClassContext(ClassContext classContext) {
        try {
            if ((throwableClass != null) && !isPre14Class(classContext.getJavaClass())) {
                stack = new OpcodeStack();
                catchInfos = new HashSet<>();
                exReg = new HashMap<>();
                super.visitClassContext(classContext);
            }
        } finally {
            stack = null;
            catchInfos = null;
            exceptions = null;
            exReg = null;
        }
    }

    /**
     * looks for methods that contain a catch block and an ATHROW opcode
     *
     * @param code
     *            the context object of the current code block
     * @param method
     *            the context object of the current method
     * @return if the class throws exceptions
     */
    public boolean prescreen(Code code, Method method) {
        if (method.isSynthetic()) {
            return false;
        }

        CodeException[] ce = code.getExceptionTable();
        if (CollectionUtils.isEmpty(ce)) {
            return false;
        }

        BitSet bytecodeSet = getClassContext().getBytecodeSet(method);
        return (bytecodeSet != null) && bytecodeSet.get(Constants.ATHROW);
    }

    /**
     * implements the visitor to filter out methods that don't throw exceptions
     *
     * @param obj
     *            the context object of the currently parsed code block
     */
    @Override
    public void visitCode(Code obj) {
        if (prescreen(obj, getMethod())) {
            stack.resetForMethodEntry(this);
            catchInfos.clear();
            exceptions = collectExceptions(obj.getExceptionTable());
            exReg.clear();
            lastWasExitPoint = false;
            super.visitCode(obj);
        }
    }

    /**
     * collects all the valid exception objects (ones where start and finish are before the target
     *
     * @param exs
     *            the exceptions from the class file
     * @return the filtered exceptions
     */
    public CodeException[] collectExceptions(CodeException... exs) {
        List filteredEx = new ArrayList<>();
        for (CodeException ce : exs) {
            if ((ce.getCatchType() != 0) && (ce.getStartPC() < ce.getEndPC()) && (ce.getEndPC() <= ce.getHandlerPC())) {
                filteredEx.add(ce);
            }
        }
        return filteredEx.toArray(new CodeException[filteredEx.size()]);
    }

    /**
     * implements the visitor to find throwing alternative exceptions from a catch block, without forwarding along the original exception
     */
    @Override
    public void sawOpcode(int seen) {
        boolean markAsValid = false;

        try {
            stack.precomputation(this);

            int pc = getPC();
            for (CodeException ex : exceptions) {
                if (pc == ex.getEndPC()) {
                    if (OpcodeUtils.isReturn(seen)) {
                        addCatchBlock(ex.getHandlerPC(), Integer.MAX_VALUE);
                    } else if ((seen == GOTO) || (seen == GOTO_W)) {
                        addCatchBlock(ex.getHandlerPC(), this.getBranchTarget());
                    } else {
                        addCatchBlock(ex.getHandlerPC(), Integer.MAX_VALUE);
                    }
                } else if (pc == ex.getHandlerPC()) {
                    removePreviousHandlers(pc);
                }
            }

            Iterator it = catchInfos.iterator();
            while (it.hasNext()) {
                try {
                    CatchInfo catchInfo = it.next();
                    if (pc == catchInfo.getStart()) {
                        if (!updateExceptionRegister(catchInfo, seen, pc)) {
                            it.remove();
                        }
                        break;
                    } else if (pc > catchInfo.getFinish()) {
                        it.remove();
                        break;
                    } else if ((pc > catchInfo.getStart()) && (pc <= catchInfo.getFinish())) {
                        if (seen == INVOKESPECIAL) {
                            if (Values.CONSTRUCTOR.equals(getNameConstantOperand())) {
                                String className = getClassConstantOperand();
                                JavaClass exClass = Repository.lookupClass(className);
                                if (exClass.instanceOf(throwableClass)) {
                                    String sig = getSigConstantOperand();
                                    if ((sig.indexOf("Exception") >= 0) || (sig.indexOf("Throwable") >= 0) || (sig.indexOf("Error") >= 0)) {
                                        markAsValid = true;
                                        break;
                                    }
                                    if (exClass.instanceOf(assertionClass)) {
                                        // just ignore LEST for AssertionErrors
                                        markAsValid = true;
                                        break;
                                    }
                                }
                            } else if (isPossibleExBuilder(catchInfo.getRegister())) {
                                markAsValid = true;
                            }
                        } else if (seen == INVOKEVIRTUAL) {
                            String methodName = getNameConstantOperand();
                            if ("initCause".equals(methodName) || "addSuppressed".equals(methodName)) {
                                if (stack.getStackDepth() > 1) {
                                    String className = getClassConstantOperand();
                                    JavaClass exClass = Repository.lookupClass(className);
                                    if (exClass.instanceOf(throwableClass)) {
                                        OpcodeStack.Item itm = stack.getStackItem(1);
                                        int reg = itm.getRegisterNumber();
                                        if (reg >= 0) {
                                            exReg.put(Integer.valueOf(reg), Boolean.TRUE);
                                        }
                                        markAsValid = true; // Fixes javac generated code
                                    }
                                }
                            } else if (("getTargetException".equals(methodName) || "getCause".equals(methodName))
                                    && "java/lang/reflect/InvocationTargetException".equals(getClassConstantOperand())) {
                                markAsValid = true;
                            } else if (isPossibleExBuilder(catchInfo.getRegister())) {
                                markAsValid = true;
                            }
                        } else if ((seen == INVOKEINTERFACE) || (seen == INVOKESTATIC)) {
                            if (isPossibleExBuilder(catchInfo.getRegister())) {
                                markAsValid = true;
                            }
                        } else if (seen == ATHROW) {
                            if (stack.getStackDepth() > 0) {
                                OpcodeStack.Item itm = stack.getStackItem(0);
                                if ((itm.getRegisterNumber() != catchInfo.getRegister()) && (itm.getUserValue() == null)) {
                                    if (!isPre14Class(itm.getJavaClass())) {
                                        int priority = getPrevOpcode(1) == MONITOREXIT ? LOW_PRIORITY : NORMAL_PRIORITY;
                                        bugReporter.reportBug(new BugInstance(this, BugType.LEST_LOST_EXCEPTION_STACK_TRACE.name(), priority).addClass(this)
                                                .addMethod(this).addSourceLine(this));
                                    }
                                    it.remove();
                                    break;
                                }
                            }
                            if (catchInfo.getFinish() == Integer.MAX_VALUE) {
                                catchInfo.setFinish(pc);
                            }

                        } else if (OpcodeUtils.isAStore(seen)) {
                            if (lastWasExitPoint) {
                                // crazy jdk6 finally block injection -- shut
                                // off detection
                                catchInfos.clear();
                                break;
                            }

                            if (stack.getStackDepth() > 0) {
                                OpcodeStack.Item itm = stack.getStackItem(0);
                                int reg = RegisterUtils.getAStoreReg(this, seen);
                                exReg.put(Integer.valueOf(reg), (Boolean) itm.getUserValue());
                                if ((reg == catchInfo.getRegister()) && (catchInfo.getFinish() == Integer.MAX_VALUE)) {
                                    it.remove();
                                }
                            }
                        } else if (OpcodeUtils.isALoad(seen)) {
                            Boolean valid = exReg.get(Integer.valueOf(RegisterUtils.getALoadReg(this, seen)));
                            if (valid != null) {
                                markAsValid = valid.booleanValue();
                            }
                        } else if (OpcodeUtils.isReturn(seen)) {
                            removeIndeterminateHandlers(pc);
                            break;
                        }
                    }
                } catch (ClassNotFoundException cnfe) {
                    bugReporter.reportMissingClass(cnfe);
                    it.remove();
                }
            }

            lastWasExitPoint = (seen == GOTO) || (seen == GOTO_W) || (seen == ATHROW) || OpcodeUtils.isReturn(seen);
        } finally {
            TernaryPatcher.pre(stack, seen);
            stack.sawOpcode(this, seen);
            TernaryPatcher.post(stack, seen);
            if (markAsValid && (stack.getStackDepth() > 0)) {
                OpcodeStack.Item itm = stack.getStackItem(0);
                itm.setUserValue(Boolean.TRUE);
            }
        }
    }

    /**
     * returns whether the method called might be a method that builds an exception using the original exception. It does so by looking to see if the method
     * returns an exception, and if one of the parameters is the original exception
     *
     * @param excReg
     *            the register of the original exception caught
     * @return whether this method call could be an exception builder method
     *
     * @throws ClassNotFoundException
     *             if the class of the return type can't be found
     */
    public boolean isPossibleExBuilder(int excReg) throws ClassNotFoundException {
        String sig = getSigConstantOperand();
        String returnSig = SignatureUtils.getReturnSignature(sig);
        if (returnSig.startsWith(Values.SIG_QUALIFIED_CLASS_PREFIX)) {
            returnSig = SignatureUtils.trimSignature(returnSig);
            JavaClass retCls = Repository.lookupClass(returnSig);
            if (retCls.instanceOf(throwableClass)) {
                int numParms = SignatureUtils.getNumParameters(sig);
                if (stack.getStackDepth() >= numParms) {
                    for (int p = 0; p < numParms; p++) {
                        OpcodeStack.Item item = stack.getStackItem(p);
                        if (item.getRegisterNumber() == excReg) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }

    /**
     * returns whether the class in question was compiled with a jdk less than 1.4
     *
     * @param cls
     *            the class to check
     * @return whether the class is compiled with a jdk less than 1.4
     */
    private static boolean isPre14Class(JavaClass cls) {
        return (cls != null) && (cls.getMajor() < Constants.MAJOR_1_4);
    }

    private void removePreviousHandlers(int pc) {
        // This unnecessarily squashes some nested catch blocks, but better than
        // false positives
        Iterator it = catchInfos.iterator();
        while (it.hasNext()) {
            CatchInfo ci = it.next();
            if (ci.getStart() < pc) {
                it.remove();
            }
        }
    }

    private void removeIndeterminateHandlers(int pc) {
        Iterator it = catchInfos.iterator();
        while (it.hasNext()) {
            CatchInfo ci = it.next();
            if ((ci.getStart() < pc) && (ci.getFinish() == Integer.MAX_VALUE)) {
                it.remove();
            }
        }
    }

    /**
     * looks to update the catchinfo block with the register used for the exception variable. If their is a local variable table, but the local variable can't
     * be found return false, signifying an empty catch block.
     *
     * @param ci
     *            the catchinfo record for the catch starting at this pc
     * @param seen
     *            the opcode of the currently visited instruction
     * @param pc
     *            the current pc
     *
     * @return whether the catch block is empty
     */
    private boolean updateExceptionRegister(CatchInfo ci, int seen, int pc) {
        if (OpcodeUtils.isAStore(seen)) {
            int reg = RegisterUtils.getAStoreReg(this, seen);
            ci.setReg(reg);
            exReg.put(Integer.valueOf(reg), Boolean.TRUE);
            LocalVariableTable lvt = getMethod().getLocalVariableTable();
            if (lvt != null) {
                LocalVariable lv = lvt.getLocalVariable(reg, pc + 2);
                if (lv != null) {
                    int finish = lv.getStartPC() + lv.getLength();
                    if (finish < ci.getFinish()) {
                        ci.setFinish(finish);
                    }
                } else {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * add a catch block info record for the catch block that is guessed to be in the range of start to finish
     *
     * @param start
     *            the handler pc
     * @param finish
     *            the guessed end of the catch block
     */
    private void addCatchBlock(int start, int finish) {
        CatchInfo ci = new CatchInfo(start, finish);
        catchInfos.add(ci);
    }

    private static class CatchInfo {
        private final int catchStart;
        private int catchFinish;
        private int exReg;

        public CatchInfo(int start, int finish) {
            catchStart = start;
            catchFinish = finish;
            exReg = -1;
        }

        public void setReg(int reg) {
            exReg = reg;
        }

        public int getStart() {
            return catchStart;
        }

        public int getFinish() {
            return catchFinish;
        }

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

        public int getRegister() {
            return exReg;
        }

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy