/*
 * 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.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
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.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.FieldInstruction;
import org.apache.bcel.generic.IFNONNULL;
import org.apache.bcel.generic.IFNULL;
import org.apache.bcel.generic.INVOKESTATIC;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.ObjectType;
import org.apache.bcel.generic.Type;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.CallGraph;
import edu.umd.cs.findbugs.CallGraphEdge;
import edu.umd.cs.findbugs.CallGraphNode;
import edu.umd.cs.findbugs.CallSite;
import edu.umd.cs.findbugs.Detector;
import edu.umd.cs.findbugs.IntAnnotation;
import edu.umd.cs.findbugs.Priorities;
import edu.umd.cs.findbugs.SelfCalls;
import edu.umd.cs.findbugs.SourceLineAnnotation;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.CFG;
import edu.umd.cs.findbugs.ba.CFGBuilderException;
import edu.umd.cs.findbugs.ba.ClassContext;
import edu.umd.cs.findbugs.ba.DataflowAnalysisException;
import edu.umd.cs.findbugs.ba.Hierarchy;
import edu.umd.cs.findbugs.ba.InnerClassAccess;
import edu.umd.cs.findbugs.ba.InnerClassAccessMap;
import edu.umd.cs.findbugs.ba.JCIPAnnotationDatabase;
import edu.umd.cs.findbugs.ba.Location;
import edu.umd.cs.findbugs.ba.LockChecker;
import edu.umd.cs.findbugs.ba.LockSet;
import edu.umd.cs.findbugs.ba.SignatureConverter;
import edu.umd.cs.findbugs.ba.XFactory;
import edu.umd.cs.findbugs.ba.XField;
import edu.umd.cs.findbugs.ba.ch.Subtypes2;
import edu.umd.cs.findbugs.ba.type.TopType;
import edu.umd.cs.findbugs.ba.type.TypeDataflow;
import edu.umd.cs.findbugs.ba.type.TypeFrame;
import edu.umd.cs.findbugs.ba.vna.ValueNumber;
import edu.umd.cs.findbugs.ba.vna.ValueNumberDataflow;
import edu.umd.cs.findbugs.ba.vna.ValueNumberFrame;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.DescriptorFactory;
import edu.umd.cs.findbugs.classfile.MethodDescriptor;
import edu.umd.cs.findbugs.props.WarningPropertySet;
import edu.umd.cs.findbugs.util.Util;

/**
 * Find instance fields which are sometimes accessed (read or written) with the
 * receiver lock held and sometimes without. These are candidates to be data
 * races.
 *
 * @author David Hovemeyer
 * @author Bill Pugh
 */
public class FindInconsistentSync2 implements Detector {
    private static final boolean DEBUG = SystemProperties.getBoolean("fis.debug");

    private static final boolean SYNC_ACCESS = true;

    // Boolean.getBoolean("fis.syncAccess");
    private static final boolean ADJUST_SUBCLASS_ACCESSES = !SystemProperties.getBoolean("fis.noAdjustSubclass");

    private static final boolean EVAL = SystemProperties.getBoolean("fis.eval");

    /*
     * ----------------------------------------------------------------------
     * Tuning parameters
     * ----------------------------------------------------------------------
     */

    /**
     * Minimum percent of unbiased field accesses that must be synchronized in
     * order to report a field as inconsistently synchronized. This is meant to
     * distinguish incidental synchronization from intentional synchronization.
     */
    private static final int MIN_SYNC_PERCENT = SystemProperties.getInt("findbugs.fis.minSyncPercent", 50);

    /**
     * Bias that writes are given with respect to reads. The idea is that this
     * should be above 1.0, because unsynchronized writes are more dangerous
     * than unsynchronized reads.
     */
    private static final double WRITE_BIAS = Double.parseDouble(SystemProperties.getProperty("findbugs.fis.writeBias", "2.0"));

    /**
     * Factor which the biased number of unsynchronized accesses is multiplied
     * by. I.e., for factor <i>f</i>, if <i>nUnsync</i> is the biased number of
     * unsynchronized accesses, and <i>nSync</i> is the biased number of
     * synchronized accesses, and
     *
     * <pre>
     *      <i>f</i>(<i>nUnsync</i>) &gt; <i>nSync</i>
     * </pre>
     *
     * then we report a bug. Default value is 2.0, which means that we report a
     * bug if more than 1/3 of accesses are unsynchronized.
     * <p/>
     * <p>
     * Note that <code>MIN_SYNC_PERCENT</code> also influences whether we report
     * a bug: it specifies the minimum unbiased percentage of synchronized
     * accesses.
     */
    private static final double UNSYNC_FACTOR = Double.parseDouble(SystemProperties.getProperty("findbugs.fis.unsyncFactor",
            "1.6"));

