/*
* FindBugs - Find bugs in Java programs
* Copyright (C) 2003-2005, University of Maryland
*
* 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 edu.umd.cs.findbugs.detect;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.ListIterator;
import java.util.Set;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.CodeException;
import org.apache.bcel.classfile.LineNumber;
import org.apache.bcel.classfile.LineNumberTable;
import org.apache.bcel.classfile.Utility;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.Detector;
import edu.umd.cs.findbugs.SourceLineAnnotation;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.Token;
import edu.umd.cs.findbugs.Tokenizer;
import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.ClassContext;
import edu.umd.cs.findbugs.ba.Hierarchy;
import edu.umd.cs.findbugs.ba.SourceFile;
import edu.umd.cs.findbugs.ba.SourceFinder;
import edu.umd.cs.findbugs.visitclass.PreorderVisitor;
public class DroppedException extends PreorderVisitor implements Detector {
private static final boolean DEBUG = SystemProperties.getBoolean("de.debug");
private static final boolean LOOK_IN_SOURCE_TO_FIND_COMMENTED_CATCH_BLOCKS = SystemProperties
.getBoolean("findbugs.de.comment");
Set<String> reported = new HashSet<String>();
Set<String> causes = new HashSet<String>();
Set<String> checkedCauses = new HashSet<String>();
private BugReporter bugReporter;
private ClassContext classContext;
public DroppedException(BugReporter bugReporter) {
this.bugReporter = bugReporter;
if (DEBUG)
System.out.println("Dropped Exception debugging turned on");
}
public void visitClassContext(ClassContext classContext) {
this.classContext = classContext;
classContext.getJavaClass().accept(this);
}
public void report() {
}
boolean isChecked(String c) {
if (!causes.add(c))
return checkedCauses.contains(c);
try {
if (Hierarchy.isSubtype(c, "java.lang.Exception") && !Hierarchy.isSubtype(c, "java.lang.RuntimeException")) {
checkedCauses.add(c);
return true;
}
} catch (ClassNotFoundException e) {
bugReporter.reportMissingClass(e);
}
return false;
}
private int getUnsignedShort(byte[] a, int i) {
return asUnsignedByte(a[i]) << 8 | asUnsignedByte(a[i + 1]);
}
@Override
public void visit(Code obj) {
CodeException[] exp = obj.getExceptionTable();
LineNumberTable lineNumbers = obj.getLineNumberTable();
if (exp == null)
return;
byte[] code = obj.getCode();
for (CodeException aExp : exp) {
int handled = aExp.getHandlerPC();
int start = aExp.getStartPC();
int end = aExp.getEndPC();
int cause = aExp.getCatchType();
boolean exitInTryBlock = false;
if (DEBUG) {
System.out.println("start = " + start + ", end = " + end + ", codeLength = " + code.length + ", handled = "
+ handled);
}
for (int j = start; j <= end && j < code.length;) {
int opcode = asUnsignedByte(code[j]);
if (NO_OF_OPERANDS[opcode] < 0) {
exitInTryBlock = true;
break;
}
j += 1 + NO_OF_OPERANDS[opcode];
if (opcode >= IRETURN && opcode <= RETURN || opcode >= IFEQ && opcode <= GOTO && (opcode != GOTO || j < end)) {
exitInTryBlock = true;
if (DEBUG)
System.out.println("\texit: " + opcode + " in " + getFullyQualifiedMethodName());
break;
}
}
if (exitInTryBlock) {
if (DEBUG)
System.out.println("Exit in try block");
continue;
}
if (handled < 5)
continue;
String c;
if (cause == 0)
c = "Throwable";
else {
c = Utility.compactClassName(getConstantPool().getConstantString(cause, CONSTANT_Class), false);
if (!isChecked(c))
continue;
}
int jumpAtEnd = 0;
if (end < code.length && asUnsignedByte(code[end]) == GOTO) {
jumpAtEnd = getUnsignedShort(code, end + 1);
if (jumpAtEnd < handled)
jumpAtEnd = 0;
}
int opcode = asUnsignedByte(code[handled]);
int afterHandler = 0;
if (DEBUG)
System.out.println("DE:\topcode is " + opcode + ", " + asUnsignedByte(code[handled + 1]));
boolean drops = false;
boolean startsWithASTORE03 = opcode >= ASTORE_0 && opcode <= ASTORE_3;
if (startsWithASTORE03 && asUnsignedByte(code[handled + 1]) == RETURN) {
if (DEBUG)
System.out.println("Drop 1");
drops = true;
afterHandler = handled + 1;
}
if (handled + 2 < code.length && opcode == ASTORE && asUnsignedByte(code[handled + 2]) == RETURN) {
drops = true;
afterHandler = handled + 2;
if (DEBUG)
System.out.println("Drop 2");
}
if (handled + 3 < code.length && !exitInTryBlock) {
if (DEBUG)
System.out.println("DE: checking for jumps");
if (startsWithASTORE03 && asUnsignedByte(code[handled - 3]) == GOTO) {
int offsetBefore = getUnsignedShort(code, handled - 2);
if (DEBUG)
System.out.println("offset before = " + offsetBefore);
if (offsetBefore == 4) {
drops = true;
afterHandler = handled + 1;
if (DEBUG)
System.out.println("Drop 3");
}
}
if (opcode == ASTORE && asUnsignedByte(code[handled - 3]) == GOTO) {
int offsetBefore = getUnsignedShort(code, handled - 2);
if (offsetBefore == 5) {
drops = true;
afterHandler = handled + 2;
if (DEBUG)
System.out.println("Drop 4");
}
}
if (startsWithASTORE03 && asUnsignedByte(code[handled + 1]) == GOTO && asUnsignedByte(code[handled - 3]) == GOTO) {
int offsetBefore = getUnsignedShort(code, handled - 2);
int offsetAfter = getUnsignedShort(code, handled + 2);
if (offsetAfter > 0 && offsetAfter + 4 == offsetBefore) {
drops = true;
afterHandler = handled + 4;
if (DEBUG)
System.out.println("Drop 5");
}
}
if (opcode == ASTORE && asUnsignedByte(code[handled + 2]) == GOTO && asUnsignedByte(code[handled - 3]) == GOTO) {
int offsetBefore = getUnsignedShort(code, handled - 2);
int offsetAfter = getUnsignedShort(code, handled + 3);
if (offsetAfter > 0 && offsetAfter + 5 == offsetBefore) {
drops = true;
afterHandler = handled + 5;
if (DEBUG)
System.out.println("Drop 6");
}
}
}
boolean multiLineHandler = false;
if (DEBUG)
System.out.println("afterHandler = " + afterHandler + ", handled = " + handled);
if (afterHandler > handled && lineNumbers != null) {
int startHandlerLinenumber = lineNumbers.getSourceLine(handled);
int endHandlerLinenumber = getNextExecutableLineNumber(lineNumbers, afterHandler) - 1;
if (DEBUG)
System.out.println("Handler in lines " + startHandlerLinenumber + "-" + endHandlerLinenumber);
if (endHandlerLinenumber > startHandlerLinenumber) {
multiLineHandler = true;
if (DEBUG)
System.out.println("Multiline handler");
}
}
if (end - start >= 4 && drops && !c.equals("java.lang.InterruptedException")
&& !c.equals("java.lang.CloneNotSupportedException")) {
int priority = NORMAL_PRIORITY;
if (exitInTryBlock)
priority++;
if (end - start == 4)
priority++;
SourceLineAnnotation srcLine = SourceLineAnnotation.fromVisitedInstruction(this.classContext, this, handled);
if (srcLine != null && LOOK_IN_SOURCE_TO_FIND_COMMENTED_CATCH_BLOCKS) {
if (catchBlockHasComment(srcLine))
return;
else
priority++;
} else {
// can't look at source
if (lineNumbers == null || multiLineHandler)
priority += 2;
}
if (c.equals("java.lang.Error") || c.equals("java.lang.Exception") || c.equals("java.lang.Throwable")
|| c.equals("java.lang.RuntimeException")) {
priority--;
if (end - start > 30)
priority--;
}
if (DEBUG) {
System.out.println("Priority is " + priority);
}
if (priority > LOW_PRIORITY)
return;
if (priority < HIGH_PRIORITY)
priority = HIGH_PRIORITY;
if (DEBUG) {
System.out.println("reporting warning");
}
String key = (exitInTryBlock ? "mightDrop," : "mightIgnore,") + getFullyQualifiedMethodName() + "," + c;
if (reported.add(key)) {
BugInstance bugInstance = new BugInstance(this, exitInTryBlock ? "DE_MIGHT_DROP" : "DE_MIGHT_IGNORE",
priority).addClassAndMethod(this);
bugInstance.addClass(c).describe("CLASS_EXCEPTION");
bugInstance.addSourceLine(srcLine);
bugReporter.reportBug(bugInstance);
}
}
}
}
private int getNextExecutableLineNumber(LineNumberTable linenumbers, int PC) {
LineNumber[] entries = linenumbers.getLineNumberTable();
int beforePC = 0;
int i = 0;
for (; i < entries.length && entries[i].getStartPC() < PC; i++) {
int line = entries[i].getLineNumber();
if (line > beforePC)
beforePC = line;
}
if (i < entries.length) {
int secondChoice = entries[i].getLineNumber();
for (; i < entries.length; i++) {
int line = entries[i].getLineNumber();
if (line > beforePC)
return line;
}
return secondChoice;
} else
return entries[entries.length - 1].getLineNumber();
}
private static final int START = 0;
private static final int CATCH = 1;
private static final int OPEN_PAREN = 2;
private static final int CLOSE_PAREN = 3;
private static final int OPEN_BRACE = 4;
/**
* Maximum number of lines we look backwards to find the "catch" keyword.
* Looking backwards is necessary when the indentation style puts the open
* brace on a different line from the catch clause.
*/
private static final int NUM_CONTEXT_LINES = 3;
/**
* The number of lines that we'll scan to look at the source for a catch
* block.
*/
private static final int MAX_LINES = 7;
/**
* Analyze a class's source code to see if there is a comment (or other
* text) in a catch block we have marked as dropping an exception.
*
* @return true if there is a comment in the catch block, false if not (or
* if we can't tell)
*/
private boolean catchBlockHasComment(SourceLineAnnotation srcLine) {
if (!LOOK_IN_SOURCE_TO_FIND_COMMENTED_CATCH_BLOCKS)
return false;
SourceFinder sourceFinder = AnalysisContext.currentAnalysisContext().getSourceFinder();
try {
SourceFile sourceFile = sourceFinder.findSourceFile(srcLine.getPackageName(), srcLine.getSourceFile());
int startLine = srcLine.getStartLine();
int scanStartLine = startLine - NUM_CONTEXT_LINES;
if (scanStartLine < 1)
scanStartLine = 1;
int offset = sourceFile.getLineOffset(scanStartLine - 1);
if (offset < 0)
return false; // Source file has changed?
Tokenizer tokenizer = new Tokenizer(new InputStreamReader(sourceFile.getInputStreamFromOffset(offset)));
// Read the tokens into an ArrayList,
// keeping track of where the catch block is reported
// to start
ArrayList<Token> tokenList = new ArrayList<Token>(40);
int eolOfCatchBlockStart = -1;
for (int line = scanStartLine; line < scanStartLine + MAX_LINES;) {
Token token = tokenizer.next();
int kind = token.getKind();
if (kind == Token.EOF)
break;
if (kind == Token.EOL) {
if (line == startLine)
eolOfCatchBlockStart = tokenList.size();
++line;
}
tokenList.add(token);
}
if (eolOfCatchBlockStart < 0)
return false; // Couldn't scan line reported as start of catch
// block
// Starting at the end of the line reported as the start of the
// catch block,
// scan backwards for the token "catch".
ListIterator<Token> iter = tokenList.listIterator(eolOfCatchBlockStart);
boolean foundCatch = false;
while (iter.hasPrevious()) {
Token token = iter.previous();
if (token.getKind() == Token.WORD && token.getLexeme().equals("catch")) {
foundCatch = true;
break;
}
}
if (!foundCatch)
return false; // Couldn't find "catch" keyword
// Scan forward from the "catch" keyword to see what text
// is in the handler block. If the block is non-empty,
// then we suppress the warning (on the theory that the
// programmer has indicated that there is a good reason
// that the exception is ignored).
boolean done = false;
int numLines = 0;
int state = START;
int level = 0;
do {
if (!iter.hasNext())
break;
Token token = iter.next();
int type = token.getKind();
String value = token.getLexeme();
switch (type) {
case Token.EOL:
if (DEBUG)
System.out.println("Saw token: [EOL]");
++numLines;
if (numLines >= MAX_LINES)
done = true;
break;
default:
if (DEBUG)
System.out.println("Got token: " + value);
switch (state) {
case START:
if (value.equals("catch"))
state = CATCH;
break;
case CATCH:
if (value.equals("("))
state = OPEN_PAREN;
break;
case OPEN_PAREN:
if (value.equals(")")) {
if (level == 0)
state = CLOSE_PAREN;
else
--level;
} else if (value.equals("(")) {
++level;
}
break;
case CLOSE_PAREN:
if (value.equals("{"))
state = OPEN_BRACE;
break;
case OPEN_BRACE:
boolean closeBrace = value.equals("}");
if (DEBUG && !closeBrace)
System.out.println("Found a comment in catch block: " + value);
return !closeBrace;
}
break;
}
} while (!done);
} catch (IOException e) {
// Ignored; we'll just assume there is no comment
}
return false;
}
}
// vim:ts=4