src.main.java.com.mebigfatguy.fbcontrib.detect.LostExceptionStackTrace Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fb-contrib Show documentation
Show all versions of fb-contrib Show documentation
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.
/*
* 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()))
|| "java/io/UncheckedIOException".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);
}
}
}