    /*
     * ----------------------------------------------------------------------
     * Helper classes
     * ----------------------------------------------------------------------
     */

    private static final int UNLOCKED = 0;

    private static final int LOCKED = 1;

    private static final int READ = 0;

    private static final int WRITE = 2;

    private static final int NULLCHECK = 4;

    private static final int READ_UNLOCKED = READ | UNLOCKED;

    private static final int WRITE_UNLOCKED = WRITE | UNLOCKED;

    private static final int NULLCHECK_UNLOCKED = NULLCHECK | UNLOCKED;

    private static final int READ_LOCKED = READ | LOCKED;

    private static final int WRITE_LOCKED = WRITE | LOCKED;

    private static final int NULLCHECK_LOCKED = NULLCHECK | LOCKED;

    private static class FieldAccess {
        final MethodDescriptor methodDescriptor;

        final int position;

        FieldAccess(MethodDescriptor methodDescriptor, int position) {
            this.methodDescriptor = methodDescriptor;
            this.position = position;
        }

        SourceLineAnnotation asSourceLineAnnotation() {
            return SourceLineAnnotation.fromVisitedInstruction(methodDescriptor, position);
        }

        public static Collection<SourceLineAnnotation> asSourceLineAnnotation(Collection<FieldAccess> c) {
            ArrayList<SourceLineAnnotation> result = new ArrayList<SourceLineAnnotation>(c.size());
            for (FieldAccess f : c)
                result.add(f.asSourceLineAnnotation());
            return result;
        }

    }

    private static ClassDescriptor servlet = DescriptorFactory.createClassDescriptor("javax/servlet/GenericServlet");

    private static ClassDescriptor singleThreadedServlet = DescriptorFactory
            .createClassDescriptor("javax/servlet/SingleThreadModel");

    public static boolean isServletField(XField field) {
        ClassDescriptor classDescriptor = field.getClassDescriptor();

        try {
            Subtypes2 subtypes2 = AnalysisContext.currentAnalysisContext().getSubtypes2();
            if (subtypes2.isSubtype(classDescriptor, servlet) && !subtypes2.isSubtype(classDescriptor, singleThreadedServlet))
                return true;
        } catch (ClassNotFoundException e) {
            assert true;
        }
        if (classDescriptor.getClassName().endsWith("Servlet"))
            return true;
        return false;
    }

    /**
     * The access statistics for a field. Stores the number of locked and
     * unlocked reads and writes, as well as the number of accesses made with a
     * lock held.
     */
    private static class FieldStats {
        private final XField field;

        private final int[] countList = new int[6];

        private int numLocalLocks = 0;

        private int numGetterMethodAccesses = 0;

        private List<FieldAccess> unsyncAccessList = Collections.emptyList();

        private List<FieldAccess> syncAccessList = Collections.emptyList();

        boolean interesting = true;

        final boolean servletField;

        FieldStats(XField field) {
            this.field = field;
            servletField = FindInconsistentSync2.isServletField(field);
        }

        public void addAccess(int kind) {
            countList[kind]++;
        }

        public int getNumAccesses(int kind) {
            return countList[kind];
        }

        public void addLocalLock() {
            numLocalLocks++;
        }

        public int getNumLocalLocks() {
            return numLocalLocks;
        }

        public void addGetterMethodAccess() {
            numGetterMethodAccesses++;
        }

        public int getNumGetterMethodAccesses() {
            return numGetterMethodAccesses;
        }

        public boolean isInteresting() {
            return interesting;
        }

        public boolean isServletField() {
            return servletField;
        }

        public boolean hasAnySynchronizedAccesses() {
            return interesting && !syncAccessList.isEmpty();
        }

        public void addAccess(MethodDescriptor method, InstructionHandle handle, boolean isLocked) {
            if (!interesting)
                return;
            if (!SYNC_ACCESS && isLocked)
                return;

            if (!servletField && !isLocked && syncAccessList.size() == 0 && unsyncAccessList.size() > 6) {
                interesting = false;
                syncAccessList = null;
                unsyncAccessList = null;
                return;
            }
            FieldAccess fa = new FieldAccess(method, handle.getPosition());
            if (isLocked)
                syncAccessList = Util.addTo(syncAccessList, fa);
            else
                unsyncAccessList = Util.addTo(unsyncAccessList, fa);
        }

        public Iterator<SourceLineAnnotation> unsyncAccessIterator() {
            if (!interesting)
                throw new IllegalStateException("Not interesting");
            return FieldAccess.asSourceLineAnnotation(unsyncAccessList).iterator();
        }

        public Iterator<SourceLineAnnotation> syncAccessIterator() {
            if (!interesting)
                throw new IllegalStateException("Not interesting");
            return FieldAccess.asSourceLineAnnotation(syncAccessList).iterator();
        }
    }

