/*
* fb-contrib - Auxiliary detectors for Java programs
* Copyright (C) 2005-2017 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.List;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.CodeException;
import org.apache.bcel.classfile.ExceptionTable;
import org.apache.bcel.classfile.JavaClass;
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.StopOpcodeParsingException;
import com.mebigfatguy.fbcontrib.utils.ToString;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.BytecodeScanningDetector;
import edu.umd.cs.findbugs.ba.ClassContext;
/**
* find methods that return or throw exception from a finally block. Doing so short-circuits the return or exception thrown from the try block, and masks it.
*/
public class AbnormalFinallyBlockReturn extends BytecodeScanningDetector {
private final BugReporter bugReporter;
private List<FinallyBlockInfo> fbInfo;
private int loadedReg;
/**
* constructs a AFBR detector given the reporter to report bugs on.
*
* @param bugReporter
* the sync of bug reports
*/
public AbnormalFinallyBlockReturn(final BugReporter bugReporter) {
this.bugReporter = bugReporter;
}
/**
* overrides the visitor to check for java class version being as good or better than 1.4
*
* @param classContext
* the context object that holds the JavaClass parsed
*/
@Override
public void visitClassContext(ClassContext classContext) {
// TODO: Look at method calls in a finally block to see if they throw
// exceptions
// : and those exceptions are not caught in the finally block
// : Only do it if effort is on, ie: boolean fullAnalysis =
// AnalysisContext.currentAnalysisContext().getBoolProperty(FindBugsAnalysisFeatures.INTERPROCEDURAL_ANALYSIS_OF_REFERENCED_CLASSES);
try {
int majorVersion = classContext.getJavaClass().getMajor();
if (majorVersion >= MAJOR_1_4) {
fbInfo = new ArrayList<>();
super.visitClassContext(classContext);
}
} finally {
fbInfo = null;
}
}
/**
* overrides the visitor to collect finally block info.
*
* @param obj
* the code object to scan for finally blocks
*/
@Override
public void visitCode(Code obj) {
fbInfo.clear();
loadedReg = -1;
CodeException[] exc = obj.getExceptionTable();
if (exc != null) {
for (CodeException ce : exc) {
if ((ce.getCatchType() == 0) && (ce.getStartPC() == ce.getHandlerPC())) {
fbInfo.add(new FinallyBlockInfo(ce.getStartPC()));
}
}
}
if (!fbInfo.isEmpty()) {
try {
super.visitCode(obj);
} catch (StopOpcodeParsingException e) {
// no more finally blocks to check
}
}
}
/**
* overrides the visitor to find return/exceptions from the finally block.
*
* @param seen
* the opcode that is being visited
*/
@Override
public void sawOpcode(int seen) {
FinallyBlockInfo fbi = fbInfo.get(0);
if (getPC() < fbi.startPC) {
return;
}
if (getPC() == fbi.startPC) {
if (OpcodeUtils.isAStore(seen)) {
fbi.exReg = RegisterUtils.getAStoreReg(this, seen);
} else {
removeEarliestFinallyBlock();
sawOpcode(seen);
return;
}
return;
}
if (seen == MONITORENTER) {
fbi.monitorCount++;
} else if (seen == MONITOREXIT) {
fbi.monitorCount--;
if (fbi.monitorCount < 0) {
removeEarliestFinallyBlock();
sawOpcode(seen);
return;
}
}
if ((seen == ATHROW) && (loadedReg == fbi.exReg)) {
removeEarliestFinallyBlock();
sawOpcode(seen);
return;
} else if (OpcodeUtils.isALoad(seen)) {
loadedReg = RegisterUtils.getALoadReg(this, seen);
} else {
loadedReg = -1;
}
if (OpcodeUtils.isReturn(seen) || (seen == ATHROW)) {
bugReporter.reportBug(new BugInstance(this, BugType.AFBR_ABNORMAL_FINALLY_BLOCK_RETURN.name(), NORMAL_PRIORITY).addClass(this).addMethod(this)
.addSourceLine(this));
removeEarliestFinallyBlock();
} else if (OpcodeUtils.isStandardInvoke(seen)) {
try {
JavaClass cls = Repository.lookupClass(getClassConstantOperand());
Method m = findMethod(cls, getNameConstantOperand(), getSigConstantOperand());
if (m != null) {
ExceptionTable et = m.getExceptionTable();
if ((et != null) && (et.getLength() > 0) && !catchBlockInFinally(fbi)) {
bugReporter.reportBug(new BugInstance(this, BugType.AFBR_ABNORMAL_FINALLY_BLOCK_RETURN.name(), LOW_PRIORITY).addClass(this)
.addMethod(this).addSourceLine(this));
removeEarliestFinallyBlock();
}
}
} catch (ClassNotFoundException cnfe) {
bugReporter.reportMissingClass(cnfe);
}
}
}
/**
* removes the earliest finally block, as we've just concluded checking it, and if it's the last one then throw back to visitCode
*/
private void removeEarliestFinallyBlock() {
fbInfo.remove(0);
if (fbInfo.isEmpty()) {
throw new StopOpcodeParsingException();
}
}
/**
* finds the method in specified class by name and signature
*
* @param cls
* the class to look the method in
* @param name
* the name of the method to look for
* @param sig
* the signature of the method to look for
*
* @return the Method object for the specified information
*/
private static Method findMethod(JavaClass cls, String name, String sig) {
Method[] methods = cls.getMethods();
for (Method m : methods) {
if (m.getName().equals(name) && m.getSignature().equals(sig)) {
return m;
}
}
return null;
}
/**
* looks to see if any try/catch block exists inside this finally block, that wrap the current pc. This is a lax check as the try catch block may not catch
* exceptions that are thrown, but doing so would be prohibitively slow. But it should catch some problems.
*
* @param fBlockInfo
* the finally block the pc is currently in
*
* @return if all exceptions are caught inside this finally block
*/
private boolean catchBlockInFinally(FinallyBlockInfo fBlockInfo) {
CodeException[] catchExceptions = getCode().getExceptionTable();
if (CollectionUtils.isEmpty(catchExceptions)) {
return false;
}
int pc = getPC();
for (CodeException ex : catchExceptions) {
if ((ex.getStartPC() <= pc) && (ex.getEndPC() >= pc) && (ex.getStartPC() >= fBlockInfo.startPC)) {
return true;
}
}
return false;
}
/**
* holds the finally block information for a particular method.
*/
static class FinallyBlockInfo {
public int startPC;
public int monitorCount;
public int exReg;
/**
* create a finally block info for a specific code range
*
* @param start
* the start of the try block
*/
FinallyBlockInfo(int start) {
startPC = start;
monitorCount = 0;
exReg = -1;
}
@Override
public String toString() {
return ToString.build(this);
}
}
}