/*
* fb-contrib - Auxiliary detectors for Java programs
* Copyright (C) 2005-2017 Dave Brosius
*
* 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 com.mebigfatguy.fbcontrib.detect;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import com.mebigfatguy.fbcontrib.utils.BugType;
import com.mebigfatguy.fbcontrib.utils.QMethod;
import com.mebigfatguy.fbcontrib.utils.SignatureBuilder;
import com.mebigfatguy.fbcontrib.utils.SignatureUtils;
import com.mebigfatguy.fbcontrib.utils.ToString;
import com.mebigfatguy.fbcontrib.utils.UnmodifiableSet;
import com.mebigfatguy.fbcontrib.utils.Values;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.BytecodeScanningDetector;
import edu.umd.cs.findbugs.FieldAnnotation;
import edu.umd.cs.findbugs.OpcodeStack;
import edu.umd.cs.findbugs.SourceLineAnnotation;
import edu.umd.cs.findbugs.ba.ClassContext;
import edu.umd.cs.findbugs.ba.XField;
/**
* looks for fields that are implementations of java.util.List, but that are used in a set-like fashion. Since lookup type operations are performed using a
* linear search for Lists, the performance for large Lists will be poor. Consideration should be made as to whether these fields should be sets. In the case
* that order is important, consider using LinkedHashSet.
*/
public class DubiousListCollection extends BytecodeScanningDetector {
private static final Set<QMethod> setMethods = UnmodifiableSet.create(
//@formatter:off
new QMethod("contains", SignatureBuilder.SIG_OBJECT_TO_BOOLEAN),
new QMethod("containsAll", SignatureBuilder.SIG_COLLECTION_TO_PRIMITIVE_BOOLEAN),
new QMethod("remove", SignatureBuilder.SIG_OBJECT_TO_OBJECT),
new QMethod("removeAll", SignatureBuilder.SIG_COLLECTION_TO_PRIMITIVE_BOOLEAN),
new QMethod("retainAll", SignatureBuilder.SIG_COLLECTION_TO_PRIMITIVE_BOOLEAN)
//@formatter:on
);
private static final Set<QMethod> listMethods = UnmodifiableSet.create(
//@formatter:off
new QMethod("add", new SignatureBuilder().withParamTypes(Values.SIG_PRIMITIVE_INT, Values.SLASHED_JAVA_LANG_OBJECT).toString()),
new QMethod("addAll", new SignatureBuilder().withParamTypes(Values.SIG_PRIMITIVE_INT, Values.SLASHED_JAVA_UTIL_COLLECTION).withReturnType(Values.SIG_PRIMITIVE_BOOLEAN).toString()),
new QMethod("lastIndexOf", new SignatureBuilder().withParamTypes(Values.SLASHED_JAVA_LANG_OBJECT).withReturnType(Values.SIG_PRIMITIVE_INT).toString()),
new QMethod("remove", SignatureBuilder.SIG_INT_TO_OBJECT),
new QMethod("set", new SignatureBuilder().withParamTypes(Values.SIG_PRIMITIVE_INT, Values.SLASHED_JAVA_LANG_OBJECT).withReturnType(Values.SLASHED_JAVA_LANG_OBJECT).toString()),
new QMethod("subList", new SignatureBuilder().withParamTypes(Values.SIG_PRIMITIVE_INT, Values.SIG_PRIMITIVE_INT).withReturnType(Values.SLASHED_JAVA_UTIL_LIST).toString()),
new QMethod("listIterator", new SignatureBuilder().withReturnType("java/util/ListIterator").toString()),
new QMethod("listIterator", new SignatureBuilder().withParamTypes(Values.SIG_PRIMITIVE_INT).withReturnType("java/util/ListIterator").toString())
// Theoretically get(i) and indexOf(Object) are list Methods but are so
// abused, as to be meaningless
//@formatter:on
);
private final BugReporter bugReporter;
private final OpcodeStack stack = new OpcodeStack();
private final Map<String, FieldInfo> fieldsReported = new HashMap<>(10);
/**
* constructs a DLC detector given the reporter to report bugs on
*
* @param bugReporter
* the sync of bug reports
*/
public DubiousListCollection(final BugReporter bugReporter) {
this.bugReporter = bugReporter;
}
/**
* overrides the visitor to accept classes that define List based fields
*
* @param classContext
* the context object for the currently parsed class
*/
@Override
public void visitClassContext(final ClassContext classContext) {
JavaClass cls = classContext.getJavaClass();
Field[] flds = cls.getFields();
for (Field f : flds) {
String sig = f.getSignature();
if (sig.startsWith(Values.SIG_QUALIFIED_CLASS_PREFIX)) {
if (sig.startsWith("Ljava/util/") && sig.endsWith("List;")) {
fieldsReported.put(f.getName(), new FieldInfo());
}
}
}
if (!fieldsReported.isEmpty()) {
super.visitClassContext(classContext);
reportBugs();
}
}
/**
* overrides the visitor to reset the opcode stack object
*
* @param obj
* the code object for the currently parse method
*/
@Override
public void visitCode(final Code obj) {
stack.resetForMethodEntry(this);
super.visitCode(obj);
}
/**
* overrides the visitor to record all method calls on List fields. If a method is not a set based method, remove it from further consideration
*
* @param seen
* the current opcode parsed.
*/
@Override
public void sawOpcode(final int seen) {
try {
stack.precomputation(this);
if (seen == INVOKEINTERFACE) {
processInvokeInterface();
} else if (seen == INVOKEVIRTUAL) {
processInvokeVirtual();
} else if ((seen == ARETURN) && (stack.getStackDepth() > 0)) {
OpcodeStack.Item item = stack.getStackItem(0);
XField field = item.getXField();
if (field != null) {
String fieldName = field.getName();
fieldsReported.remove(fieldName);
}
}
} finally {
stack.sawOpcode(this, seen);
}
}
private void processInvokeInterface() {
String className = this.getClassConstantOperand();
if (className.startsWith("java/util/") && className.endsWith("List")) {
String signature = getSigConstantOperand();
XField field = getFieldFromStack(stack, signature);
if (field != null) {
String fieldName = field.getName();
FieldInfo fi = fieldsReported.get(fieldName);
if (fi != null) {
String methodName = getNameConstantOperand();
QMethod methodInfo = new QMethod(methodName, signature);
if (listMethods.contains(methodInfo)) {
fieldsReported.remove(fieldName);
} else if (setMethods.contains(methodInfo)) {
fi.addUse(getPC());
}
}
}
}
}
private void processInvokeVirtual() {
String className = getClassConstantOperand();
if (className.startsWith("java/util/") && className.endsWith("List")) {
XField field = getFieldFromStack(stack, getSigConstantOperand());
if (field != null) {
String fieldName = field.getName();
fieldsReported.remove(fieldName);
}
}
}
/**
* return the field object that the current method was called on, by finding the reference down in the stack based on the number of parameters
*
* @param stk
* the opcode stack where fields are stored
* @param signature
* the signature of the called method
*
* @return the field annotation for the field whose method was executed
*/
private static XField getFieldFromStack(final OpcodeStack stk, final String signature) {
int parmCount = SignatureUtils.getNumParameters(signature);
if (stk.getStackDepth() > parmCount) {
OpcodeStack.Item itm = stk.getStackItem(parmCount);
return itm.getXField();
}
return null;
}
/**
* implements the detector, by reporting all remaining fields that only have set based access
*/
private void reportBugs() {
int major = getClassContext().getJavaClass().getMajor();
for (Map.Entry<String, FieldInfo> entry : fieldsReported.entrySet()) {
String field = entry.getKey();
FieldInfo fi = entry.getValue();
int cnt = fi.getSetCount();
if (cnt > 0) {
FieldAnnotation fa = getFieldAnnotation(field);
if (fa != null) {
// can't use LinkedHashSet in 1.3 so report at LOW
bugReporter
.reportBug(new BugInstance(this, BugType.DLC_DUBIOUS_LIST_COLLECTION.name(), (major >= MAJOR_1_4) ? NORMAL_PRIORITY : LOW_PRIORITY)
.addClass(this).addField(fa).addSourceLine(fi.getSourceLineAnnotation()));
}
}
}
}
/**
* builds a field annotation by finding the field in the classes' field list
*
* @param fieldName
* the field for which to built the field annotation
*
* @return the field annotation of the specified field
*/
private FieldAnnotation getFieldAnnotation(final String fieldName) {
JavaClass cls = getClassContext().getJavaClass();
Field[] fields = cls.getFields();
for (Field f : fields) {
if (f.getName().equals(fieldName)) {
return new FieldAnnotation(cls.getClassName(), fieldName, f.getSignature(), f.isStatic());
}
}
return null; // shouldn't happen
}
/**
* holds information about fields and keeps counts of set methods called on them
*/
class FieldInfo {
private int setCnt = 0;
private SourceLineAnnotation slAnnotation = null;
/**
* increments the number of times this field has a set method called on it
*
* @param pc
* the current instruction offset
*/
public void addUse(final int pc) {
setCnt++;
if (slAnnotation == null) {
slAnnotation = SourceLineAnnotation.fromVisitedInstruction(DubiousListCollection.this.getClassContext(), DubiousListCollection.this, pc);
}
}
public SourceLineAnnotation getSourceLineAnnotation() {
return slAnnotation;
}
public int getSetCount() {
return setCnt;
}
@Override
public String toString() {
return ToString.build(this);
}
}
}