    /*
     * ----------------------------------------------------------------------
     * Fields
     * ----------------------------------------------------------------------
     */

    private final BugReporter bugReporter;

    private final Map<XField, FieldStats> statMap = new HashMap<XField, FieldStats>();

    /*
     * ----------------------------------------------------------------------
     * Public methods
     * ----------------------------------------------------------------------
     */

    public FindInconsistentSync2(BugReporter bugReporter) {
        this.bugReporter = bugReporter;
    }

    public void visitClassContext(ClassContext classContext) {
        JavaClass javaClass = classContext.getJavaClass();
        if (DEBUG)
            System.out.println("******** Analyzing class " + javaClass.getClassName());

        // Build self-call graph
        SelfCalls selfCalls = new SelfCalls(classContext) {
            @Override
            public boolean wantCallsFor(Method method) {
                return !method.isPublic();
            }
        };

        Set<Method> lockedMethodSet;
        // Set<Method> publicReachableMethods;
        Set<Method> allMethods = new HashSet<Method>(Arrays.asList(javaClass.getMethods()));

        try {
            selfCalls.execute();
            CallGraph callGraph = selfCalls.getCallGraph();
            if (DEBUG)
                System.out.println("Call graph (not unlocked methods): " + callGraph.getNumVertices() + " nodes, "
                        + callGraph.getNumEdges() + " edges");
            // Find call edges that are obviously locked
            Set<CallSite> obviouslyLockedSites = findObviouslyLockedCallSites(classContext, selfCalls);
            lockedMethodSet = findNotUnlockedMethods(classContext, selfCalls, obviouslyLockedSites);
            lockedMethodSet.retainAll(findLockedMethods(classContext, selfCalls, obviouslyLockedSites));
            // publicReachableMethods = findPublicReachableMethods(classContext,
            // selfCalls);
        } catch (CFGBuilderException e) {
            bugReporter.logError("Error finding locked call sites", e);
            return;
        } catch (DataflowAnalysisException e) {
            bugReporter.logError("Error finding locked call sites", e);
            return;
        }
        
            
        for (Method method : allMethods) {
            if (DEBUG)
                System.out.println("******** considering method " + method.getName());

            if (classContext.getMethodGen(method) == null)
                continue;

            if (method.getName().startsWith("access$"))
                // Ignore inner class access methods;
                // we will treat calls to them as field accesses
                continue;

            String name = method.getName();

            boolean inConstructor = name.equals("<init>") || name.equals("<clinit>")
            || name.equals("readObject") || name.equals("clone") || name.equals("close")
            || name.equals("finalize");

            if (inConstructor)
                continue;

            if (DEBUG)
                System.out.println("******** Analyzing method " + method.getName());

            try {
                analyzeMethod(classContext, method, lockedMethodSet);
            } catch (CFGBuilderException e) {
                bugReporter.logError("Error analyzing method", e);
            } catch (DataflowAnalysisException e) {
                bugReporter.logError("Error analyzing method", e);
            }
        }
        for (Field f : javaClass.getFields())
            if (f.isPrivate()) {
                XField xf = XFactory.getExactXField(classContext.getClassDescriptor().getClassName(), f);
                FieldStats stats = statMap.get(xf);
                if (stats == null)
                    continue;
                if (!stats.isServletField() && !stats.hasAnySynchronizedAccesses())
                    statMap.remove(xf);
            }
    }

