/*
 * 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("	exit: " + 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:	opcode 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 >= 5 && drops && !c.equals("java.lang.InterruptedException")
					&& !c.equals("java.lang.CloneNotSupportedException")) {
				int priority = NORMAL_PRIORITY;
				if (exitInTryBlock) 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
