/*
 * FindBugs - Find bugs in Java programs
 * Copyright (C) 2004 Brian Goetz <briangoetz@users.sourceforge.net>
 * Copyright (C) 2004 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.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.CodeException;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ASTORE;
import org.apache.bcel.generic.InstructionHandle;

import edu.umd.cs.findbugs.BugAccumulator;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.OpcodeStack;
import edu.umd.cs.findbugs.SourceLineAnnotation;
import edu.umd.cs.findbugs.StatelessDetector;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.ba.BasicBlock;
import edu.umd.cs.findbugs.ba.CFG;
import edu.umd.cs.findbugs.ba.CFGBuilderException;
import edu.umd.cs.findbugs.ba.DataflowAnalysisException;
import edu.umd.cs.findbugs.ba.Hierarchy2;
import edu.umd.cs.findbugs.ba.LiveLocalStoreDataflow;
import edu.umd.cs.findbugs.ba.Location;
import edu.umd.cs.findbugs.ba.MethodUnprofitableException;
import edu.umd.cs.findbugs.ba.SignatureConverter;
import edu.umd.cs.findbugs.ba.XClass;
import edu.umd.cs.findbugs.ba.XMethod;
import edu.umd.cs.findbugs.bcel.OpcodeStackDetector;
import edu.umd.cs.findbugs.classfile.CheckedAnalysisException;
import edu.umd.cs.findbugs.classfile.DescriptorFactory;
import edu.umd.cs.findbugs.classfile.Global;
import edu.umd.cs.findbugs.classfile.MissingClassException;
import edu.umd.cs.findbugs.internalAnnotations.DottedClassName;
import edu.umd.cs.findbugs.util.ClassName;

/**
 * RuntimeExceptionCapture
 *
 * @author Brian Goetz
 * @author Bill Pugh
 * @author David Hovemeyer
 */
public class RuntimeExceptionCapture extends OpcodeStackDetector implements StatelessDetector {
    private static final boolean DEBUG = SystemProperties.getBoolean("rec.debug");

    private final BugReporter bugReporter;

    private final List<ExceptionCaught> catchList = new ArrayList<ExceptionCaught>();

    private final List<ExceptionThrown> throwList = new ArrayList<ExceptionThrown>();

    private final BugAccumulator accumulator;

    private static class ExceptionCaught {
        public String exceptionClass;

        public int startOffset, endOffset, sourcePC;

        public boolean seen = false;

        public boolean dead = false;

        public ExceptionCaught(String exceptionClass, int startOffset, int endOffset, int sourcePC) {
            this.exceptionClass = exceptionClass;
            this.startOffset = startOffset;
            this.endOffset = endOffset;
            this.sourcePC = sourcePC;
        }
    }

    private static class ExceptionThrown {
        public @DottedClassName
        String exceptionClass;

        public int offset;

        public ExceptionThrown(@DottedClassName String exceptionClass, int offset) {
            this.exceptionClass = exceptionClass;
            this.offset = offset;
        }
    }

    public RuntimeExceptionCapture(BugReporter bugReporter) {
        this.bugReporter = bugReporter;
        accumulator = new BugAccumulator(bugReporter);
    }

    @Override
    public void visitJavaClass(JavaClass c) {
        super.visitJavaClass(c);
        accumulator.reportAccumulatedBugs();
    }

    @Override
    public void visitAfter(Code obj) {
        for (ExceptionCaught caughtException : catchList) {
            Set<String> thrownSet = new HashSet<String>();
            for (ExceptionThrown thrownException : throwList) {
                if (thrownException.offset >= caughtException.startOffset && thrownException.offset < caughtException.endOffset) {
                    thrownSet.add(thrownException.exceptionClass);
                    if (thrownException.exceptionClass.equals(caughtException.exceptionClass)) {
                        caughtException.seen = true;
                    }
                }
            }
            int catchClauses = 0;
            if ("java.lang.Exception".equals(caughtException.exceptionClass) && !caughtException.seen) {
                // Now we have a case where Exception is caught, but not thrown
                boolean rteCaught = false;
                for (ExceptionCaught otherException : catchList) {
                    if (otherException.startOffset == caughtException.startOffset
                            && otherException.endOffset == caughtException.endOffset) {
                        catchClauses++;
                        if ("java.lang.RuntimeException".equals(otherException.exceptionClass)) {
                            rteCaught = true;
                        }
                    }
                }
                int range = caughtException.endOffset - caughtException.startOffset;
                if (!rteCaught) {
                    int priority = LOW_PRIORITY + 1;
                    if (range > 300) {
                        priority--;
                    } else if (range < 30) {
                        priority++;
                    }
                    if (catchClauses > 1) {
                        priority++;
                    }
                    if (thrownSet.size() > 1) {
                        priority--;
                    }
                    if (caughtException.dead) {
                        priority--;
                    }
                    accumulator.accumulateBug(new BugInstance(this, "REC_CATCH_EXCEPTION", priority).addClassAndMethod(this),
                            SourceLineAnnotation.fromVisitedInstruction(getClassContext(), this, caughtException.sourcePC));
                }
            }
        }
        catchList.clear();
        throwList.clear();
    }