    public void report() {
        for (XField xfield : statMap.keySet()) {
            FieldStats stats = statMap.get(xfield);
            if (!stats.interesting)
                continue;
            JCIPAnnotationDatabase jcipAnotationDatabase = AnalysisContext.currentAnalysisContext().getJCIPAnnotationDatabase();
            boolean guardedByThis = "this".equals(jcipAnotationDatabase.getFieldAnnotation(xfield, "GuardedBy"));
            boolean notThreadSafe = jcipAnotationDatabase.hasClassAnnotation(xfield.getClassName(), "NotThreadSafe");
            boolean threadSafe = jcipAnotationDatabase.hasClassAnnotation(xfield.getClassName().replace('/', '.'), "ThreadSafe");
            if (notThreadSafe)
                continue;

            WarningPropertySet<InconsistentSyncWarningProperty> propertySet = new WarningPropertySet<InconsistentSyncWarningProperty>();

            int numReadUnlocked = stats.getNumAccesses(READ_UNLOCKED);
            int numWriteUnlocked = stats.getNumAccesses(WRITE_UNLOCKED);
            int numNullCheckUnlocked = stats.getNumAccesses(NULLCHECK_UNLOCKED);

            int numReadLocked = stats.getNumAccesses(READ_LOCKED);
            int numWriteLocked = stats.getNumAccesses(WRITE_LOCKED);
            int numNullCheckLocked = stats.getNumAccesses(NULLCHECK_LOCKED);

            int extra = 0;
            if (numWriteUnlocked > 0)
                extra = numNullCheckLocked;
            int locked = numReadLocked + numWriteLocked + numNullCheckLocked;
            int biasedLocked = numReadLocked + (int) (WRITE_BIAS * (numWriteLocked + numNullCheckLocked + extra));
            int unlocked = numReadUnlocked + numWriteUnlocked + numNullCheckUnlocked;
            int biasedUnlocked = numReadUnlocked + (int) (WRITE_BIAS * (numWriteUnlocked));
            // int writes = numWriteLocked + numWriteUnlocked;

            if (unlocked == 0) {
                continue;
                // propertySet.addProperty(InconsistentSyncWarningProperty.NEVER_UNLOCKED);
            }

            if (guardedByThis) {
                propertySet.addProperty(InconsistentSyncWarningProperty.ANNOTATED_AS_GUARDED_BY_THIS);

            }

            if (threadSafe) {
                propertySet.addProperty(InconsistentSyncWarningProperty.ANNOTATED_AS_THREAD_SAFE);

            }
            if (!guardedByThis && locked == 0 && !stats.isServletField()) {
                continue;
                // propertySet.addProperty(InconsistentSyncWarningProperty.NEVER_LOCKED);
            }

            if (stats.isServletField() && numWriteLocked == 0 && numWriteUnlocked == 0)
                continue;

            if (DEBUG) {
                System.out.println("IS2: " + xfield);
                if (guardedByThis)
                    System.out.println("Guarded by this");
                System.out.println("  RL: " + numReadLocked);
                System.out.println("  WL: " + numWriteLocked);
                System.out.println("  NL: " + numNullCheckLocked);

                System.out.println("  RU: " + numReadUnlocked);
                System.out.println("  WU: " + numWriteUnlocked);
                System.out.println("  NU: " + numNullCheckUnlocked);
            }
            if (!EVAL && numReadUnlocked > 0 && ((int) (UNSYNC_FACTOR * (biasedUnlocked - 1))) > biasedLocked
                    && !stats.isServletField()) {
                // continue;
                propertySet.addProperty(InconsistentSyncWarningProperty.MANY_BIASED_UNLOCKED);
            }

            // NOTE: we ignore access to public, volatile, and final fields

            if (numWriteUnlocked + numWriteLocked == 0) {
                // No writes outside of constructor
                if (DEBUG)
                    System.out.println("  No writes outside of constructor");
                propertySet.addProperty(InconsistentSyncWarningProperty.NEVER_WRITTEN);
                // continue;
            }

            if (numReadUnlocked + numReadLocked == 0) {
                // No reads outside of constructor
                if (DEBUG)
                    System.out.println("  No reads outside of constructor");
                // continue;
                propertySet.addProperty(InconsistentSyncWarningProperty.NEVER_READ);
            }

            if (stats.getNumLocalLocks() == 0) {
                if (DEBUG)
                    System.out.println("  No local locks");
                // continue;
                propertySet.addProperty(InconsistentSyncWarningProperty.NO_LOCAL_LOCKS);
            }

            int freq, printFreq;
            if (locked + unlocked > 0) {
                freq = (100 * locked) / (locked + unlocked);
                printFreq = (100 * locked) / (locked + unlocked + numNullCheckUnlocked);
            } else {
                printFreq = freq = 0;
            }

            if (freq < MIN_SYNC_PERCENT) {
                // continue;
                propertySet.addProperty(InconsistentSyncWarningProperty.BELOW_MIN_SYNC_PERCENT);
            }
            if (DEBUG)
                System.out.println("  Sync %: " + freq);

            if (stats.getNumGetterMethodAccesses() >= unlocked) {
                // Unlocked accesses are only in getter method(s).
                propertySet.addProperty(InconsistentSyncWarningProperty.ONLY_UNSYNC_IN_GETTERS);
            }

            // At this point, we report the field as being inconsistently
            // synchronized
            if (stats.isServletField())
                propertySet.addProperty(InconsistentSyncWarningProperty.MUTABLE_SERVLET_FIELD);

            BugInstance bugInstance;
            if (stats.isServletField())
                bugInstance = new BugInstance(this, "MSF_MUTABLE_SERVLET_FIELD", Priorities.NORMAL_PRIORITY).addClass(
                        xfield.getClassName()).addField(xfield);
            else
                bugInstance = new BugInstance(this, guardedByThis ? "IS_FIELD_NOT_GUARDED" : "IS2_INCONSISTENT_SYNC",
                        Priorities.NORMAL_PRIORITY).addClass(xfield.getClassName()).addField(xfield).addInt(printFreq)
                        .describe(IntAnnotation.INT_SYNC_PERCENT);

            propertySet.decorateBugInstance(bugInstance);
            // Add source lines for unsynchronized accesses
            for (Iterator<SourceLineAnnotation> j = stats.unsyncAccessIterator(); j.hasNext();) {
                SourceLineAnnotation accessSourceLine = j.next();
                bugInstance.addSourceLine(accessSourceLine).describe("SOURCE_LINE_UNSYNC_ACCESS");
            }

            if (SYNC_ACCESS) {
                // Add source line for synchronized accesses;
                // useful for figuring out what the detector is doing
                for (Iterator<SourceLineAnnotation> j = stats.syncAccessIterator(); j.hasNext();) {
                    SourceLineAnnotation accessSourceLine = j.next();
                    bugInstance.addSourceLine(accessSourceLine).describe("SOURCE_LINE_SYNC_ACCESS");
                }
            }

            if (EVAL) {
                bugInstance.addInt(biasedLocked).describe("INT_BIASED_LOCKED");
                bugInstance.addInt(biasedUnlocked).describe("INT_BIASED_UNLOCKED");
            }

            bugReporter.reportBug(bugInstance);

        }
    }

