/*
 * 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.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.CheckForNull;

import org.apache.bcel.Repository;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.Signature;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.ClassAnnotation;
import edu.umd.cs.findbugs.Lookup;
import edu.umd.cs.findbugs.MethodAnnotation;
import edu.umd.cs.findbugs.OpcodeStack;
import edu.umd.cs.findbugs.Priorities;
import edu.umd.cs.findbugs.StatelessDetector;
import edu.umd.cs.findbugs.TypeAnnotation;
import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.EqualsKindSummary;
import edu.umd.cs.findbugs.ba.XClass;
import edu.umd.cs.findbugs.ba.XMethod;
import edu.umd.cs.findbugs.ba.ch.Subtypes2;
import edu.umd.cs.findbugs.bcel.BCELUtil;
import edu.umd.cs.findbugs.bcel.OpcodeStackDetector;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.DescriptorFactory;
import edu.umd.cs.findbugs.internalAnnotations.DottedClassName;
import edu.umd.cs.findbugs.util.ClassName;
import edu.umd.cs.findbugs.visitclass.PreorderVisitor;

public class FindHEmismatch extends OpcodeStackDetector implements StatelessDetector {
    boolean hasFields = false;

    boolean visibleOutsidePackage = false;

    boolean hasHashCode = false;

    boolean hasEqualsObject = false;

    boolean hashCodeIsAbstract = false;

    boolean equalsObjectIsAbstract = false;

    boolean equalsMethodIsInstanceOfEquals = false;

    boolean equalsReimplementesObjectEquals = false;

    boolean hasCompareToObject = false;

    boolean hasCompareToBridgeMethod = false;

    boolean hasEqualsSelf = false;

    boolean hasEqualsOther = false;

    boolean hasCompareToSelf = false;

    boolean extendsObject = false;

    MethodAnnotation equalsMethod = null;

    MethodAnnotation equalsOtherMethod = null;

    ClassDescriptor equalsOtherClass = null;

    MethodAnnotation compareToMethod = null;

    MethodAnnotation compareToObjectMethod = null;

    MethodAnnotation compareToSelfMethod = null;

    MethodAnnotation hashCodeMethod = null;

    HashSet<String> nonHashableClasses = new HashSet<String>();

    public boolean isHashableClassName(String dottedClassName) {
        return !nonHashableClasses.contains(dottedClassName);
    }

    Map<String, BugInstance> potentialBugs = new HashMap<String, BugInstance>();

    private BugReporter bugReporter;

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

    @Override
    public void visitAfter(JavaClass obj) {
        if (!obj.isClass())
            return;
        if (getDottedClassName().equals("java.lang.Object"))
            return;
        int accessFlags = obj.getAccessFlags();
        if ((accessFlags & ACC_INTERFACE) != 0)
            return;
        visibleOutsidePackage = obj.isPublic() || obj.isProtected();

        String whereEqual = getDottedClassName();
        boolean inheritedHashCodeIsFinal = false;
        boolean inheritedEqualsIsFinal = false;
        boolean inheritedEqualsIsAbstract = false;
        boolean inheritedEqualsFromAbstractClass = false;
        XMethod inheritedEquals = null;
        if (!hasEqualsObject) {
            XClass we = Lookup.findImplementor(getXClass(), "equals", "(Ljava/lang/Object;)Z", false, bugReporter);
            if (we == null || we.equals(getXClass())) {
                whereEqual = "java.lang.Object";
            } else {
                inheritedEqualsFromAbstractClass = we.isAbstract();
                whereEqual = we.getClassDescriptor().getDottedClassName();
                inheritedEquals = we.findMethod("equals", "(Ljava/lang/Object;)Z", false);
                if (inheritedEquals != null) {
                    inheritedEqualsIsFinal = inheritedEquals.isFinal();
                    inheritedEqualsIsAbstract = inheritedEquals.isAbstract();
                }
            }
        }
        boolean usesDefaultEquals = whereEqual.equals("java.lang.Object");
        String whereHashCode = getDottedClassName();
        if (!hasHashCode) {
            XClass wh = Lookup.findSuperImplementor(getXClass(), "hashCode", "()I", false, bugReporter);
            if (wh == null) {
                whereHashCode = "java.lang.Object";
            } else {
                whereHashCode = wh.getClassDescriptor().getDottedClassName();
                XMethod m = wh.findMethod("hashCode", "()I", false);
                if (m != null && m.isFinal())
                    inheritedHashCodeIsFinal = true;
            }
        }
        boolean usesDefaultHashCode = whereHashCode.equals("java.lang.Object");
        if (false && (usesDefaultEquals || usesDefaultHashCode)) {
            try {
                if (Repository.implementationOf(obj, "java/util/Set") || Repository.implementationOf(obj, "java/util/List")
                        || Repository.implementationOf(obj, "java/util/Map")) {
                    // System.out.println(getDottedClassName() + " uses default
                    // hashCode or equals");
                }
            } catch (ClassNotFoundException e) {
                // e.printStackTrace();
            }
        }

        if (!hasEqualsObject && !hasEqualsSelf && hasEqualsOther) {
            BugInstance bug = new BugInstance(this, usesDefaultEquals ? "EQ_OTHER_USE_OBJECT" : "EQ_OTHER_NO_OBJECT",
                    NORMAL_PRIORITY).addClass(this).addMethod(equalsOtherMethod).addClass(equalsOtherClass);
            bugReporter.reportBug(bug);
        }
        if (!hasEqualsObject && hasEqualsSelf) {

            if (usesDefaultEquals) {
                int priority = HIGH_PRIORITY;
                if (usesDefaultHashCode || obj.isAbstract())
                    priority++;
                if (!visibleOutsidePackage)
                    priority++;
                String bugPattern = "EQ_SELF_USE_OBJECT";

                BugInstance bug = new BugInstance(this, bugPattern, priority).addClass(getDottedClassName());
                if (equalsMethod != null)
                    bug.addMethod(equalsMethod);
                bugReporter.reportBug(bug);
            } else {
                int priority = NORMAL_PRIORITY;
                if (hasFields)
                    priority--;
                if (obj.isAbstract())
                    priority++;
                String bugPattern = "EQ_SELF_NO_OBJECT";
                String superclassName = obj.getSuperclassName();
                if (superclassName.equals("java.lang.Enum")) {
                    bugPattern = "EQ_DONT_DEFINE_EQUALS_FOR_ENUM";
                    priority = HIGH_PRIORITY;
                }
                BugInstance bug = new BugInstance(this, bugPattern, priority).addClass(getDottedClassName());
                if (equalsMethod != null)
                    bug.addMethod(equalsMethod);
                bugReporter.reportBug(bug);
            }
        }

        // System.out.println("Class " + getDottedClassName());
        // System.out.println("usesDefaultEquals: " + usesDefaultEquals);
        // System.out.println("hasHashCode: : " + hasHashCode);
        // System.out.println("usesDefaultHashCode: " + usesDefaultHashCode);
        // System.out.println("hasEquals: : " + hasEqualsObject);
        // System.out.println("hasCompareToObject: : " + hasCompareToObject);
        // System.out.println("hasCompareToSelf: : " + hasCompareToSelf);

        if ((hasCompareToObject || hasCompareToSelf) && usesDefaultEquals) {
            BugInstance bug = new BugInstance(this, "EQ_COMPARETO_USE_OBJECT_EQUALS", obj.isAbstract() ? Priorities.LOW_PRIORITY
                    : Priorities.NORMAL_PRIORITY).addClass(this);
            if (compareToSelfMethod != null)
                bug.addMethod(compareToSelfMethod);
            else
                bug.addMethod(compareToObjectMethod);
            bugReporter.reportBug(bug);
        }
        if (!hasCompareToObject && !hasCompareToBridgeMethod && hasCompareToSelf) {
            if (!extendsObject)
                bugReporter.reportBug(new BugInstance(this, "CO_SELF_NO_OBJECT", NORMAL_PRIORITY).addClass(getDottedClassName())
                        .addMethod(compareToMethod));
        }

        // if (!hasFields) return;
        if (hasHashCode && !hashCodeIsAbstract && !(hasEqualsObject && !equalsReimplementesObjectEquals || hasEqualsSelf)) {
            int priority = LOW_PRIORITY;
            if (usesDefaultEquals)
                bugReporter.reportBug(new BugInstance(this, "HE_HASHCODE_USE_OBJECT_EQUALS", priority).addClass(
                        getDottedClassName()).addMethod(hashCodeMethod));
            else if (!inheritedEqualsIsFinal)
                bugReporter.reportBug(new BugInstance(this, "HE_HASHCODE_NO_EQUALS", priority).addClass(getDottedClassName())
                        .addMethod(hashCodeMethod));
        }
        if (equalsObjectIsAbstract) {
            // no errors reported
        } else if (!hasHashCode && (hasEqualsObject || hasEqualsSelf)) {
            EqualsKindSummary.KindOfEquals equalsKind = AnalysisContext.currentAnalysisContext().getEqualsKindSummary()
                    .get(new ClassAnnotation(obj.getClassName()));
            if (equalsKind == EqualsKindSummary.KindOfEquals.ALWAYS_FALSE)
                return;
            if (usesDefaultHashCode) {
                int priority = HIGH_PRIORITY;
                if (equalsMethodIsInstanceOfEquals)
                    priority += 2;
                else if (obj.isAbstract() || !hasEqualsObject)
                    priority++;
                if (priority == HIGH_PRIORITY)
                    nonHashableClasses.add(getDottedClassName());
                if (!visibleOutsidePackage) {
                    priority++;
                }
                BugInstance bug = new BugInstance(this, "HE_EQUALS_USE_HASHCODE", priority).addClass(getDottedClassName());
                if (equalsMethod != null)
                    bug.addMethod(equalsMethod);
                bugReporter.reportBug(bug);
            } else if (!inheritedHashCodeIsFinal && !whereHashCode.startsWith("java.util.Abstract")) {
                int priority = LOW_PRIORITY;

                if (hasEqualsObject && inheritedEqualsIsAbstract)
                    priority++;
                if (hasFields)
                    priority--;
                if (equalsMethodIsInstanceOfEquals || !hasEqualsObject)
                    priority += 2;
                else if (obj.isAbstract())
                    priority++;
                BugInstance bug = new BugInstance(this, "HE_EQUALS_NO_HASHCODE", priority).addClass(getDottedClassName());
                if (equalsMethod != null)
                    bug.addMethod(equalsMethod);
                bugReporter.reportBug(bug);
            }
        }
        if (!hasHashCode && !hasEqualsObject && !hasEqualsSelf && !usesDefaultEquals && usesDefaultHashCode && !obj.isAbstract()
                && inheritedEqualsFromAbstractClass) {
            BugInstance bug = new BugInstance(this, "HE_INHERITS_EQUALS_USE_HASHCODE", NORMAL_PRIORITY)
                    .addClass(getDottedClassName());
            if (equalsMethod != null)
                bug.addMethod(equalsMethod);
            bugReporter.reportBug(bug);
        }
        if (!hasEqualsObject && !hasEqualsSelf && !usesDefaultEquals && !obj.isAbstract() && hasFields && inheritedEquals != null
                && !inheritedEqualsIsFinal && !inheritedEqualsFromAbstractClass
                && !inheritedEquals.getClassDescriptor().getSimpleName().contains("Abstract")
                && !inheritedEquals.getClassDescriptor().getClassName().equals("java/lang/Enum")) {

            BugInstance bug = new BugInstance(this, "EQ_DOESNT_OVERRIDE_EQUALS", NORMAL_PRIORITY).addClass(this)
                    .addMethod(inheritedEquals).describe(MethodAnnotation.METHOD_DID_YOU_MEAN_TO_OVERRIDE);
            bugReporter.reportBug(bug);
        }
    }

    @Override
    public void visit(JavaClass obj) {
        extendsObject = getDottedSuperclassName().equals("java.lang.Object");
        hasFields = false;
        hasHashCode = false;
        hasCompareToObject = false;
        hasCompareToBridgeMethod = false;
        hasCompareToSelf = false;
        hasEqualsObject = false;
        hasEqualsSelf = false;
        hasEqualsOther = false;
        hashCodeIsAbstract = false;
        equalsObjectIsAbstract = false;
        equalsMethodIsInstanceOfEquals = false;
        equalsMethod = null;
        equalsOtherMethod = null;
        compareToMethod = null;
        compareToSelfMethod = null;
        compareToObjectMethod = null;
        hashCodeMethod = null;
        equalsOtherClass = null;
        isApplicationClass = AnalysisContext.currentAnalysisContext().isApplicationClass(obj);
    }
    
    @Override
    public boolean shouldVisitCode(Code obj) {
        if (isApplicationClass)
            return true;
        String name = getMethod().getName();
        if (name.equals("hashCode") || name.equals("equals"))
            return true;
        return false;
        
    }
    boolean isApplicationClass;

    public static int opcode(byte code[], int offset) {
        return code[offset] & 0xff;
    }

    @Override
    public void visit(Field obj) {
        int accessFlags = obj.getAccessFlags();
        if ((accessFlags & ACC_STATIC) != 0)
            return;
        if (!obj.getName().startsWith("this$") && !BCELUtil.isSynthetic(obj) && !obj.isTransient())
            hasFields = true;
    }

    static final Pattern predicateOverAnInstance = Pattern.compile("\\(L([^;]+);\\)Z");

    @Override
    public void visit(Method obj) {

        int accessFlags = obj.getAccessFlags();
        if ((accessFlags & ACC_STATIC) != 0)
            return;
        String name = obj.getName();
        String sig = obj.getSignature();
        if ((accessFlags & ACC_ABSTRACT) != 0) {
            if (name.equals("equals") && sig.equals("(L" + getClassName() + ";)Z")) {
                bugReporter.reportBug(new BugInstance(this, "EQ_ABSTRACT_SELF", LOW_PRIORITY).addClass(getDottedClassName()));
                return;
            } else if (name.equals("compareTo") && sig.equals("(L" + getClassName() + ";)I")) {
                bugReporter.reportBug(new BugInstance(this, "CO_ABSTRACT_SELF", LOW_PRIORITY).addClass(getDottedClassName()));
                return;
            }
        }
        boolean sigIsObject = sig.equals("(Ljava/lang/Object;)Z");
        if (name.equals("hashCode") && sig.equals("()I")) {
            hasHashCode = true;
            if (obj.isAbstract())
                hashCodeIsAbstract = true;
            hashCodeMethod = MethodAnnotation.fromVisitedMethod(this);
            // System.out.println("Found hashCode for " + betterClassName);
        } else if (obj.isPublic() && name.equals("equals")) {
            Matcher m = predicateOverAnInstance.matcher(sig);
            if (m.matches()) {
                if (sigIsObject) {
                    equalsMethod = MethodAnnotation.fromVisitedMethod(this);
                    hasEqualsObject = true;
                    if (obj.isAbstract())
                        equalsObjectIsAbstract = true;
                    else if (!obj.isNative()) {
                        Code code = obj.getCode();
                        byte[] codeBytes = code.getCode();
                        if (codeBytes.length == 9) {
                            int op0 = opcode(codeBytes, 0);
                            int op1 = opcode(codeBytes, 1);
                            int op2 = opcode(codeBytes, 2);
                            int op5 = opcode(codeBytes, 5);
                            int op6 = opcode(codeBytes, 6);
                            int op7 = opcode(codeBytes, 7);
                            int op8 = opcode(codeBytes, 8);
                            if ((op0 == ALOAD_0 && op1 == ALOAD_1 || op0 == ALOAD_1 && op1 == ALOAD_0)
                                    && (op2 == IF_ACMPEQ || op2 == IF_ACMPNE) && (op5 == ICONST_0 || op5 == ICONST_1)
                                    && op6 == IRETURN && (op7 == ICONST_0 || op7 == ICONST_1) && op8 == IRETURN)
                                equalsMethodIsInstanceOfEquals = true;
                        } else if (codeBytes.length == 11) {
                            int op0 = opcode(codeBytes, 0);
                            int op1 = opcode(codeBytes, 1);
                            int op2 = opcode(codeBytes, 2);
                            int op5 = opcode(codeBytes, 5);
                            int op6 = opcode(codeBytes, 6);
                            int op9 = opcode(codeBytes, 9);
                            int op10 = opcode(codeBytes, 10);
                            if ((op0 == ALOAD_0 && op1 == ALOAD_1 || op0 == ALOAD_1 && op1 == ALOAD_0)
                                    && (op2 == IF_ACMPEQ || op2 == IF_ACMPNE) && (op5 == ICONST_0 || op5 == ICONST_1)
                                    && op6 == GOTO && (op9 == ICONST_0 || op9 == ICONST_1) && op10 == IRETURN)
                                equalsMethodIsInstanceOfEquals = true;

                        } else if ((codeBytes.length == 5 && (codeBytes[1] & 0xff) == INSTANCEOF)
                                || (codeBytes.length == 15 && (codeBytes[1] & 0xff) == INSTANCEOF && (codeBytes[11] & 0xff) == INVOKESPECIAL)) {
                            equalsMethodIsInstanceOfEquals = true;
                        }
                    }
                } else if (sig.equals("(L" + getClassName() + ";)Z")) {
                    hasEqualsSelf = true;
                    if (equalsMethod == null)
                        equalsMethod = MethodAnnotation.fromVisitedMethod(this);
                } else {
                    String arg = m.group(1);
                    if (getSuperclassName().equals(arg)) {
                        JavaClass findSuperImplementor = Lookup.findSuperDefiner(getThisClass(), name, sig, bugReporter);
                        if (findSuperImplementor == null) {
                            hasEqualsOther = true;
                            equalsOtherMethod = MethodAnnotation.fromVisitedMethod(this);
                            equalsOtherClass = DescriptorFactory.createClassDescriptor(arg);
                        }
                    }

                }
            }
        } else if (name.equals("compareTo") && sig.endsWith(")I") && !obj.isStatic()) {
            MethodAnnotation tmp = MethodAnnotation.fromVisitedMethod(this);
            if (BCELUtil.isSynthetic(obj))
                hasCompareToBridgeMethod = true;
            if (sig.equals("(Ljava/lang/Object;)I")) {
                hasCompareToObject = true;
                compareToObjectMethod = compareToMethod = tmp;
            } else if (sig.equals("(L" + getClassName() + ";)I")) {
                hasCompareToSelf = true;
                compareToSelfMethod = compareToMethod = tmp;
            }
        }
    }

    Method findMethod(JavaClass clazz, String name, String sig) {
        Method[] m = clazz.getMethods();
        for (Method aM : m)
            if (aM.getName().equals(name) && aM.getSignature().equals(sig))
                return aM;
        return null;
    }

    @Override
    public void sawOpcode(int seen) {
        if (seen == INVOKEVIRTUAL || seen == INVOKEINTERFACE) {
            String className = getClassConstantOperand();
            if (className.equals("java/util/Map") || className.equals("java/util/HashMap")
                    || className.equals("java/util/LinkedHashMap") || className.equals("java/util/concurrent/ConcurrentHashMap")
                    || className.contains("Hash")
                    && Subtypes2.instanceOf(ClassName.toDottedClassName(className), "java.util.Map")) {
                if (getNameConstantOperand().equals("put")
                        && getSigConstantOperand().equals("(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;")
                        && stack.getStackDepth() >= 3)
                    check(1);
                else if ((getNameConstantOperand().equals("get") || getNameConstantOperand().equals("remove"))
                        && getSigConstantOperand().startsWith("(Ljava/lang/Object;)") && stack.getStackDepth() >= 2)
                    check(0);
            } else if (className.equals("java/util/Set") || className.equals("java/util/HashSet") || className.contains("Hash")
                    && Subtypes2.instanceOf(ClassName.toDottedClassName(className), "java.util.Set")) {
                if (getNameConstantOperand().equals("add") || getNameConstantOperand().equals("contains")
                        || getNameConstantOperand().equals("remove") && getSigConstantOperand().equals("(Ljava/lang/Object;)Z")
                        && stack.getStackDepth() >= 2)
                    check(0);
            }
        }
    }

    private void check(int pos) {
        OpcodeStack.Item item = stack.getStackItem(pos);
        JavaClass type = null;

        try {
            type = item.getJavaClass();
        } catch (ClassNotFoundException e) {
            AnalysisContext.reportMissingClass(e);
        }
        if (type == null)
            return;
        String typeName = type.getClassName();
        if (typeName.startsWith("java.lang"))
            return;
        int priority = NORMAL_PRIORITY;

        OpcodeStack.Item collection = stack.getStackItem(PreorderVisitor.getNumberArguments(getSigConstantOperand()));
        String collectionSignature = collection.getSignature();
        if (collectionSignature.indexOf("Tree") >= 0
                || collectionSignature.indexOf("Sorted") >= 0
                || collectionSignature.indexOf("SkipList") >= 0 )
            return;

        if (collectionSignature.indexOf("Hash") >= 0)
            priority--;
        if (!AnalysisContext.currentAnalysisContext()/* .getSubtypes() */.isApplicationClass(type))
            priority++;

        if (type.isAbstract() || type.isInterface())
            priority++;
        potentialBugs.put(
                type.getClassName(),
                new BugInstance(this, "HE_USE_OF_UNHASHABLE_CLASS", priority).addClassAndMethod(this)
                        .addTypeOfNamedClass(type.getClassName()).describe(TypeAnnotation.UNHASHABLE_ROLE).addCalledMethod(this)
                        .addSourceLine(this));
    }

    static final Pattern mapPattern = Pattern.compile("[^y]HashMap<L([^;<]*);");

    static final Pattern hashTablePattern = Pattern.compile("Hashtable<L([^;<]*);");

    static final Pattern setPattern = Pattern.compile("[^y]HashSet<L([^;<]*);");

    @CheckForNull
    @DottedClassName
    String findHashedClassInSignature(String sig) {
        Matcher m = mapPattern.matcher(sig);
        if (m.find())
            return m.group(1).replace('/', '.');
        m = hashTablePattern.matcher(sig);
        if (m.find())
            return m.group(1).replace('/', '.');

        m = setPattern.matcher(sig);
        if (m.find())
            return m.group(1).replace('/', '.');
        return null;

    }

    @Override
    public void visit(Signature obj) {
        if (!isApplicationClass)
            return;

        String sig = obj.getSignature();
        String className = findHashedClassInSignature(sig);
        if (className == null)
            return;
        if (className.startsWith("java.lang"))
            return;
        JavaClass type = null;

        try {
            type = Repository.lookupClass(className);
        } catch (ClassNotFoundException e) {
            AnalysisContext.reportMissingClass(e);
        }
        if (type == null)
            return;

        int priority = NORMAL_PRIORITY;
        if (sig.indexOf("Hash") >= 0)
            priority--;
        if (type.isAbstract() || type.isInterface())
            priority++;
        if (!AnalysisContext.currentAnalysisContext()/* .getSubtypes() */.isApplicationClass(type))
            priority++;

        BugInstance bug = null;

        if (visitingField())
            bug = new BugInstance(this, "HE_SIGNATURE_DECLARES_HASHING_OF_UNHASHABLE_CLASS", priority).addClass(this)
                    .addVisitedField(this).addTypeOfNamedClass(className).describe(TypeAnnotation.UNHASHABLE_ROLE);
        else if (visitingMethod())
            bug = new BugInstance(this, "HE_SIGNATURE_DECLARES_HASHING_OF_UNHASHABLE_CLASS", priority).addClassAndMethod(this)
                    .addTypeOfNamedClass(className).describe(TypeAnnotation.UNHASHABLE_ROLE);
        else
            bug = new BugInstance(this, "HE_SIGNATURE_DECLARES_HASHING_OF_UNHASHABLE_CLASS", priority).addClass(this)
                    .addClass(this).addTypeOfNamedClass(className).describe(TypeAnnotation.UNHASHABLE_ROLE);
        potentialBugs.put(className, bug);
    }

    @Override
    public void report() {
        for (Map.Entry<String, BugInstance> e : potentialBugs.entrySet()) {
            if (!isHashableClassName(e.getKey())) {
                BugInstance bug = e.getValue();

                bugReporter.reportBug(bug);
            }
        }

    }
}