    @Override
    public void visit(CodeException obj) {
        try {
            super.visit(obj);
            int type = obj.getCatchType();
            if (type == 0) {
                return;
            }
            String name = getConstantPool().constantToString(getConstantPool().getConstant(type));

            ExceptionCaught caughtException = new ExceptionCaught(name, obj.getStartPC(), obj.getEndPC(), obj.getHandlerPC());
            catchList.add(caughtException);

            // See if the store that saves the exception object
            // is alive or dead. We rely on the fact that javac
            // always (?) emits an ASTORE instruction to save
            // the caught exception.
            LiveLocalStoreDataflow dataflow = getClassContext().getLiveLocalStoreDataflow(getMethod());
            CFG cfg = getClassContext().getCFG(getMethod());
            Collection<BasicBlock> blockList = cfg.getBlocksContainingInstructionWithOffset(obj.getHandlerPC());
            for (BasicBlock block : blockList) {
                InstructionHandle first = block.getFirstInstruction();
                if (first != null && first.getPosition() == obj.getHandlerPC() && first.getInstruction() instanceof ASTORE) {
                    ASTORE astore = (ASTORE) first.getInstruction();
                    BitSet liveStoreSet = dataflow.getFactAtLocation(new Location(first, block));
                    if (!liveStoreSet.get(astore.getIndex())) {
                        // The ASTORE storing the exception object is dead
                        if (DEBUG) {
                            System.out.println("Dead exception store at " + first);
                        }
                        caughtException.dead = true;
                        break;
                    }
                }
            }
        } catch (MethodUnprofitableException e) {
            Method m = getMethod();
            bugReporter.reportSkippedAnalysis(DescriptorFactory.instance().getMethodDescriptor(getClassName(), getMethodName(),
                    getMethodSig(), m.isStatic()));
        } catch (DataflowAnalysisException e) {
            bugReporter.logError("Error checking for dead exception store", e);
        } catch (CFGBuilderException e) {
            bugReporter.logError("Error checking for dead exception store", e);
        }
    }

    @Override
    public void sawOpcode(int seen) {
        switch (seen) {
        case ATHROW:
            if (stack.getStackDepth() > 0) {
                OpcodeStack.Item item = stack.getStackItem(0);
                String signature = item.getSignature();
                if (signature != null && signature.length() > 0) {
                    if (signature.startsWith("L")) {
                        signature = SignatureConverter.convert(signature);
                    } else {
                        signature = signature.replace('/', '.');
                    }
                    throwList.add(new ExceptionThrown(signature, getPC()));
                }
            }
            break;

        case INVOKEVIRTUAL:
        case INVOKESPECIAL:
        case INVOKESTATIC:
            String className = getClassConstantOperand();
            if (!className.startsWith("[")) {
                try {
                    XClass c = Global.getAnalysisCache().getClassAnalysis(XClass.class,
                            DescriptorFactory.createClassDescriptor(className));
                    XMethod m = Hierarchy2.findInvocationLeastUpperBound(c, getNameConstantOperand(), getSigConstantOperand(),
                            seen == INVOKESTATIC, seen == INVOKEINTERFACE);
                    if (m == null) {
                        break;
                    }
                    String[] exceptions = m.getThrownExceptions();
                    if (exceptions != null) {
                        for (String name : exceptions) {
                            throwList.add(new ExceptionThrown(ClassName.toDottedClassName(name), getPC()));
                        }
                    }
                } catch (MissingClassException e) {
                    bugReporter.reportMissingClass(e.getClassDescriptor());
                } catch (CheckedAnalysisException e) {
                    bugReporter.logError("Error looking up " + className, e);
                }
            }
            break;
        default:
            break;
        }

    }

}