    /*
     * ----------------------------------------------------------------------
     * Implementation
     * ----------------------------------------------------------------------
     */

    private static boolean isConstructor(String methodName) {
        return methodName.equals("<init>") || methodName.equals("<clinit>") || methodName.equals("readObject")
                || methodName.equals("clone") || methodName.equals("close") || methodName.equals("writeObject")
                || methodName.equals("toString") || methodName.equals("init") || methodName.equals("initialize")
                || methodName.equals("dispose") || methodName.equals("finalize") || methodName.equals("this")
                || methodName.equals("_jspInit") || methodName.equals("_jspDestroy");

    }

    private void analyzeMethod(ClassContext classContext, Method method, Set<Method> lockedMethodSet) throws CFGBuilderException,
            DataflowAnalysisException {

        InnerClassAccessMap icam = AnalysisContext.currentAnalysisContext().getInnerClassAccessMap();
        ConstantPoolGen cpg = classContext.getConstantPoolGen();
        MethodGen methodGen = classContext.getMethodGen(method);
        if (methodGen == null)
            return;
        CFG cfg = classContext.getCFG(method);
        LockChecker lockChecker = classContext.getLockChecker(method);
        ValueNumberDataflow vnaDataflow = classContext.getValueNumberDataflow(method);
        boolean isGetterMethod = isGetterMethod(classContext, method);
        MethodDescriptor methodDescriptor = DescriptorFactory.instance().getMethodDescriptor(classContext.getJavaClass(), method);
        if (DEBUG)
            System.out.println("**** Analyzing method " + SignatureConverter.convertMethodSignature(methodGen));

        for (Iterator<Location> i = cfg.locationIterator(); i.hasNext();) {
            Location location = i.next();
            try {
                Instruction ins = location.getHandle().getInstruction();
                XField xfield = null;
                boolean isWrite = false;
                boolean isLocal = false;
                boolean isNullCheck = false;

                if (ins instanceof FieldInstruction) {
                    InstructionHandle n = location.getHandle().getNext();
                    isNullCheck = n.getInstruction() instanceof IFNONNULL || n.getInstruction() instanceof IFNULL;
                    if (DEBUG && isNullCheck)
                        System.out.println("is null check");
                    FieldInstruction fins = (FieldInstruction) ins;
                    xfield = Hierarchy.findXField(fins, cpg);
                    if (xfield == null) continue;
                    isWrite = ins.getOpcode() == Constants.PUTFIELD;
                    isLocal = fins.getClassName(cpg).equals(classContext.getJavaClass().getClassName());
                    if (DEBUG)
                        System.out.println("Handling field access: " + location.getHandle() + " (frame="
                                + vnaDataflow.getFactAtLocation(location) + ") :" + n);
                } else if (ins instanceof INVOKESTATIC) {
                    INVOKESTATIC inv = (INVOKESTATIC) ins;
                    InnerClassAccess access = icam.getInnerClassAccess(inv, cpg);
                    if (access != null && access.getMethodSignature().equals(inv.getSignature(cpg))) {
                        xfield = access.getField();
                        isWrite = !access.isLoad();
                        isLocal = false;
                        if (DEBUG)
                            System.out.println("Handling inner class access: " + location.getHandle() + " (frame="
                                    + vnaDataflow.getFactAtLocation(location) + ")");
                    }
                }

                if (xfield == null)
                    continue;

                // We only care about mutable nonvolatile nonpublic instance
                // fields.
                if (xfield.isStatic() || xfield.isPublic() || xfield.isVolatile() || xfield.isFinal())
                    continue;

                // The value number frame could be invalid if the basic
                // block became unreachable due to edge pruning (dead code).
                ValueNumberFrame frame = vnaDataflow.getFactAtLocation(location);
                if (!frame.isValid())
                    continue;

                // Get lock set and instance value
                ValueNumber thisValue = !method.isStatic() ? vnaDataflow.getAnalysis().getThisValue() : null;
                LockSet lockSet = lockChecker.getFactAtLocation(location);
                InstructionHandle handle = location.getHandle();
                ValueNumber instance = frame.getInstance(handle.getInstruction(), cpg);
                if (DEBUG) {
                    System.out.println("Lock set: " + lockSet);
                    System.out.println("value number: " + instance.getNumber());
                    System.out.println("Lock count: " + lockSet.getLockCount(instance.getNumber()));
                }

                // Is the instance locked?
                // We consider the access to be locked if either
                // - the object is explicitly locked, or
                // - the field is accessed through the "this" reference,
                // and the method is in the locked method set, or
                // - any value returned by a called method is locked;
                // the (conservative) assumption is that the return lock object
                // is correct for synchronizing the access
                boolean isExplicitlyLocked = lockSet.getLockCount(instance.getNumber()) > 0;
                boolean isAccessedThroughThis = thisValue != null && thisValue.equals(instance);
                boolean isLocked = isExplicitlyLocked
                        || ((isConstructor(method.getName()) || lockedMethodSet.contains(method)) && isAccessedThroughThis)
                        || lockSet.containsReturnValue(vnaDataflow.getAnalysis().getFactory());

                // Adjust the field so its class name is the same
                // as the type of reference it is accessed through.
                // This helps fix false positives produced when a
                // threadsafe class is extended by a subclass that
                // doesn't care about thread safety.
                if (ADJUST_SUBCLASS_ACCESSES) {
                    // Find the type of the object instance
                    TypeDataflow typeDataflow = classContext.getTypeDataflow(method);
                    TypeFrame typeFrame = typeDataflow.getFactAtLocation(location);
                    if (!typeFrame.isValid())
                        continue;
                    Type instanceType = typeFrame.getInstance(handle.getInstruction(), cpg);
                    if (instanceType instanceof TopType) {
                        if (DEBUG)
                            System.out.println("Freaky: typeFrame is " + typeFrame);
                        continue;
                    }
                    // Note: instance type can be Null,
                    // in which case we won't adjust the field type.
                    if (instanceType != TypeFrame.getNullType() && instanceType != TypeFrame.getBottomType()) {
                        if (!(instanceType instanceof ObjectType)) {
                            throw new DataflowAnalysisException("Field accessed through non-object reference " + instanceType,
                                    methodGen, handle);
                        }
                        ObjectType objType = (ObjectType) instanceType;

                        // If instance class name is not the same as that of the
                        // field,
                        // make it so
                        String instanceClassName = objType.getClassName();
                        if (!instanceClassName.equals(xfield.getClassName())) {
                            xfield = XFactory.getExactXField(instanceClassName, xfield.getName(), xfield.getSignature(),
                                    xfield.isStatic());
                        }
                    }
                }

                int kind = 0;
                kind |= isLocked ? LOCKED : UNLOCKED;
                kind |= isWrite ? WRITE : isNullCheck ? NULLCHECK : READ;

                // if (isLocked || !isConstructor(method.getName())) {
                if (DEBUG)
                    System.out.println("IS2:\t" + SignatureConverter.convertMethodSignature(methodGen) + "\t" + xfield + "\t"
                            + ((isWrite ? "W" : "R") + "/" + (isLocked ? "L" : "U")));

                if (!isLocked && methodDescriptor.getClassDescriptor().isAnonymousClass())
                    continue;

                FieldStats stats = getStats(xfield);

                // Don't count a contructor's synchronized access
                // toward the field statistics because it's
                // trivially true and doesn't really represent the
                // programmer's intention
                if (!(isLocked && isConstructor(method.getName()))) {
                    stats.addAccess(kind);
                }

                if (isExplicitlyLocked && isLocal)
                    stats.addLocalLock();

                if (isGetterMethod && !isLocked)
                    stats.addGetterMethodAccess();

                stats.addAccess(methodDescriptor, handle, isLocked);
                // }
            } catch (ClassNotFoundException e) {
                bugReporter.reportMissingClass(e);
            }
        }
    }

