/*
* FindBugs - Find bugs in Java programs
* Copyright (C) 2003-2007, 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.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
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.ArrayType;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.FieldInstruction;
import org.apache.bcel.generic.GETFIELD;
import org.apache.bcel.generic.GETSTATIC;
import org.apache.bcel.generic.INVOKEINTERFACE;
import org.apache.bcel.generic.INVOKESPECIAL;
import org.apache.bcel.generic.INVOKESTATIC;
import org.apache.bcel.generic.INVOKEVIRTUAL;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InvokeInstruction;
import org.apache.bcel.generic.LDC;
import org.apache.bcel.generic.LDC2_W;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.ObjectType;
import org.apache.bcel.generic.ReferenceType;
import org.apache.bcel.generic.Type;
import edu.umd.cs.findbugs.BugAccumulator;
import edu.umd.cs.findbugs.BugAnnotation;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.Detector;
import edu.umd.cs.findbugs.FieldAnnotation;
import edu.umd.cs.findbugs.FindBugsAnalysisFeatures;
import edu.umd.cs.findbugs.MethodAnnotation;
import edu.umd.cs.findbugs.OpcodeStack.Item;
import edu.umd.cs.findbugs.Priorities;
import edu.umd.cs.findbugs.SourceLineAnnotation;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.TypeAnnotation;
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.ClassSummary;
import edu.umd.cs.findbugs.ba.DataflowAnalysisException;
import edu.umd.cs.findbugs.ba.DepthFirstSearch;
import edu.umd.cs.findbugs.ba.FieldSummary;
import edu.umd.cs.findbugs.ba.Hierarchy;
import edu.umd.cs.findbugs.ba.Hierarchy2;
import edu.umd.cs.findbugs.ba.IncompatibleTypes;
import edu.umd.cs.findbugs.ba.Location;
import edu.umd.cs.findbugs.ba.RepositoryLookupFailureCallback;
import edu.umd.cs.findbugs.ba.SignatureConverter;
import edu.umd.cs.findbugs.ba.TestCaseDetector;
import edu.umd.cs.findbugs.ba.XFactory;
import edu.umd.cs.findbugs.ba.XField;
import edu.umd.cs.findbugs.ba.XMethod;
import edu.umd.cs.findbugs.ba.npe.IsNullValueDataflow;
import edu.umd.cs.findbugs.ba.npe.IsNullValueFrame;
import edu.umd.cs.findbugs.ba.type.ExceptionSetFactory;
import edu.umd.cs.findbugs.ba.type.ExtendedTypes;
import edu.umd.cs.findbugs.ba.type.NullType;
import edu.umd.cs.findbugs.ba.type.StandardTypeMerger;
import edu.umd.cs.findbugs.ba.type.TypeAnalysis;
import edu.umd.cs.findbugs.ba.type.TypeDataflow;
import edu.umd.cs.findbugs.ba.type.TypeFrame;
import edu.umd.cs.findbugs.ba.type.TypeFrameModelingVisitor;
import edu.umd.cs.findbugs.ba.type.TypeMerger;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.DescriptorFactory;
import edu.umd.cs.findbugs.classfile.Global;
import edu.umd.cs.findbugs.classfile.MethodDescriptor;
import edu.umd.cs.findbugs.internalAnnotations.DottedClassName;
import edu.umd.cs.findbugs.internalAnnotations.StaticConstant;
import edu.umd.cs.findbugs.log.Profiler;
import edu.umd.cs.findbugs.props.WarningProperty;
import edu.umd.cs.findbugs.props.WarningPropertySet;
import edu.umd.cs.findbugs.props.WarningPropertyUtil;
import edu.umd.cs.findbugs.util.ClassName;
/**
* Find suspicious reference comparisons. This includes:
* <ul>
* <li>Strings and other java.lang objects compared by reference equality</li>
* <li>Calls to equals(Object) where the argument is a different type than the
* receiver object</li>
* </ul>
*
* @author David Hovemeyer
* @author Bill Pugh
*/
public class FindRefComparison implements Detector, ExtendedTypes {
private static final boolean DEBUG = SystemProperties.getBoolean("frc.debug");
private static final boolean REPORT_ALL_REF_COMPARISONS = true || SystemProperties.getBoolean("findbugs.refcomp.reportAll");
private static final int BASE_ES_PRIORITY = SystemProperties.getInt("es.basePriority", NORMAL_PRIORITY);
/**
* Classes that are suspicious if compared by reference.
*/
@StaticConstant
private static final HashSet<String> DEFAULT_SUSPICIOUS_SET = new HashSet<String>();
static {
DEFAULT_SUSPICIOUS_SET.add("java.lang.Boolean");
DEFAULT_SUSPICIOUS_SET.add("java.lang.Byte");
DEFAULT_SUSPICIOUS_SET.add("java.lang.Character");
DEFAULT_SUSPICIOUS_SET.add("java.lang.Double");
DEFAULT_SUSPICIOUS_SET.add("java.lang.Float");
DEFAULT_SUSPICIOUS_SET.add("java.lang.Integer");
DEFAULT_SUSPICIOUS_SET.add("java.lang.Long");
DEFAULT_SUSPICIOUS_SET.add("java.lang.Short");
}
/**
* Set of opcodes that invoke instance methods on an object.
*/
private static final BitSet invokeInstanceSet = new BitSet();
static {
invokeInstanceSet.set(Constants.INVOKEVIRTUAL);
invokeInstanceSet.set(Constants.INVOKEINTERFACE);
invokeInstanceSet.set(Constants.INVOKESPECIAL);
invokeInstanceSet.set(Constants.INVOKESTATIC);
}
/**
* Set of bytecodes using for prescreening.
*/
private static final BitSet prescreenSet = new BitSet();
static {
prescreenSet.or(invokeInstanceSet);
prescreenSet.set(Constants.IF_ACMPEQ);
prescreenSet.set(Constants.IF_ACMPNE);
}
/*
* ----------------------------------------------------------------------
* Helper classes
* ----------------------------------------------------------------------
*/
private static final byte T_DYNAMIC_STRING = T_AVAIL_TYPE + 0;
private static final byte T_STATIC_STRING = T_AVAIL_TYPE + 1;
private static final byte T_PARAMETER_STRING = T_AVAIL_TYPE + 2;
private static final byte T_STATIC_FINAL_PUBLIC_CONSTANT = T_AVAIL_TYPE + 3;
private static final String STRING_SIGNATURE = "Ljava/lang/String;";
/**
* @author pugh
*/
private final static class SpecialTypeAnalysis extends TypeAnalysis {
/**
* @param method
* @param methodGen
* @param cfg
* @param dfs
* @param typeMerger
* @param visitor
* @param lookupFailureCallback
* @param exceptionSetFactory
*/
private SpecialTypeAnalysis(Method method, MethodGen methodGen, CFG cfg, DepthFirstSearch dfs, TypeMerger typeMerger,
TypeFrameModelingVisitor visitor, RepositoryLookupFailureCallback lookupFailureCallback,
ExceptionSetFactory exceptionSetFactory) {
super(method, methodGen, cfg, dfs, typeMerger, visitor, lookupFailureCallback, exceptionSetFactory);
}
@Override
public void initEntryFact(TypeFrame result) {
super.initEntryFact(result);
for (int i = 0; i < methodGen.getMaxLocals(); i++) {
Type t = result.getValue(i);
if (t.equals(Type.STRING)) {
result.setValue(i, parameterStringTypeInstance);
}
}
}
}
/**
* Type representing a dynamically created String. This sort of String
* should never be compared using reference equality.
*/
public static class DynamicStringType extends ObjectType {
private static final long serialVersionUID = 1L;
public DynamicStringType() {
super("java.lang.String");
}
@Override
public byte getType() {
return T_DYNAMIC_STRING;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
@Override
public boolean equals(Object o) {
return o == this;
}
@Override
public String toString() {
return "<dynamic string>";
}
}
public static class FinalConstant extends ObjectType {
private static final long serialVersionUID = 1L;
final @Nonnull
XField field;
public FinalConstant(@DottedClassName String type, @Nonnull XField field) {
super(type);
this.field = field;
}
@Override
public int hashCode() {
return super.hashCode() * 31 + field.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof FinalConstant))
return false;
FinalConstant other = (FinalConstant) obj;
return super.equals(other) && this.field.equals(other.field);
}
public XField getXField() {
return field;
}
@Override
public String toString() {
return super.toString() + " " + field;
}
}
private static final Type dynamicStringTypeInstance = new DynamicStringType();
/**
* Type representing a static String. E.g., interned strings and constant
* strings. It is generally OK to compare this sort of String using
* reference equality.
*/
public static class StaticStringType extends ObjectType {
private static final long serialVersionUID = 1L;
public StaticStringType() {
super("java.lang.String");
}
@Override
public byte getType() {
return T_STATIC_STRING;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
@Override
public boolean equals(Object o) {
return o == this;
}
@Override
public String toString() {
return "<static string>";
}
}
private static final Type staticStringTypeInstance = new StaticStringType();
public static class EmptyStringType extends StaticStringType {
private static final long serialVersionUID = 1L;
public EmptyStringType() {
super();
}
@Override
public byte getType() {
return T_STATIC_STRING;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
@Override
public boolean equals(Object o) {
return o == this;
}
@Override
public String toString() {
return "<empty string>";
}
}
private static final Type emptyStringTypeInstance = new EmptyStringType();
/**
* Type representing a String passed as a parameter.
*/
public static class ParameterStringType extends ObjectType {
private static final long serialVersionUID = 1L;
public ParameterStringType() {
super("java.lang.String");
}
@Override
public byte getType() {
return T_PARAMETER_STRING;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
@Override
public boolean equals(Object o) {
return o == this;
}
@Override
public String toString() {
return "<parameter string>";
}
}
private static final Type parameterStringTypeInstance = new ParameterStringType();
private static class RefComparisonTypeFrameModelingVisitor extends TypeFrameModelingVisitor {
private final RepositoryLookupFailureCallback lookupFailureCallback;
private boolean sawStringIntern;
public RefComparisonTypeFrameModelingVisitor(ConstantPoolGen cpg, TypeMerger typeMerger,
RepositoryLookupFailureCallback lookupFailureCallback) {
super(cpg, typeMerger);
this.lookupFailureCallback = lookupFailureCallback;
this.sawStringIntern = false;
}
public boolean sawStringIntern() {
return sawStringIntern;
}
// Override handlers for bytecodes that may return String objects
// known to be dynamic or static.
@Override
public void visitINVOKESTATIC(INVOKESTATIC obj) {
if (returnsString(obj)) {
consumeStack(obj);
String className = obj.getClassName(getCPG());
if (className.equals("java.lang.String")) {
pushValue(dynamicStringTypeInstance);
} else {
pushReturnType(obj);
}
} else {
super.visitINVOKESTATIC(obj);
}
}
@Override
public void visitINVOKESPECIAL(INVOKESPECIAL obj) {
if (returnsString(obj))
handleInstanceMethod(obj);
else
super.visitINVOKESPECIAL(obj);
}
@Override
public void visitINVOKEINTERFACE(INVOKEINTERFACE obj) {
if (returnsString(obj))
handleInstanceMethod(obj);
else
super.visitINVOKEINTERFACE(obj);
}
@Override
public void visitINVOKEVIRTUAL(INVOKEVIRTUAL obj) {
if (returnsString(obj))
handleInstanceMethod(obj);
else
super.visitINVOKEVIRTUAL(obj);
}
private boolean returnsString(InvokeInstruction inv) {
String methodSig = inv.getSignature(getCPG());
return methodSig.endsWith(")Ljava/lang/String;");
}
private void handleInstanceMethod(InvokeInstruction obj) {
assert returnsString(obj);
consumeStack(obj);
String className = obj.getClassName(getCPG());
String methodName = obj.getName(getCPG());
// System.out.println(className + "." + methodName);
if (methodName.equals("intern") && className.equals("java.lang.String")) {
sawStringIntern = true;
pushValue(staticStringTypeInstance);
} else if (methodName.equals("toString") || className.equals("java.lang.String")) {
pushValue(dynamicStringTypeInstance);
// System.out.println(" dynamic");
} else {
pushReturnType(obj);
}
}
@Override
public void visitLDC(LDC obj) {
Type type = obj.getType(getCPG());
if (isString(type)) {
Object value = obj.getValue(getCPG());
if (value instanceof String && ((String)value).length() == 0)
pushValue( emptyStringTypeInstance);
else pushValue( staticStringTypeInstance);
}
else pushValue(type);
}
@Override
public void visitLDC2_W(LDC2_W obj) {
Type type = obj.getType(getCPG());
pushValue(isString(type) ? staticStringTypeInstance : type);
}
private boolean isString(Type type) {
return type.getSignature().equals(STRING_SIGNATURE);
}
@Override
public void visitGETSTATIC(GETSTATIC obj) {
Type type = obj.getType(getCPG());
XField xf = XFactory.createXField(obj, cpg);
if (xf.isFinal()) {
FieldSummary fieldSummary = AnalysisContext.currentAnalysisContext().getFieldSummary();
Item summary = fieldSummary.getSummary(xf);
if (summary.isNull()) {
pushValue(TypeFrame.getNullType());
return;
}
String slashedClassName = ClassName.fromFieldSignature(type.getSignature());
if (slashedClassName != null) {
String dottedClassName = ClassName.toDottedClassName(slashedClassName);
if (DEFAULT_SUSPICIOUS_SET.contains(dottedClassName)) {
type = new FinalConstant(dottedClassName, xf);
consumeStack(obj);
pushValue(type);
return;
}
}
}
if (type.getSignature().equals(STRING_SIGNATURE)) {
handleLoad(obj);
} else
super.visitGETSTATIC(obj);
}
@Override
public void visitGETFIELD(GETFIELD obj) {
Type type = obj.getType(getCPG());
if (type.getSignature().equals(STRING_SIGNATURE)) {
handleLoad(obj);
} else {
XField xf = XFactory.createXField(obj, cpg);
if (xf.isFinal()) {
FieldSummary fieldSummary = AnalysisContext.currentAnalysisContext().getFieldSummary();
Item summary = fieldSummary.getSummary(xf);
if (summary.isNull()) {
consumeStack(obj);
pushValue(TypeFrame.getNullType());
return;
}
String slashedClassName = ClassName.fromFieldSignature(type.getSignature());
if (slashedClassName != null) {
String dottedClassName = ClassName.toDottedClassName(slashedClassName);
if (DEFAULT_SUSPICIOUS_SET.contains(dottedClassName)) {
type = new FinalConstant(dottedClassName, xf);
consumeStack(obj);
pushValue(type);
return;
}
}
}
super.visitGETFIELD(obj);
}
}
private void handleLoad(FieldInstruction obj) {
consumeStack(obj);
Type type = obj.getType(getCPG());
if (!type.getSignature().equals(STRING_SIGNATURE))
throw new IllegalArgumentException("type is not String: " + type);
try {
String className = obj.getClassName(getCPG());
String fieldName = obj.getName(getCPG());
Field field = Hierarchy.findField(className, fieldName);
if (field != null) {
// If the field is final, we'll assume that the String value
// is static.
if (field.isFinal() && field.isFinal()) {
pushValue(staticStringTypeInstance);
} else {
pushValue(type);
}
return;
}
} catch (ClassNotFoundException ex) {
lookupFailureCallback.reportMissingClass(ex);
}
pushValue(type);
}
}
/**
* Type merger to use the extended String types.
*/
private static class RefComparisonTypeMerger extends StandardTypeMerger {
public RefComparisonTypeMerger(RepositoryLookupFailureCallback lookupFailureCallback,
ExceptionSetFactory exceptionSetFactory) {
super(lookupFailureCallback, exceptionSetFactory);
}
@Override
protected boolean isReferenceType(byte type) {
return super.isReferenceType(type) || type == T_STATIC_STRING || type == T_DYNAMIC_STRING;
}
@Override
protected ReferenceType mergeReferenceTypes(ReferenceType aRef, ReferenceType bRef) throws DataflowAnalysisException {
byte aType = aRef.getType();
byte bType = bRef.getType();
if (isExtendedStringType(aType) || isExtendedStringType(bType)) {
// If both types are the same extended String type,
// then the same type is returned. Otherwise, extended
// types are downgraded to plain java.lang.String,
// and a standard merge is applied.
if (aType == bType) {
return aRef;
}
if (isExtendedStringType(aType)) {
aRef = Type.STRING;
}
if (isExtendedStringType(bType)) {
bRef = Type.STRING;
}
}
return super.mergeReferenceTypes(aRef, bRef);
}
private boolean isExtendedStringType(byte type) {
return type == T_DYNAMIC_STRING || type == T_STATIC_STRING || type == T_PARAMETER_STRING;
}
}
/*
* ----------------------------------------------------------------------
* Fields
* ----------------------------------------------------------------------
*/
private final BugReporter bugReporter;
private final BugAccumulator bugAccumulator;
private ClassContext classContext;
private final Set<String> suspiciousSet;
/*
* ----------------------------------------------------------------------
* Implementation
* ----------------------------------------------------------------------
*/
public FindRefComparison(BugReporter bugReporter) {
this.bugReporter = bugReporter;
this.bugAccumulator = new BugAccumulator(bugReporter);
this.suspiciousSet = new HashSet<String>(DEFAULT_SUSPICIOUS_SET);
// Check frc.suspicious system property for additional suspicious types
// to check
String extraSuspiciousTypes = SystemProperties.getProperty("frc.suspicious");
if (extraSuspiciousTypes != null) {
StringTokenizer tok = new StringTokenizer(extraSuspiciousTypes, ",");
while (tok.hasMoreTokens()) {
suspiciousSet.add(tok.nextToken());
}
}
}
public void visitClassContext(ClassContext classContext) {
this.classContext = classContext;
JavaClass jclass = classContext.getJavaClass();
Method[] methodList = jclass.getMethods();
for (Method method : methodList) {
MethodGen methodGen = classContext.getMethodGen(method);
if (methodGen == null) {
continue;
}
// Prescreening - must have IF_ACMPEQ, IF_ACMPNE,
// or an invocation of an instance method
BitSet bytecodeSet = classContext.getBytecodeSet(method);
if (bytecodeSet == null || !bytecodeSet.intersects(prescreenSet)) {
continue;
}
if (DEBUG) {
System.out.println("FindRefComparison: analyzing " + SignatureConverter.convertMethodSignature(methodGen));
}
try {
analyzeMethod(classContext, method);
} catch (CFGBuilderException e) {
bugReporter.logError("Error analyzing " + method.toString(), e);
} catch (DataflowAnalysisException e) {
// bugReporter.logError("Error analyzing " + method.toString(),
// e);
}
bugAccumulator.reportAccumulatedBugs();
}
}
/**
* A BugInstance and its WarningPropertySet.
*/
private static class WarningWithProperties {
final BugInstance instance;
final SourceLineAnnotation sourceLine;
final WarningPropertySet<WarningProperty> propertySet;
final Location location;
WarningWithProperties(BugInstance warning, WarningPropertySet<WarningProperty> propertySet,
SourceLineAnnotation sourceLine, Location location) {
this.instance = warning;
this.propertySet = propertySet;
this.sourceLine = sourceLine;
this.location = location;
}
}
private interface WarningDecorator {
public void decorate(WarningWithProperties warn);
}
private void analyzeMethod(ClassContext classContext, final Method method) throws CFGBuilderException,
DataflowAnalysisException {
MethodGen methodGen = classContext.getMethodGen(method);
if (methodGen == null) {
return;
}
JavaClass jclass = classContext.getJavaClass();
ConstantPoolGen cpg = classContext.getConstantPoolGen();
// Enqueue all of the potential violations we find in the method.
// Normally we'll only report the first highest-priority warning,
// but if in relaxed mode or if REPORT_ALL_REF_COMPARISONS is set,
// then we'll report everything.
LinkedList<WarningWithProperties> refComparisonList = new LinkedList<WarningWithProperties>();
LinkedList<WarningWithProperties> stringComparisonList = new LinkedList<WarningWithProperties>();
comparedForEqualityInThisMethod = new HashMap<String,Integer>();
CFG cfg = classContext.getCFG(method);
DepthFirstSearch dfs = classContext.getDepthFirstSearch(method);
ExceptionSetFactory exceptionSetFactory = classContext.getExceptionSetFactory(method);
// Perform type analysis using our special type merger
// (which handles String types specially, keeping track of
// which ones appear to be dynamically created)
RefComparisonTypeMerger typeMerger = new RefComparisonTypeMerger(bugReporter, exceptionSetFactory);
RefComparisonTypeFrameModelingVisitor visitor = new RefComparisonTypeFrameModelingVisitor(methodGen.getConstantPool(),
typeMerger, bugReporter);
TypeAnalysis typeAnalysis = new SpecialTypeAnalysis(method, methodGen, cfg, dfs, typeMerger, visitor, bugReporter,
exceptionSetFactory);
TypeDataflow typeDataflow = new TypeDataflow(cfg, typeAnalysis);
Profiler profiler = Global.getAnalysisCache().getProfiler();
profiler.start(SpecialTypeAnalysis.class);
try {
typeDataflow.execute();
} finally {
profiler.end(SpecialTypeAnalysis.class);
}
// Inspect Locations in the method for suspicious ref comparisons and
// calls to equals()
for (Iterator<Location> i = cfg.locationIterator(); i.hasNext();) {
Location location = i.next();
inspectLocation(jclass, cpg, method, methodGen, refComparisonList, stringComparisonList, visitor, typeDataflow,
location);
}
if (stringComparisonList.isEmpty() && refComparisonList.isEmpty()) {
return;
}
// Add method-wide properties to BugInstances
final boolean likelyTestcase = TestCaseDetector.likelyTestCase(XFactory.createXMethod(jclass, method));
decorateWarnings(stringComparisonList, new WarningDecorator() {
public void decorate(WarningWithProperties warn) {
if (mightBeLaterCheckedUsingEquals(warn)) {
warn.propertySet.addProperty(RefComparisonWarningProperty.SAW_CALL_TO_EQUALS);
}
if (likelyTestcase) {
warn.propertySet.addProperty(RefComparisonWarningProperty.COMPARE_IN_TEST_CASE);
}
if (false && !(method.isPublic() || method.isProtected())) {
warn.propertySet.addProperty(RefComparisonWarningProperty.PRIVATE_METHOD);
}
}
});
decorateWarnings(refComparisonList, new WarningDecorator() {
public void decorate(WarningWithProperties warn) {
if (likelyTestcase) {
warn.propertySet.addProperty(RefComparisonWarningProperty.COMPARE_IN_TEST_CASE);
}
if (mightBeLaterCheckedUsingEquals(warn)) {
warn.propertySet.addProperty(RefComparisonWarningProperty.SAW_CALL_TO_EQUALS);
}
}
});
// Report violations
boolean relaxed = FindBugsAnalysisFeatures.isRelaxedMode();
reportBest(classContext, method, stringComparisonList, relaxed);
reportBest(classContext, method, refComparisonList, relaxed);
}
boolean mightBeLaterCheckedUsingEquals(WarningWithProperties warning) {
for (BugAnnotation a : warning.instance.getAnnotations())
if (a instanceof TypeAnnotation) {
String signature = ((TypeAnnotation) a).getTypeDescriptor();
Integer pc = comparedForEqualityInThisMethod.get(signature);
if (pc != null && pc > warning.location.getHandle().getPosition())
return true;
}
return false;
}
private void inspectLocation(JavaClass jclass, ConstantPoolGen cpg, Method method, MethodGen methodGen,
LinkedList<WarningWithProperties> refComparisonList, LinkedList<WarningWithProperties> stringComparisonList,
RefComparisonTypeFrameModelingVisitor visitor, TypeDataflow typeDataflow, Location location)
throws DataflowAnalysisException {
Instruction ins = location.getHandle().getInstruction();
short opcode = ins.getOpcode();
if (opcode == Constants.IF_ACMPEQ || opcode == Constants.IF_ACMPNE) {
checkRefComparison(location, jclass, method, methodGen, visitor, typeDataflow, stringComparisonList,
refComparisonList);
} else if (ins instanceof InvokeInstruction) {
InvokeInstruction inv = (InvokeInstruction) ins;
boolean isStatic = inv instanceof INVOKESTATIC;
@DottedClassName String className = inv.getClassName(cpg);
String methodName = inv.getMethodName(cpg);
String methodSig = inv.getSignature(cpg);
if ( methodName.equals("assertSame") && methodSig.equals("(Ljava/lang/Object;Ljava/lang/Object;)V")) {
checkRefComparison(location, jclass, method, methodGen, visitor, typeDataflow, stringComparisonList,
refComparisonList);
} else if ( methodName.equals("assertFalse") && methodSig.equals("(Z)V")) {
SourceLineAnnotation lastLocation = bugAccumulator.getLastBugLocation();
InstructionHandle prevHandle = location.getHandle().getPrev();
if (lastLocation != null && prevHandle != null && lastLocation.getEndBytecode() == prevHandle.getPosition()){
bugAccumulator.forgetLastBug();
if (DEBUG)
System.out.println("Forgetting last bug due to call to " + className +"." + methodName);
}
} else {
boolean equalsMethod = !isStatic && methodName.equals("equals") && methodSig.equals("(Ljava/lang/Object;)Z")
|| isStatic && methodName.equals("assertEquals")
&& methodSig.equals("(Ljava/lang/Object;Ljava/lang/Object;)V")
|| isStatic && methodName.equals("equal") && methodSig.equals("(Ljava/lang/Object;Ljava/lang/Object;)Z")
&& className.equals("com.google.common.base.Objects")
|| isStatic && methodName.equals("equals") && methodSig.equals("(Ljava/lang/Object;Ljava/lang/Object;)Z")
&& className.equals("java.util.Objects");
if (equalsMethod) {
checkEqualsComparison(location, jclass, method, methodGen, cpg, typeDataflow);
}
}
}
}
private void decorateWarnings(LinkedList<WarningWithProperties> stringComparisonList, WarningDecorator warningDecorator) {
for (WarningWithProperties warn : stringComparisonList) {
warningDecorator.decorate(warn);
warn.propertySet.decorateBugInstance(warn.instance);
}
}
private void reportBest(ClassContext classContext, Method method, LinkedList<WarningWithProperties> warningList,
boolean relaxed) {
boolean reportAll = relaxed || REPORT_ALL_REF_COMPARISONS;
int bestPriority = Integer.MAX_VALUE;
for (WarningWithProperties warn : warningList) {
int priority = warn.instance.getPriority();
if (bestPriority > priority)
bestPriority = priority;
if (reportAll) {
if (relaxed) {
// Add general warning properties
WarningPropertyUtil.addPropertiesForDataMining(warn.propertySet, classContext, method, warn.location);
// Convert warning properties to bug properties
warn.propertySet.decorateBugInstance(warn.instance);
}
bugAccumulator.accumulateBug(warn.instance, warn.sourceLine);
}
}
if (!reportAll)
for (WarningWithProperties warn : warningList) {
BugInstance bug = warn.instance;
int priority = warn.instance.getPriority();
if (priority <= bestPriority)
bugAccumulator.accumulateBug(warn.instance, warn.sourceLine);
}
}
private void checkRefComparison(Location location, JavaClass jclass, Method method, MethodGen methodGen,
RefComparisonTypeFrameModelingVisitor visitor, TypeDataflow typeDataflow,
List<WarningWithProperties> stringComparisonList, List<WarningWithProperties> refComparisonList)
throws DataflowAnalysisException {
InstructionHandle handle = location.getHandle();
TypeFrame frame = typeDataflow.getFactAtLocation(location);
if (frame.getStackDepth() < 2) {
throw new DataflowAnalysisException("Stack underflow", methodGen, handle);
}
int numSlots = frame.getNumSlots();
Type lhsType = frame.getValue(numSlots - 2);
Type rhsType = frame.getValue(numSlots - 1);
if (lhsType instanceof NullType || rhsType instanceof NullType) {
return;
}
if (lhsType instanceof ReferenceType && rhsType instanceof ReferenceType) {
IncompatibleTypes result = IncompatibleTypes.getPriorityForAssumingCompatible(lhsType, rhsType, true);
if (result != IncompatibleTypes.SEEMS_OK && result != IncompatibleTypes.UNCHECKED) {
String sourceFile = jclass.getSourceFileName();
boolean isAssertSame = handle.getInstruction() instanceof INVOKESTATIC;
if (isAssertSame)
bugAccumulator.accumulateBug(
new BugInstance(this, "TESTING", result.getPriority())
.addClassAndMethod(methodGen, sourceFile)
.addString("Calling assertSame with two distinct objects")
.addFoundAndExpectedType(rhsType, lhsType)
.addSomeSourceForTopTwoStackValues(classContext, method, location),
SourceLineAnnotation.fromVisitedInstruction(classContext, methodGen, sourceFile, handle));
else
bugAccumulator.accumulateBug(
new BugInstance(this, "EC_UNRELATED_TYPES_USING_POINTER_EQUALITY", result.getPriority())
.addClassAndMethod(methodGen, sourceFile).addFoundAndExpectedType(rhsType, lhsType)
.addSomeSourceForTopTwoStackValues(classContext, method, location),
SourceLineAnnotation.fromVisitedInstruction(classContext, methodGen, sourceFile, handle));
return;
}
if (lhsType.equals(Type.OBJECT) && rhsType.equals(Type.OBJECT))
return;
String lhs = SignatureConverter.convert(lhsType.getSignature());
String rhs = SignatureConverter.convert(rhsType.getSignature());
if (lhs.equals("java.lang.String") || rhs.equals("java.lang.String")) {
handleStringComparison(jclass, method, methodGen, visitor, stringComparisonList, location, lhsType, rhsType);
} else if (suspiciousSet.contains(lhs)) {
handleSuspiciousRefComparison(jclass, method, methodGen, refComparisonList, location, lhs,
(ReferenceType) lhsType, (ReferenceType) rhsType);
} else if (suspiciousSet.contains(rhs)) {
handleSuspiciousRefComparison(jclass, method, methodGen, refComparisonList, location, rhs,
(ReferenceType) lhsType, (ReferenceType) rhsType);
}
}
}
private void handleStringComparison(JavaClass jclass, Method method, MethodGen methodGen,
RefComparisonTypeFrameModelingVisitor visitor, List<WarningWithProperties> stringComparisonList, Location location,
Type lhsType, Type rhsType) {
if (DEBUG) {
System.out.println("String/String comparison at " + location.getHandle());
}
// Compute the priority:
// - two static strings => do not report
// - dynamic string and anything => high
// - static string and unknown => medium
// - all other cases => low
// System.out.println("Compare " + lhsType + " == " + rhsType);
byte type1 = lhsType.getType();
byte type2 = rhsType.getType();
String bugPattern = "ES_COMPARING_STRINGS_WITH_EQ";
// T1 T2 result
// S S no-op
// D ? high
// ? D high
// S ? normal
// ? S normal
WarningPropertySet<WarningProperty> propertySet = new WarningPropertySet<WarningProperty>();
if (type1 == T_STATIC_STRING && type2 == T_STATIC_STRING) {
propertySet.addProperty(RefComparisonWarningProperty.COMPARE_STATIC_STRINGS);
} else if (type1 == T_DYNAMIC_STRING || type2 == T_DYNAMIC_STRING) {
propertySet.addProperty(RefComparisonWarningProperty.DYNAMIC_AND_UNKNOWN);
} else if (type2 == T_PARAMETER_STRING || type1 == T_PARAMETER_STRING) {
bugPattern = "ES_COMPARING_PARAMETER_STRING_WITH_EQ";
if (methodGen.isPublic() || methodGen.isProtected()) {
propertySet.addProperty(RefComparisonWarningProperty.STRING_PARAMETER_IN_PUBLIC_METHOD);
} else {
propertySet.addProperty(RefComparisonWarningProperty.STRING_PARAMETER);
}
} else if (type1 == T_STATIC_STRING || type2 == T_STATIC_STRING) {
if (lhsType instanceof EmptyStringType || rhsType instanceof EmptyStringType)
propertySet.addProperty(RefComparisonWarningProperty.EMPTY_AND_UNKNOWN);
else
propertySet.addProperty(RefComparisonWarningProperty.STATIC_AND_UNKNOWN);
} else if (visitor.sawStringIntern()) {
propertySet.addProperty(RefComparisonWarningProperty.SAW_INTERN);
}
String sourceFile = jclass.getSourceFileName();
BugInstance instance = new BugInstance(this, bugPattern, BASE_ES_PRIORITY).addClassAndMethod(methodGen, sourceFile)
.addType("Ljava/lang/String;").describe(TypeAnnotation.FOUND_ROLE).addSomeSourceForTopTwoStackValues(classContext, method, location);
SourceLineAnnotation sourceLineAnnotation = SourceLineAnnotation.fromVisitedInstruction(classContext, methodGen,
sourceFile, location.getHandle());
if (sourceLineAnnotation != null) {
WarningWithProperties warn = new WarningWithProperties(instance, propertySet, sourceLineAnnotation, location);
stringComparisonList.add(warn);
}
}
private void handleSuspiciousRefComparison(JavaClass jclass, Method method, MethodGen methodGen,
List<WarningWithProperties> refComparisonList, Location location, String lhs, ReferenceType lhsType,
ReferenceType rhsType) {
XField xf = null;
if (lhsType instanceof FinalConstant)
xf = ((FinalConstant) lhsType).getXField();
else if (rhsType instanceof FinalConstant)
xf = ((FinalConstant) rhsType).getXField();
String sourceFile = jclass.getSourceFileName();
String bugPattern = "RC_REF_COMPARISON";
int priority = Priorities.HIGH_PRIORITY;
if (lhs.equals("java.lang.Boolean")) {
bugPattern = "RC_REF_COMPARISON_BAD_PRACTICE_BOOLEAN";
priority = Priorities.NORMAL_PRIORITY;
} else if (xf != null && xf.isStatic() && xf.isFinal()) {
bugPattern = "RC_REF_COMPARISON_BAD_PRACTICE";
if (xf.isPublic() || !methodGen.isPublic())
priority = Priorities.NORMAL_PRIORITY;
}
BugInstance instance = new BugInstance(this, bugPattern, priority).addClassAndMethod(methodGen, sourceFile)
.addType("L" + lhs.replace('.', '/') + ";").describe(TypeAnnotation.FOUND_ROLE);
if (xf != null)
instance.addField(xf).describe(FieldAnnotation.LOADED_FROM_ROLE);
else
instance.addSomeSourceForTopTwoStackValues(classContext, method, location);
SourceLineAnnotation sourceLineAnnotation = SourceLineAnnotation.fromVisitedInstruction(classContext, methodGen,
sourceFile, location.getHandle());
if (sourceLineAnnotation != null)
refComparisonList.add(new WarningWithProperties(instance, new WarningPropertySet<WarningProperty>(),
sourceLineAnnotation, location));
}
private Map<String, Integer> comparedForEqualityInThisMethod;
void addEqualsCheck(String type, int pc) {
Integer oldPC = comparedForEqualityInThisMethod.get(type);
if (oldPC == null || pc < oldPC)
comparedForEqualityInThisMethod.put(type, pc);
}
private void checkEqualsComparison(Location location, JavaClass jclass, Method method, MethodGen methodGen,
ConstantPoolGen cpg, TypeDataflow typeDataflow) throws DataflowAnalysisException {
InstructionHandle handle = location.getHandle();
InstructionHandle next = handle.getNext();
if (next != null && next.getInstruction() instanceof INVOKESTATIC) {
INVOKESTATIC is = (INVOKESTATIC) next.getInstruction();
if (is.getMethodName(cpg).equals("assertFalse")) {
return;
}
}
String sourceFile = jclass.getSourceFileName();
TypeFrame frame = typeDataflow.getFactAtLocation(location);
if (frame.getStackDepth() < 2) {
throw new DataflowAnalysisException("Stack underflow", methodGen, handle);
}
int numSlots = frame.getNumSlots();
Type lhsType_ = frame.getValue(numSlots - 2);
Type rhsType_ = frame.getValue(numSlots - 1);
// Ignore top and bottom values
if (lhsType_.getType() == T_TOP || lhsType_.getType() == T_BOTTOM || rhsType_.getType() == T_TOP
|| rhsType_.getType() == T_BOTTOM) {
return;
}
InvokeInstruction inv = (InvokeInstruction) handle.getInstruction();
MethodAnnotation calledMethodAnnotation = getMethodCalledAnnotation(cpg, inv);
boolean looksLikeTestCase = TestCaseDetector.likelyTestCase(XFactory.createXMethod(methodGen));
int priorityModifier = 0;
if (looksLikeTestCase) {
priorityModifier = 1;
}
if (rhsType_.getType() == T_NULL) {
// A literal null value was passed directly to equals().
if (!looksLikeTestCase) {
try {
IsNullValueDataflow isNullDataflow = classContext.getIsNullValueDataflow(method);
IsNullValueFrame isNullFrame = isNullDataflow.getFactAtLocation(location);
BugAnnotation a = BugInstance.getSourceForTopStackValue(classContext, method, location);
int priority = NORMAL_PRIORITY;
if (a instanceof FieldAnnotation && ((FieldAnnotation) a).isStatic())
priority = LOW_PRIORITY;
if (isNullFrame.isValid() && isNullFrame.getTopValue().isDefinitelyNull()) {
String type = "EC_NULL_ARG";
if (calledMethodAnnotation != null && calledMethodAnnotation.isStatic()){
type = "DMI_DOH";
priority = LOW_PRIORITY;
}
BugInstance bug = new BugInstance(this, type, priority + priorityModifier).addClassAndMethod(methodGen, sourceFile)
.addOptionalAnnotation(calledMethodAnnotation);
if (type.equals("DMI_DOH"))
bug.addString("Use \"== null\" to check for a value being null");
bugAccumulator.accumulateBug(
bug,
SourceLineAnnotation.fromVisitedInstruction(this.classContext, methodGen, sourceFile,
location.getHandle()));
}
} catch (CFGBuilderException e) {
AnalysisContext.logError("Error getting null value analysis", e);
}
}
return;
} else if (lhsType_.getType() == T_NULL) {
// Hmm...in this case, equals() is being invoked on
// a literal null value. This is really the
// purview of FindNullDeref. So, we'll just do nothing.
return;
} else if (!(lhsType_ instanceof ReferenceType) || !(rhsType_ instanceof ReferenceType)) {
bugReporter.logError("equals() used to compare non-object type(s) " + lhsType_ + " and " + rhsType_ + " in "
+ SignatureConverter.convertMethodSignature(methodGen) + " at " + location.getHandle());
return;
}
IncompatibleTypes result = IncompatibleTypes.getPriorityForAssumingCompatible(lhsType_, rhsType_);
if (lhsType_ instanceof ArrayType && rhsType_ instanceof ArrayType) {
String pattern = "EC_BAD_ARRAY_COMPARE";
IncompatibleTypes result2 = IncompatibleTypes.getPriorityForAssumingCompatible(lhsType_, rhsType_, true);
if (result2.getPriority() <= Priorities.NORMAL_PRIORITY)
pattern = "EC_INCOMPATIBLE_ARRAY_COMPARE";
else if (calledMethodAnnotation != null && calledMethodAnnotation.getClassName().equals("org.testng.Assert"))
return;
bugAccumulator.accumulateBug(new BugInstance(this, pattern, NORMAL_PRIORITY).addClassAndMethod(methodGen, sourceFile)
.addFoundAndExpectedType(rhsType_, lhsType_)
.addSomeSourceForTopTwoStackValues(classContext, method, location)
.addOptionalAnnotation(calledMethodAnnotation, MethodAnnotation.METHOD_CALLED),
SourceLineAnnotation.fromVisitedInstruction(this.classContext, methodGen, sourceFile, location.getHandle()));
return;
}
if (result.getPriority() >= Priorities.LOW_PRIORITY) {
addEqualsCheck(lhsType_.getSignature(), handle.getPosition());
addEqualsCheck(rhsType_.getSignature(), handle.getPosition());
}
if (result == IncompatibleTypes.SEEMS_OK) return;
if (result.getPriority() > Priorities.LOW_PRIORITY)
return;
if (result == IncompatibleTypes.ARRAY_AND_NON_ARRAY || result == IncompatibleTypes.ARRAY_AND_OBJECT) {
String lhsSig = lhsType_.getSignature();
String rhsSig = rhsType_.getSignature();
boolean allOk = checkForWeirdEquals(lhsSig, rhsSig, new HashSet<XMethod>());
if (allOk)
priorityModifier += 2;
bugAccumulator.accumulateBug(new BugInstance(this, "EC_ARRAY_AND_NONARRAY", result.getPriority() + priorityModifier)
.addClassAndMethod(methodGen, sourceFile).addFoundAndExpectedType(rhsType_, lhsType_)
.addSomeSourceForTopTwoStackValues(classContext, method, location)
.addOptionalAnnotation(calledMethodAnnotation, MethodAnnotation.METHOD_CALLED),
SourceLineAnnotation.fromVisitedInstruction(this.classContext, methodGen, sourceFile, location.getHandle()));
} else if (result == IncompatibleTypes.INCOMPATIBLE_CLASSES) {
String lhsSig = lhsType_.getSignature();
String rhsSig = rhsType_.getSignature();
boolean core = lhsSig.startsWith("Ljava") && rhsSig.startsWith("Ljava");
if (core) {
looksLikeTestCase = false;
priorityModifier = 0;
}
if (true) {
Set<XMethod> targets = new HashSet<XMethod>();
boolean allOk = checkForWeirdEquals(lhsSig, rhsSig, targets);
if (allOk)
priorityModifier += 2;
int priority = result.getPriority() + priorityModifier;
bugAccumulator.accumulateBug(
new BugInstance(this, "EC_UNRELATED_TYPES", priority)
.addClassAndMethod(methodGen, sourceFile).addFoundAndExpectedType(rhsType_, lhsType_)
.addSomeSourceForTopTwoStackValues(classContext, method, location).addEqualsMethodUsed(targets)
.addOptionalAnnotation(calledMethodAnnotation, MethodAnnotation.METHOD_CALLED),
SourceLineAnnotation.fromVisitedInstruction(this.classContext, methodGen, sourceFile,
location.getHandle()));
}
} else if (result == IncompatibleTypes.UNRELATED_CLASS_AND_INTERFACE
|| result == IncompatibleTypes.UNRELATED_FINAL_CLASS_AND_INTERFACE) {
bugAccumulator.accumulateBug(
new BugInstance(this, "EC_UNRELATED_CLASS_AND_INTERFACE", result.getPriority() + priorityModifier)
.addClassAndMethod(methodGen, sourceFile).addFoundAndExpectedType(rhsType_, lhsType_)
.addSomeSourceForTopTwoStackValues(classContext, method, location)
.addEqualsMethodUsed(DescriptorFactory.createClassDescriptorFromSignature(lhsType_.getSignature()))
.addOptionalAnnotation(calledMethodAnnotation, MethodAnnotation.METHOD_CALLED),
SourceLineAnnotation.fromVisitedInstruction(this.classContext, methodGen, sourceFile, location.getHandle()));
} else if (result == IncompatibleTypes.UNRELATED_INTERFACES) {
bugAccumulator.accumulateBug(
new BugInstance(this, "EC_UNRELATED_INTERFACES", result.getPriority() + priorityModifier)
.addClassAndMethod(methodGen, sourceFile).addFoundAndExpectedType(rhsType_, lhsType_)
.addSomeSourceForTopTwoStackValues(classContext, method, location)
.addEqualsMethodUsed(DescriptorFactory.createClassDescriptorFromSignature(lhsType_.getSignature()))
.addOptionalAnnotation(calledMethodAnnotation, MethodAnnotation.METHOD_CALLED),
SourceLineAnnotation.fromVisitedInstruction(this.classContext, methodGen, sourceFile, location.getHandle()));
} else if (result != IncompatibleTypes.UNCHECKED && result.getPriority() <= Priorities.LOW_PRIORITY) {
bugAccumulator.accumulateBug(new BugInstance(this, "EC_UNRELATED_TYPES", result.getPriority() + priorityModifier)
.addClassAndMethod(methodGen, sourceFile).addFoundAndExpectedType(rhsType_, lhsType_)
.addSomeSourceForTopTwoStackValues(classContext, method, location)
.addOptionalAnnotation(calledMethodAnnotation, MethodAnnotation.METHOD_CALLED),
SourceLineAnnotation.fromVisitedInstruction(this.classContext, methodGen, sourceFile, location.getHandle()));
}
}
/**
* @param cpg
* @param inv
*/
public @CheckForNull MethodAnnotation getMethodCalledAnnotation(ConstantPoolGen cpg, InvokeInstruction inv) {
MethodDescriptor invokedMethod = getInvokedMethod(cpg, inv);
boolean standardEquals = invokedMethod.getName().equals("equals")
&& invokedMethod.getSignature().equals("(Ljava/lang/Object;)Z") && !invokedMethod.isStatic();
return standardEquals ? null : MethodAnnotation.fromMethodDescriptor(invokedMethod);
}
/**
* @param cpg
* @param inv
* @return
*/
public MethodDescriptor getInvokedMethod(ConstantPoolGen cpg, InvokeInstruction inv) {
String invoked = inv.getClassName(cpg);
String methodName = inv.getMethodName(cpg);
String methodSig = inv.getSignature(cpg);
MethodDescriptor invokedMethod =
DescriptorFactory.instance().getMethodDescriptor(ClassName.toSlashedClassName(invoked), methodName, methodSig, inv instanceof INVOKESTATIC);
return invokedMethod;
}
/**
* @param lhsSig
* @param rhsSig
* @param targets
* @return
*/
private boolean checkForWeirdEquals(String lhsSig, String rhsSig, Set<XMethod> targets) {
boolean allOk = false;
try {
ClassSummary classSummary = AnalysisContext.currentAnalysisContext().getClassSummary();
ClassDescriptor expectedClassDescriptor = DescriptorFactory.createClassDescriptorFromSignature(lhsSig);
ClassDescriptor actualClassDescriptor = DescriptorFactory.createClassDescriptorFromSignature(rhsSig);
targets.addAll(Hierarchy2.resolveVirtualMethodCallTargets(expectedClassDescriptor, "equals", "(Ljava/lang/Object;)Z",
false, false));
allOk = targets.size() > 0;
for (XMethod m2 : targets)
if (!classSummary.mightBeEqualTo(m2.getClassDescriptor(), actualClassDescriptor))
allOk = false;
} catch (ClassNotFoundException e) {
AnalysisContext.reportMissingClass(e);
}
return allOk;
}
public void report() {
// do nothing
}
}
// vim:ts=3