001: /*
002: * FindBugs - Find bugs in Java programs
003: * Copyright (C) 2003-2005 University of Maryland
004: *
005: * This library is free software; you can redistribute it and/or
006: * modify it under the terms of the GNU Lesser General Public
007: * License as published by the Free Software Foundation; either
008: * version 2.1 of the License, or (at your option) any later version.
009: *
010: * This library is distributed in the hope that it will be useful,
011: * but WITHOUT ANY WARRANTY; without even the implied warranty of
012: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013: * Lesser General Public License for more details.
014: *
015: * You should have received a copy of the GNU Lesser General Public
016: * License along with this library; if not, write to the Free Software
017: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018: */
019:
020: package edu.umd.cs.findbugs.detect;
021:
022: import java.util.HashMap;
023: import java.util.HashSet;
024: import java.util.Map;
025: import java.util.regex.Matcher;
026: import java.util.regex.Pattern;
027:
028: import org.apache.bcel.Repository;
029: import org.apache.bcel.classfile.Code;
030: import org.apache.bcel.classfile.Field;
031: import org.apache.bcel.classfile.JavaClass;
032: import org.apache.bcel.classfile.Method;
033: import org.apache.bcel.classfile.Signature;
034:
035: import edu.umd.cs.findbugs.BugInstance;
036: import edu.umd.cs.findbugs.BugReporter;
037: import edu.umd.cs.findbugs.Lookup;
038: import edu.umd.cs.findbugs.MethodAnnotation;
039: import edu.umd.cs.findbugs.OpcodeStack;
040: import edu.umd.cs.findbugs.Priorities;
041: import edu.umd.cs.findbugs.StatelessDetector;
042: import edu.umd.cs.findbugs.TypeAnnotation;
043: import edu.umd.cs.findbugs.annotations.CheckForNull;
044: import edu.umd.cs.findbugs.ba.AnalysisContext;
045: import edu.umd.cs.findbugs.ba.XClass;
046: import edu.umd.cs.findbugs.ba.XMethod;
047: import edu.umd.cs.findbugs.bcel.OpcodeStackDetector;
048: import edu.umd.cs.findbugs.classfile.ClassDescriptor;
049: import edu.umd.cs.findbugs.classfile.DescriptorFactory;
050:
051: public class FindHEmismatch extends OpcodeStackDetector implements
052: StatelessDetector {
053: boolean hasFields = false;
054:
055: boolean visibleOutsidePackage = false;
056:
057: boolean hasHashCode = false;
058:
059: boolean hasEqualsObject = false;
060:
061: boolean hashCodeIsAbstract = false;
062:
063: boolean equalsObjectIsAbstract = false;
064:
065: boolean equalsMethodIsInstanceOfEquals = false;
066:
067: boolean hasCompareToObject = false;
068: boolean hasCompareToBridgeMethod = false;
069:
070: boolean hasEqualsSelf = false;
071: boolean hasEqualsOther = false;
072: MethodAnnotation equalsOther = null;
073:
074: boolean hasCompareToSelf = false;
075:
076: boolean extendsObject = false;
077:
078: MethodAnnotation equalsMethod = null;
079: MethodAnnotation equalsOtherMethod = null;
080: ClassDescriptor equalsOtherClass = null;
081:
082: MethodAnnotation compareToMethod = null;
083: MethodAnnotation compareToObjectMethod = null;
084: MethodAnnotation compareToSelfMethod = null;
085:
086: MethodAnnotation hashCodeMethod = null;
087:
088: HashSet<String> nonHashableClasses = new HashSet<String>();
089:
090: public boolean isHashableClassName(String dottedClassName) {
091: return !nonHashableClasses.contains(dottedClassName);
092: }
093:
094: Map<String, BugInstance> potentialBugs = new HashMap<String, BugInstance>();
095:
096: private BugReporter bugReporter;
097:
098: public FindHEmismatch(BugReporter bugReporter) {
099: this .bugReporter = bugReporter;
100: }
101:
102: @Override
103: public void visitAfter(JavaClass obj) {
104: if (!obj.isClass())
105: return;
106: if (getDottedClassName().equals("java.lang.Object"))
107: return;
108: int accessFlags = obj.getAccessFlags();
109: if ((accessFlags & ACC_INTERFACE) != 0)
110: return;
111: visibleOutsidePackage = obj.isPublic() || obj.isProtected();
112:
113: String whereEqual = getDottedClassName();
114: boolean inheritedHashCodeIsFinal = false;
115: boolean inheritedEqualsIsFinal = false;
116: boolean inheritedEqualsIsAbstract = false;
117: boolean inheritedEqualsFromAbstractClass = false;
118: XMethod inheritedEquals = null;
119: if (!hasEqualsObject) {
120: XClass we = Lookup.findImplementor(getXClass(), "equals",
121: "(Ljava/lang/Object;)Z", false, bugReporter);
122: if (we == null || we.equals(getXClass())) {
123: whereEqual = "java.lang.Object";
124: } else {
125: inheritedEqualsFromAbstractClass = we.isAbstract();
126: whereEqual = we.getClassDescriptor()
127: .getDottedClassName();
128: inheritedEquals = we.findMethod("equals",
129: "(Ljava/lang/Object;)Z", false);
130: if (inheritedEquals != null) {
131: inheritedEqualsIsFinal = inheritedEquals.isFinal();
132: inheritedEqualsIsAbstract = inheritedEquals
133: .isAbstract();
134: }
135: }
136: }
137: boolean usesDefaultEquals = whereEqual
138: .equals("java.lang.Object");
139: String whereHashCode = getDottedClassName();
140: if (!hasHashCode) {
141: XClass wh = Lookup.findSuperImplementor(getXClass(),
142: "hashCode", "()I", false, bugReporter);
143: if (wh == null) {
144: whereHashCode = "java.lang.Object";
145: } else {
146: whereHashCode = wh.getClassDescriptor()
147: .getDottedClassName();
148: XMethod m = wh.findMethod("hashCode", "()I", false);
149: if (m != null && m.isFinal())
150: inheritedHashCodeIsFinal = true;
151: }
152: }
153: boolean usesDefaultHashCode = whereHashCode
154: .equals("java.lang.Object");
155: if (false && (usesDefaultEquals || usesDefaultHashCode)) {
156: try {
157: if (Repository.implementationOf(obj, "java/util/Set")
158: || Repository.implementationOf(obj,
159: "java/util/List")
160: || Repository.implementationOf(obj,
161: "java/util/Map")) {
162: // System.out.println(getDottedClassName() + " uses default
163: // hashCode or equals");
164: }
165: } catch (ClassNotFoundException e) {
166: // e.printStackTrace();
167: }
168: }
169:
170: if (!hasEqualsObject && !hasEqualsSelf && hasEqualsOther) {
171: BugInstance bug = new BugInstance(this ,
172: usesDefaultEquals ? "EQ_OTHER_USE_OBJECT"
173: : "EQ_OTHER_NO_OBJECT", NORMAL_PRIORITY)
174: .addClass(this ).addMethod(equalsOtherMethod)
175: .addClass(equalsOtherClass);
176: bugReporter.reportBug(bug);
177: }
178: if (!hasEqualsObject && hasEqualsSelf) {
179:
180: if (usesDefaultEquals) {
181: int priority = HIGH_PRIORITY;
182: if (usesDefaultHashCode || obj.isAbstract())
183: priority++;
184: if (!visibleOutsidePackage)
185: priority++;
186: String bugPattern = "EQ_SELF_USE_OBJECT";
187:
188: BugInstance bug = new BugInstance(this , bugPattern,
189: priority).addClass(getDottedClassName());
190: if (equalsMethod != null)
191: bug.addMethod(equalsMethod);
192: bugReporter.reportBug(bug);
193: } else {
194: int priority = NORMAL_PRIORITY;
195: if (hasFields)
196: priority--;
197: if (obj.isAbstract())
198: priority++;
199: String bugPattern = "EQ_SELF_NO_OBJECT";
200: String super className = obj.getSuperclassName();
201: if (super className.equals("java.lang.Enum")) {
202: bugPattern = "EQ_DONT_DEFINE_EQUALS_FOR_ENUM";
203: priority = HIGH_PRIORITY;
204: }
205: BugInstance bug = new BugInstance(this , bugPattern,
206: priority).addClass(getDottedClassName());
207: if (equalsMethod != null)
208: bug.addMethod(equalsMethod);
209: bugReporter.reportBug(bug);
210: }
211: }
212:
213: // System.out.println("Class " + getDottedClassName());
214: // System.out.println("usesDefaultEquals: " + usesDefaultEquals);
215: // System.out.println("hasHashCode: : " + hasHashCode);
216: // System.out.println("usesDefaultHashCode: " + usesDefaultHashCode);
217: // System.out.println("hasEquals: : " + hasEqualsObject);
218: // System.out.println("hasCompareToObject: : " + hasCompareToObject);
219: // System.out.println("hasCompareToSelf: : " + hasCompareToSelf);
220:
221: if ((hasCompareToObject || hasCompareToSelf)
222: && usesDefaultEquals) {
223: BugInstance bug = new BugInstance(this ,
224: "EQ_COMPARETO_USE_OBJECT_EQUALS",
225: obj.isAbstract() ? Priorities.LOW_PRIORITY
226: : Priorities.NORMAL_PRIORITY)
227: .addClass(this );
228: if (compareToSelfMethod != null)
229: bug.addMethod(compareToSelfMethod);
230: else
231: bug.addMethod(compareToObjectMethod);
232: bugReporter.reportBug(bug);
233: }
234: if (!hasCompareToObject && !hasCompareToBridgeMethod
235: && hasCompareToSelf) {
236: if (!extendsObject)
237: bugReporter.reportBug(new BugInstance(this ,
238: "CO_SELF_NO_OBJECT", NORMAL_PRIORITY).addClass(
239: getDottedClassName())
240: .addMethod(compareToMethod));
241: }
242:
243: // if (!hasFields) return;
244: if (hasHashCode && !hashCodeIsAbstract
245: && !(hasEqualsObject || hasEqualsSelf)) {
246: int priority = LOW_PRIORITY;
247: if (usesDefaultEquals)
248: bugReporter.reportBug(new BugInstance(this ,
249: "HE_HASHCODE_USE_OBJECT_EQUALS", priority)
250: .addClass(getDottedClassName()).addMethod(
251: hashCodeMethod));
252: else if (!inheritedEqualsIsFinal)
253: bugReporter
254: .reportBug(new BugInstance(this ,
255: "HE_HASHCODE_NO_EQUALS", priority)
256: .addClass(getDottedClassName())
257: .addMethod(hashCodeMethod));
258: }
259: if (!hasHashCode
260: && (hasEqualsObject && !equalsObjectIsAbstract || hasEqualsSelf)) {
261: if (usesDefaultHashCode) {
262: int priority = HIGH_PRIORITY;
263: if (equalsMethodIsInstanceOfEquals)
264: priority += 2;
265: else if (obj.isAbstract() || !hasEqualsObject)
266: priority++;
267: if (priority == HIGH_PRIORITY)
268: nonHashableClasses.add(getDottedClassName());
269: if (!visibleOutsidePackage) {
270: priority++;
271: }
272: BugInstance bug = new BugInstance(this ,
273: "HE_EQUALS_USE_HASHCODE", priority)
274: .addClass(getDottedClassName());
275: if (equalsMethod != null)
276: bug.addMethod(equalsMethod);
277: bugReporter.reportBug(bug);
278: } else if (!inheritedHashCodeIsFinal
279: && !whereHashCode.startsWith("java.util.Abstract")) {
280: int priority = LOW_PRIORITY;
281:
282: if (hasEqualsObject && inheritedEqualsIsAbstract)
283: priority++;
284: if (hasFields)
285: priority--;
286: if (equalsMethodIsInstanceOfEquals || !hasEqualsObject)
287: priority += 2;
288: else if (obj.isAbstract())
289: priority++;
290: BugInstance bug = new BugInstance(this ,
291: "HE_EQUALS_NO_HASHCODE", priority)
292: .addClass(getDottedClassName());
293: if (equalsMethod != null)
294: bug.addMethod(equalsMethod);
295: bugReporter.reportBug(bug);
296: }
297: }
298: if (!hasHashCode && !hasEqualsObject && !hasEqualsSelf
299: && !usesDefaultEquals && usesDefaultHashCode
300: && !obj.isAbstract()
301: && inheritedEqualsFromAbstractClass) {
302: BugInstance bug = new BugInstance(this ,
303: "HE_INHERITS_EQUALS_USE_HASHCODE", NORMAL_PRIORITY)
304: .addClass(getDottedClassName());
305: if (equalsMethod != null)
306: bug.addMethod(equalsMethod);
307: bugReporter.reportBug(bug);
308: }
309: if (!hasEqualsObject
310: && !hasEqualsSelf
311: && !usesDefaultEquals
312: && !obj.isAbstract()
313: && hasFields
314: && inheritedEquals != null
315: && !inheritedEqualsIsFinal
316: && !inheritedEqualsFromAbstractClass
317: && !inheritedEquals.getClassDescriptor()
318: .getSimpleName().startsWith("Abstract")
319: && !inheritedEquals.getClassDescriptor().getClassName()
320: .equals("java/lang/Enum")) {
321:
322: BugInstance bug = new BugInstance(this ,
323: "EQ_DOESNT_OVERRIDE_EQUALS", NORMAL_PRIORITY)
324: .addClass(this ).addMethod(inheritedEquals);
325: bugReporter.reportBug(bug);
326: }
327: }
328:
329: @Override
330: public void visit(JavaClass obj) {
331: extendsObject = getDottedSuperclassName().equals(
332: "java.lang.Object");
333: hasFields = false;
334: hasHashCode = false;
335: hasCompareToObject = false;
336: hasCompareToBridgeMethod = false;
337: hasCompareToSelf = false;
338: hasEqualsObject = false;
339: hasEqualsSelf = false;
340: hasEqualsOther = false;
341: hashCodeIsAbstract = false;
342: equalsObjectIsAbstract = false;
343: equalsMethodIsInstanceOfEquals = false;
344: equalsMethod = null;
345: equalsOtherMethod = null;
346: compareToMethod = null;
347: compareToSelfMethod = null;
348: compareToObjectMethod = null;
349: hashCodeMethod = null;
350: equalsOtherClass = null;
351: }
352:
353: @Override
354: public void visit(Field obj) {
355: int accessFlags = obj.getAccessFlags();
356: if ((accessFlags & ACC_STATIC) != 0)
357: return;
358: if (!obj.getName().startsWith("this$") && !obj.isSynthetic()
359: && !obj.isTransient())
360: hasFields = true;
361: }
362:
363: static final Pattern predicateOverAnInstance = Pattern
364: .compile("\\(L([^;]+);\\)Z");
365:
366: @Override
367: public void visit(Method obj) {
368:
369: int accessFlags = obj.getAccessFlags();
370: if ((accessFlags & ACC_STATIC) != 0)
371: return;
372: String name = obj.getName();
373: String sig = obj.getSignature();
374: if ((accessFlags & ACC_ABSTRACT) != 0) {
375: if (name.equals("equals")
376: && sig.equals("(L" + getClassName() + ";)Z")) {
377: bugReporter.reportBug(new BugInstance(this ,
378: "EQ_ABSTRACT_SELF", LOW_PRIORITY)
379: .addClass(getDottedClassName()));
380: return;
381: } else if (name.equals("compareTo")
382: && sig.equals("(L" + getClassName() + ";)I")) {
383: bugReporter.reportBug(new BugInstance(this ,
384: "CO_ABSTRACT_SELF", LOW_PRIORITY)
385: .addClass(getDottedClassName()));
386: return;
387: }
388: }
389: boolean sigIsObject = sig.equals("(Ljava/lang/Object;)Z");
390: if (name.equals("hashCode") && sig.equals("()I")) {
391: hasHashCode = true;
392: if (obj.isAbstract())
393: hashCodeIsAbstract = true;
394: hashCodeMethod = MethodAnnotation.fromVisitedMethod(this );
395: // System.out.println("Found hashCode for " + betterClassName);
396: } else if (obj.isPublic() && name.equals("equals")) {
397: Matcher m = predicateOverAnInstance.matcher(sig);
398: if (m.matches()) {
399: if (sigIsObject) {
400: equalsMethod = MethodAnnotation
401: .fromVisitedMethod(this );
402: hasEqualsObject = true;
403: if (obj.isAbstract())
404: equalsObjectIsAbstract = true;
405: else if (!obj.isNative()) {
406: Code code = obj.getCode();
407: byte[] codeBytes = code.getCode();
408:
409: if ((codeBytes.length == 5 && (codeBytes[1] & 0xff) == INSTANCEOF)
410: || (codeBytes.length == 15
411: && (codeBytes[1] & 0xff) == INSTANCEOF && (codeBytes[11] & 0xff) == INVOKESPECIAL)) {
412: equalsMethodIsInstanceOfEquals = true;
413: }
414: }
415: } else if (sig.equals("(L" + getClassName() + ";)Z")) {
416: hasEqualsSelf = true;
417: if (equalsMethod == null)
418: equalsMethod = MethodAnnotation
419: .fromVisitedMethod(this );
420: } else {
421: String arg = m.group(1);
422: if (getSuperclassName().equals(arg)) {
423: JavaClass findSuperImplementor = Lookup
424: .findSuperDefiner(getThisClass(), name,
425: sig, bugReporter);
426: if (findSuperImplementor == null) {
427: hasEqualsOther = true;
428: equalsOtherMethod = MethodAnnotation
429: .fromVisitedMethod(this );
430: equalsOtherClass = DescriptorFactory
431: .createClassDescriptor(arg);
432: }
433: }
434:
435: }
436: }
437: } else if (name.equals("compareTo") && sig.endsWith(")I")
438: && !obj.isStatic()) {
439: MethodAnnotation tmp = MethodAnnotation
440: .fromVisitedMethod(this );
441: if (obj.isSynthetic())
442: hasCompareToBridgeMethod = true;
443: if (sig.equals("(Ljava/lang/Object;)I")) {
444: hasCompareToObject = true;
445: compareToObjectMethod = compareToMethod = tmp;
446: } else if (sig.equals("(L" + getClassName() + ";)I")) {
447: hasCompareToSelf = true;
448: compareToSelfMethod = compareToMethod = tmp;
449: }
450: }
451: }
452:
453: Method findMethod(JavaClass clazz, String name, String sig) {
454: Method[] m = clazz.getMethods();
455: for (Method aM : m)
456: if (aM.getName().equals(name)
457: && aM.getSignature().equals(sig))
458: return aM;
459: return null;
460: }
461:
462: @Override
463: public void sawOpcode(int seen) {
464: if (seen == INVOKEVIRTUAL) {
465: String className = getClassConstantOperand();
466: if (className.equals("java/util/Map")
467: || className.equals("java/util/HashMap")
468: || className.equals("java/util/LinkedHashMap")
469: || className
470: .equals("java/util/concurrent/ConcurrentHashMap")) {
471: if (getNameConstantOperand().equals("put")
472: && getSigConstantOperand()
473: .equals(
474: "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;")
475: && stack.getStackDepth() >= 3)
476: check(1);
477: else if ((getNameConstantOperand().equals("get") || getNameConstantOperand()
478: .equals("remove"))
479: && getSigConstantOperand()
480: .equals(
481: "(Ljava/lang/Object;)Ljava/lang/Object;")
482: && stack.getStackDepth() >= 2)
483: check(0);
484: } else if (className.equals("java/util/Set")
485: || className.equals("java/util/HashSet")) {
486: if (getNameConstantOperand().equals("add")
487: || getNameConstantOperand().equals("contains")
488: || getNameConstantOperand().equals("remove")
489: && getSigConstantOperand().equals(
490: "(Ljava/lang/Object;)Z")
491: && stack.getStackDepth() >= 2)
492: check(0);
493: }
494: }
495: }
496:
497: private void check(int pos) {
498: OpcodeStack.Item item = stack.getStackItem(pos);
499: JavaClass type = null;
500:
501: try {
502: type = item.getJavaClass();
503: } catch (ClassNotFoundException e) {
504: AnalysisContext.reportMissingClass(e);
505: }
506: if (type == null)
507: return;
508: int priority = NORMAL_PRIORITY;
509: if (getClassConstantOperand().indexOf("Hash") >= 0)
510: priority--;
511: if (!AnalysisContext.currentAnalysisContext().getSubtypes()
512: .isApplicationClass(type))
513: priority++;
514:
515: if (type.isAbstract() || type.isInterface())
516: priority++;
517: potentialBugs.put(type.getClassName(), new BugInstance(this ,
518: "HE_USE_OF_UNHASHABLE_CLASS", priority)
519: .addClassAndMethod(this ).addTypeOfNamedClass(
520: type.getClassName()).describe(
521: TypeAnnotation.UNHASHABLE_ROLE)
522: .addTypeOfNamedClass(getClassConstantOperand())
523: .addSourceLine(this ));
524: }
525:
526: static final Pattern mapPattern = Pattern
527: .compile("HashMap<L([^;<]*);");
528: static final Pattern hashTablePattern = Pattern
529: .compile("Hashtable<L([^;<]*);");
530:
531: static final Pattern setPattern = Pattern
532: .compile("HashSet<L([^;<]*);");
533:
534: @CheckForNull
535: String findHashedClassInSignature(String sig) {
536: Matcher m = mapPattern.matcher(sig);
537: if (m.find())
538: return m.group(1).replace('/', '.');
539: m = hashTablePattern.matcher(sig);
540: if (m.find())
541: return m.group(1).replace('/', '.');
542:
543: m = setPattern.matcher(sig);
544: if (m.find())
545: return m.group(1).replace('/', '.');
546: ;
547: return null;
548:
549: }
550:
551: @Override
552: public void visit(Signature obj) {
553: String sig = obj.getSignature();
554: String className = findHashedClassInSignature(sig);
555: if (className == null)
556: return;
557: JavaClass type = null;
558:
559: try {
560: type = Repository.lookupClass(className);
561: } catch (ClassNotFoundException e) {
562: AnalysisContext.reportMissingClass(e);
563: }
564: if (type == null)
565: return;
566:
567: int priority = NORMAL_PRIORITY;
568: if (sig.indexOf("Hash") >= 0)
569: priority--;
570: if (type.isAbstract() || type.isInterface())
571: priority++;
572: if (!AnalysisContext.currentAnalysisContext().getSubtypes()
573: .isApplicationClass(type))
574: priority++;
575:
576: BugInstance bug = null;
577:
578: if (visitingField())
579: bug = new BugInstance(this , "HE_USE_OF_UNHASHABLE_CLASS",
580: priority).addClass(this ).addVisitedField(this )
581: .addTypeOfNamedClass(className).describe(
582: TypeAnnotation.UNHASHABLE_ROLE);
583: else if (visitingMethod())
584: bug = new BugInstance(this , "HE_USE_OF_UNHASHABLE_CLASS",
585: priority).addClassAndMethod(this )
586: .addTypeOfNamedClass(className).describe(
587: TypeAnnotation.UNHASHABLE_ROLE);
588: else
589: bug = new BugInstance(this , "HE_USE_OF_UNHASHABLE_CLASS",
590: priority).addClass(this ).addClass(this )
591: .addTypeOfNamedClass(className).describe(
592: TypeAnnotation.UNHASHABLE_ROLE);
593: potentialBugs.put(className, bug);
594: }
595:
596: @Override
597: public void report() {
598: for (Map.Entry<String, BugInstance> e : potentialBugs
599: .entrySet()) {
600: if (!isHashableClassName(e.getKey())) {
601: BugInstance bug = e.getValue();
602:
603: bugReporter.reportBug(bug);
604: }
605: }
606:
607: }
608: }
|