    /**
     * Determine whether or not the the given method is a getter method. I.e.,
     * if it just returns the value of an instance field.
     *
     * @param classContext
     *            the ClassContext for the class containing the method
     * @param method
     *            the method
     */
    @SuppressWarnings("unchecked")
    public static boolean isGetterMethod(ClassContext classContext, Method method) {
        MethodGen methodGen = classContext.getMethodGen(method);
        if (methodGen == null)
            return false;
        InstructionList il = methodGen.getInstructionList();
        // System.out.println("Checking getter method: " + method.getName());
        if (il.getLength() > 60)
            return false;

        int count = 0;
        Iterator<InstructionHandle> it = il.iterator();
        while (it.hasNext()) {
            InstructionHandle ih = it.next();
            switch (ih.getInstruction().getOpcode()) {
            case Constants.GETFIELD:
                count++;
                if (count > 1)
                    return false;
                break;
            case Constants.PUTFIELD:
            case Constants.BALOAD:
            case Constants.CALOAD:
            case Constants.DALOAD:
            case Constants.FALOAD:
            case Constants.IALOAD:
            case Constants.LALOAD:
            case Constants.SALOAD:
            case Constants.AALOAD:
            case Constants.BASTORE:
            case Constants.CASTORE:
            case Constants.DASTORE:
            case Constants.FASTORE:
            case Constants.IASTORE:
            case Constants.LASTORE:
            case Constants.SASTORE:
            case Constants.AASTORE:
            case Constants.PUTSTATIC:
                return false;
            case Constants.INVOKESTATIC:
            case Constants.INVOKEVIRTUAL:
            case Constants.INVOKEINTERFACE:
            case Constants.INVOKESPECIAL:
            case Constants.GETSTATIC:
                // no-op

            }
        }
        // System.out.println("Found getter method: " + method.getName());
        return true;
    }

    /**
     * Get the access statistics for given field.
     */
    private FieldStats getStats(XField field) {
        FieldStats stats = statMap.get(field);
        if (stats == null) {
            stats = new FieldStats(field);
            statMap.put(field, stats);
        }
        return stats;
    }

    /**
     * Find methods that appear to never be called from an unlocked context We
     * assume that nonpublic methods will only be called from within the class,
     * which is not really a valid assumption.
     */
    private Set<Method> findNotUnlockedMethods(ClassContext classContext, SelfCalls selfCalls, Set<CallSite> obviouslyLockedSites)
            {

        JavaClass javaClass = classContext.getJavaClass();
        Method[] methodList = javaClass.getMethods();

        CallGraph callGraph = selfCalls.getCallGraph();

        // Initially, assume no methods are called from an
        // unlocked context
        Set<Method> lockedMethodSet = new HashSet<Method>();
        lockedMethodSet.addAll(Arrays.asList(methodList));

        // Assume all public methods are called from
        // unlocked context
        for (Method method : methodList) {
            if (method.isPublic() && !isConstructor(method.getName())) {
                lockedMethodSet.remove(method);
            }
        }

        // Explore the self-call graph to find nonpublic methods
        // that can be called from an unlocked context.
        boolean change;
        do {
            change = false;

            for (Iterator<CallGraphEdge> i = callGraph.edgeIterator(); i.hasNext();) {
                CallGraphEdge edge = i.next();
                CallSite callSite = edge.getCallSite();

                // Ignore obviously locked edges
                if (obviouslyLockedSites.contains(callSite))
                    continue;

                // If the calling method is locked, ignore the edge
                if (lockedMethodSet.contains(callSite.getMethod()))
                    continue;

                // Calling method is unlocked, so the called method
                // is also unlocked.
                CallGraphNode target = edge.getTarget();
                if (lockedMethodSet.remove(target.getMethod()))
                    change = true;
            }
        } while (change);

        if (DEBUG) {
            System.out.println("Apparently not unlocked methods:");
            for (Method method : lockedMethodSet) {
                System.out.println("\t" + method.getName());
            }
        }

        // We assume that any methods left in the locked set
        // are called only from a locked context.
        return lockedMethodSet;
    }

    /**
     * Find methods that appear to always be called from a locked context. We
     * assume that nonpublic methods will only be called from within the class,
     * which is not really a valid assumption.
     */
    private Set<Method> findLockedMethods(ClassContext classContext, SelfCalls selfCalls, Set<CallSite> obviouslyLockedSites)
             {

        JavaClass javaClass = classContext.getJavaClass();
        Method[] methodList = javaClass.getMethods();

        CallGraph callGraph = selfCalls.getCallGraph();

        // Initially, assume all methods are locked
        Set<Method> lockedMethodSet = new HashSet<Method>();

        // Assume all public methods are unlocked
        for (Method method : methodList) {
            if (method.isSynchronized()) {
                lockedMethodSet.add(method);
            }
        }

        // Explore the self-call graph to find nonpublic methods
        // that can be called from an unlocked context.
        boolean change;
        do {
            change = false;

            for (Iterator<CallGraphEdge> i = callGraph.edgeIterator(); i.hasNext();) {
                CallGraphEdge edge = i.next();
                CallSite callSite = edge.getCallSite();

                if (obviouslyLockedSites.contains(callSite) || lockedMethodSet.contains(callSite.getMethod())) {
                    // Calling method is locked, so the called method
                    // is also locked.
                    CallGraphNode target = edge.getTarget();
                    if (lockedMethodSet.add(target.getMethod()))
                        change = true;
                }
            }
        } while (change);

        if (DEBUG) {
            System.out.println("Apparently locked methods:");
            for (Method method : lockedMethodSet) {
                System.out.println("\t" + method.getName());
            }
        }

        // We assume that any methods left in the locked set
        // are called only from a locked context.
        return lockedMethodSet;
    }

    /**
     * Find methods that do not appear to be reachable from public methods. Such
     * methods will not be analyzed.
     */
    /*
     * private Set<Method> findPublicReachableMethods(ClassContext classContext,
     * SelfCalls selfCalls) throws CFGBuilderException,
     * DataflowAnalysisException {
     *
     * JavaClass javaClass = classContext.getJavaClass(); Method[] methodList =
     * javaClass.getMethods();
     *
     * CallGraph callGraph = selfCalls.getCallGraph();
     *
     * // Initially, assume all methods are locked Set<Method>
     * publicReachableMethodSet = new HashSet<Method>();
     *
     * // Assume all public methods are unlocked for (Method method :
     * methodList) { if (method.isPublic() && !isConstructor(method.getName()))
     * { publicReachableMethodSet.add(method); } }
     *
     * // Explore the self-call graph to find nonpublic methods // that can be
     * called from an unlocked context. boolean change; do { change = false;
     *
     * for (Iterator<CallGraphEdge> i = callGraph.edgeIterator(); i.hasNext();)
     * { CallGraphEdge edge = i.next(); CallSite callSite = edge.getCallSite();
     *
     * // Ignore obviously locked edges // If the calling method is locked,
     * ignore the edge if
     * (publicReachableMethodSet.contains(callSite.getMethod())) { // Calling
     * method is reachable, so the called method // is also reachable.
     * CallGraphNode target = edge.getTarget(); if
     * (publicReachableMethodSet.add(target.getMethod())) change = true; } } }
     * while (change);
     *
     * if (DEBUG) { System.out.println(
     * "Methods apparently reachable from public non-constructor methods:"); for
     * (Method method : publicReachableMethodSet) { System.out.println("\t" +
     * method.getName()); } }
     *
     * return publicReachableMethodSet; }
     */

    /**
     * Find all self-call sites that are obviously locked.
     */
    private Set<CallSite> findObviouslyLockedCallSites(ClassContext classContext, SelfCalls selfCalls)
            throws CFGBuilderException, DataflowAnalysisException {
        ConstantPoolGen cpg = classContext.getConstantPoolGen();

        // Find all obviously locked call sites
        Set<CallSite> obviouslyLockedSites = new HashSet<CallSite>();
        for (Iterator<CallSite> i = selfCalls.callSiteIterator(); i.hasNext();) {
            CallSite callSite = i.next();
            Method method = callSite.getMethod();
            Location location = callSite.getLocation();
            InstructionHandle handle = location.getHandle();

            // Only instance method calls qualify as candidates for
            // "obviously locked"
            Instruction ins = handle.getInstruction();
            if (ins.getOpcode() == Constants.INVOKESTATIC)
                continue;

            // Get lock set for site
            LockChecker lockChecker = classContext.getLockChecker(method);
            LockSet lockSet = lockChecker.getFactAtLocation(location);

            // Get value number frame for site
            ValueNumberDataflow vnaDataflow = classContext.getValueNumberDataflow(method);
            ValueNumberFrame frame = vnaDataflow.getFactAtLocation(location);

            // NOTE: if the CFG on which the value number analysis was performed
            // was pruned, there may be unreachable instructions. Therefore,
            // we can't assume the frame is valid.
            if (!frame.isValid())
                continue;

            // Find the ValueNumber of the receiver object
            int numConsumed = ins.consumeStack(cpg);
            MethodGen methodGen = classContext.getMethodGen(method);
            assert methodGen != null;
            if (numConsumed == Constants.UNPREDICTABLE)
                throw new DataflowAnalysisException("Unpredictable stack consumption", methodGen, handle);
            // if (DEBUG) System.out.println("Getting receiver for frame: " +
            // frame);
            ValueNumber instance = frame.getStackValue(numConsumed - 1);

            // Is the instance locked?
            int lockCount = lockSet.getLockCount(instance.getNumber());
            if (lockCount > 0) {
                // This is a locked call site
                obviouslyLockedSites.add(callSite);
            }
        }

        return obviouslyLockedSites;
    }
}

// vim:ts=3
