src.main.java.com.mebigfatguy.fbcontrib.detect.PossiblyRedundantMethodCalls Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fb-contrib Show documentation
Show all versions of fb-contrib Show documentation
An auxiliary findbugs.sourceforge.net plugin for java bug detectors that fall outside the narrow scope of detectors to be packaged with the product itself.
/*
* fb-contrib - Auxiliary detectors for Java programs
* Copyright (C) 2005-2018 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.Arrays;
import java.util.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.CodeException;
import org.apache.bcel.classfile.LineNumber;
import org.apache.bcel.classfile.LineNumberTable;
import com.mebigfatguy.fbcontrib.collect.MethodInfo;
import com.mebigfatguy.fbcontrib.collect.Statistics;
import com.mebigfatguy.fbcontrib.utils.BugType;
import com.mebigfatguy.fbcontrib.utils.FQMethod;
import com.mebigfatguy.fbcontrib.utils.OpcodeUtils;
import com.mebigfatguy.fbcontrib.utils.RegisterUtils;
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.OpcodeStack;
import edu.umd.cs.findbugs.OpcodeStack.CustomUserValue;
import edu.umd.cs.findbugs.ba.ClassContext;
import edu.umd.cs.findbugs.ba.XFactory;
import edu.umd.cs.findbugs.ba.XField;
import edu.umd.cs.findbugs.ba.XMethod;
/**
* looks for calls of the same method on the same object when that object hasn't changed. This often is redundant, and the second call can be removed, or
* combined.
*/
@CustomUserValue
public class PossiblyRedundantMethodCalls extends BytecodeScanningDetector {
public static final String PRMC_RISKY_FIELD_USER_KEY = "fbcontrib.PRMC.riskynames";
public static final String PRMC_RISKY_CLASS_USER_KEY = "fbcontrib.PRMC.riskyclasses";
public static final String PRMC_HIGH_BYTECOUNT = "fbcontrib.PRMC.highbytecount";
public static final String PRMC_HIGH_METHODCALLS = "fbcontrib.PRMC.highmethodcalls";
public static final String PRMC_NORMAL_BYTECOUNT = "fbcontrib.PRMC.normalbytecount";
public static final String PRMC_NORMAL_METHODCALLS = "fbcontrib.PRMC.normalmethodcalls";
public static final String PRMC_LOW_BYTECOUNT = "fbcontrib.PRMC.lowbytecount";
public static final String PRMC_LOW_METHODCALLS = "fbcontrib.PRMC.lowmethodcalls";
/**
* a collection of names that are to be checked against a currently parsed method, to see if that method is risky to be called redundant. The contents are
* either
*
* - a simple name that can be found as part of the methodName, like "destroy" which would match destroy(), or destroyAll()
* - a fully qualified method name that exactly matches a method, like "java/lang/String.valueOf"
*
*/
private static Set riskyMethodNameContents = new HashSet<>();
private static int highByteCountLimit = 200;
private static int highMethodCallLimit = 10;
private static int normalByteCountLimit = 75;
private static int normalMethodCallLimit = 4;
private static int lowByteCountLimit = 10;
private static int lowMethodCallLimit = 1;
static {
riskyMethodNameContents.add("next");
riskyMethodNameContents.add("add");
riskyMethodNameContents.add("create");
riskyMethodNameContents.add("append");
riskyMethodNameContents.add("find");
riskyMethodNameContents.add("put");
riskyMethodNameContents.add("remove");
riskyMethodNameContents.add("read");
riskyMethodNameContents.add("write");
riskyMethodNameContents.add("push");
riskyMethodNameContents.add("pop");
riskyMethodNameContents.add("scan");
riskyMethodNameContents.add("skip");
riskyMethodNameContents.add("clone");
riskyMethodNameContents.add("close");
riskyMethodNameContents.add("copy");
riskyMethodNameContents.add("currentTimeMillis");
riskyMethodNameContents.add("nanoTime");
riskyMethodNameContents.add("newInstance");
riskyMethodNameContents.add("noneOf");
riskyMethodNameContents.add("allOf");
riskyMethodNameContents.add("random");
riskyMethodNameContents.add("beep");
riskyMethodNameContents.add("emptyList");
riskyMethodNameContents.add("emptySet");
riskyMethodNameContents.add("emptyMap");
String userNameProp = System.getProperty(PRMC_RISKY_FIELD_USER_KEY);
if (userNameProp != null) {
String[] userNames = userNameProp.split(Values.WHITESPACE_COMMA_SPLIT);
for (String name : userNames) {
riskyMethodNameContents.add(name);
}
}
Integer prop = Integer.getInteger(PRMC_HIGH_BYTECOUNT);
if (prop != null) {
highByteCountLimit = prop.intValue();
}
prop = Integer.getInteger(PRMC_HIGH_METHODCALLS);
if (prop != null) {
highMethodCallLimit = prop.intValue();
}
prop = Integer.getInteger(PRMC_NORMAL_BYTECOUNT);
if (prop != null) {
normalByteCountLimit = prop.intValue();
}
prop = Integer.getInteger(PRMC_NORMAL_METHODCALLS);
if (prop != null) {
normalMethodCallLimit = prop.intValue();
}
prop = Integer.getInteger(PRMC_LOW_BYTECOUNT);
if (prop != null) {
lowByteCountLimit = prop.intValue();
}
prop = Integer.getInteger(PRMC_LOW_METHODCALLS);
if (prop != null) {
lowMethodCallLimit = prop.intValue();
}
}
private static Set riskyClassNames = new HashSet<>();
static {
riskyClassNames.add("java/nio/ByteBuffer");
riskyClassNames.add("java/io/DataInputStream");
riskyClassNames.add("java/io/ObjectInputStream");
riskyClassNames.add("java/util/Calendar");
riskyClassNames.add("java/util/stream/Collectors");
riskyClassNames.add("com/google/common/collect/Lists");
riskyClassNames.add("com/google/common/collect/Sets");
riskyClassNames.add("com/google/common/collect/Maps");
riskyClassNames.add("com/google/common/collect/Queues");
String userNameProp = System.getProperty(PRMC_RISKY_CLASS_USER_KEY);
if (userNameProp != null) {
String[] userNames = userNameProp.split(Values.WHITESPACE_COMMA_SPLIT);
for (String name : userNames) {
riskyClassNames.add(name);
}
}
}
private static final Set commonMethods = UnmodifiableSet.create(
// @formatter:off
new FQMethod("java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;"),
new FQMethod("java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;"),
new FQMethod("java/lang/Character", "valueOf", "(C)Ljava/lang/Character;"),
new FQMethod("java/lang/Short", "valueOf", "(S)Ljava/lang/Short;"),
new FQMethod("java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;"),
new FQMethod("java/lang/Long", "valueOf", "(J)Ljava/lang/Long;"),
new FQMethod("java/lang/Float", "valueOf", "(F)Ljava/lang/Float;"),
new FQMethod("java/lang/Double", "valueOf", "(D)Ljava/lang/Double;"),
new FQMethod("java/lang/Boolean", "booleanValue", "()Z"),
new FQMethod("java/lang/Byte", "byteValue", "()B"),
new FQMethod("java/lang/Character", "charValue", "()C"),
new FQMethod("java/lang/Short", "shortValue", "()S"),
new FQMethod("java/lang/Integer", "intValue", "()I"),
new FQMethod("java/lang/Long", "longValue", "()J"),
new FQMethod("java/lang/Float", "floatValue", "()F"),
new FQMethod("java/lang/Double", "doubleValue", "()D")
// @formatter:on
);
private final BugReporter bugReporter;
private OpcodeStack stack = null;
private Map localMethodCalls = null;
private Map fieldMethodCalls = null;
private Map staticMethodCalls = null;
private BitSet branchTargets = null;
/**
* constructs a PRMC detector given the reporter to report bugs on
*
* @param bugReporter
* the sync of bug reports
*/
public PossiblyRedundantMethodCalls(BugReporter bugReporter) {
this.bugReporter = bugReporter;
}
/**
* implements the visitor to create and clear the stack, method call maps, and branch targets
*
* @param classContext
* the context object of the currently visited class
*/
@Override
public void visitClassContext(ClassContext classContext) {
try {
stack = new OpcodeStack();
localMethodCalls = new HashMap<>();
fieldMethodCalls = new HashMap<>();
staticMethodCalls = new HashMap<>();
branchTargets = new BitSet();
super.visitClassContext(classContext);
} finally {
stack = null;
localMethodCalls = null;
fieldMethodCalls = null;
staticMethodCalls = null;
branchTargets = null;
}
}
/**
* implements the visitor to reset the stack, and method call maps for new method Note: that when collecting branch targets, it's unfortunately not good
* enough to just collect the handler pcs, as javac plays fast and loose, and will sometimes jam code below the end pc and before the first handler pc,
* which gets executed. So we need to clear our maps if we go past the end pc as well.
*
* @param obj
* the context object of the currently parsed code block
*/
@Override
public void visitCode(Code obj) {
stack.resetForMethodEntry(this);
localMethodCalls.clear();
fieldMethodCalls.clear();
staticMethodCalls.clear();
branchTargets.clear();
CodeException[] codeExceptions = obj.getExceptionTable();
for (CodeException codeEx : codeExceptions) {
// adding the end pc seems silly, but it is need because javac may repeat
// part of the finally block in the try block, at times.
branchTargets.set(codeEx.getEndPC());
branchTargets.set(codeEx.getHandlerPC());
}
super.visitCode(obj);
}
/**
* implements the visitor to look for repetitive calls to the same method on the same object using the same constant parameters. These methods must return a
* value.
*
* @param seen
* the opcode of the currently parsed instruction
*/
@Override
public void sawOpcode(int seen) {
String userValue = null;
try {
stack.precomputation(this);
int pc = getPC();
if (branchTargets.get(pc)) {
localMethodCalls.clear();
fieldMethodCalls.clear();
branchTargets.clear(pc);
}
if (((seen >= IFEQ) && (seen <= GOTO)) || ((seen >= IFNULL) && (seen <= GOTO_W))) {
branchTargets.set(getBranchTarget());
} else if ((seen == TABLESWITCH) || (seen == LOOKUPSWITCH)) {
int[] offsets = getSwitchOffsets();
for (int offset : offsets) {
branchTargets.set(offset + pc);
}
branchTargets.set(getDefaultSwitchOffset() + pc);
} else if (OpcodeUtils.isAStore(seen)) {
localMethodCalls.remove(Integer.valueOf(RegisterUtils.getAStoreReg(this, seen)));
} else if (seen == PUTFIELD) {
String fieldSource = "";
if (stack.getStackDepth() > 0) {
OpcodeStack.Item item = stack.getStackItem(0);
fieldSource = (String) item.getUserValue();
if (fieldSource == null) {
fieldSource = "";
}
}
fieldMethodCalls.remove(new FieldInfo(fieldSource, getNameConstantOperand()));
} else if (seen == GETFIELD) {
if (stack.getStackDepth() > 0) {
OpcodeStack.Item item = stack.getStackItem(0);
userValue = (String) item.getUserValue();
if (userValue == null) {
int reg = item.getRegisterNumber();
if (reg >= 0) {
userValue = String.valueOf(reg);
} else {
XField xf = item.getXField();
if (xf != null) {
userValue = xf.getName();
}
}
}
}
} else if ((seen == INVOKEVIRTUAL) || (seen == INVOKEINTERFACE) || (seen == INVOKESTATIC)) {
String className = getClassConstantOperand();
String methodName = getNameConstantOperand();
String signature = getSigConstantOperand();
int parmCount = SignatureUtils.getNumParameters(signature);
int reg = -1;
XField field = null;
MethodCall mc = null;
String fieldSource = null;
if (seen == INVOKESTATIC) {
XMethod xm = XFactory.createXMethod(getDottedClassConstantOperand(), methodName, signature, true);
String genericSignature = xm.getSourceSignature();
if ((genericSignature != null) && genericSignature.endsWith(">;")) {
return;
}
} else if (stack.getStackDepth() > parmCount) {
OpcodeStack.Item obj = stack.getStackItem(parmCount);
reg = obj.getRegisterNumber();
field = obj.getXField();
if (reg >= 0) {
mc = localMethodCalls.get(Integer.valueOf(reg));
MethodInfo mi = Statistics.getStatistics().getMethodStatistics(className, getNameConstantOperand(), signature);
if ((mi != null) && mi.getModifiesState()) {
clearFieldMethods(String.valueOf(reg));
return;
}
} else if (field != null) {
fieldSource = (String) obj.getUserValue();
if (fieldSource == null) {
fieldSource = "";
}
mc = fieldMethodCalls.get(new FieldInfo(fieldSource, field.getName()));
MethodInfo mi = Statistics.getStatistics().getMethodStatistics(className, getNameConstantOperand(), signature);
if ((mi != null) && mi.getModifiesState()) {
clearFieldMethods(fieldSource);
return;
}
}
}
int neededStackSize = parmCount + ((seen == INVOKESTATIC) ? 0 : 1);
if (stack.getStackDepth() >= neededStackSize) {
Object[] parmConstants = new Object[parmCount];
for (int i = 0; i < parmCount; i++) {
OpcodeStack.Item parm = stack.getStackItem(i);
parmConstants[i] = parm.getConstant();
if (parm.getSignature().startsWith(Values.SIG_ARRAY_PREFIX)) {
if (!Values.ZERO.equals(parm.getConstant())) {
return;
}
XField f = parm.getXField();
if (f != null) {
// Two different fields holding a 0 length array should be considered different
parmConstants[i] = f.getName() + ':' + parmConstants[i];
}
}
if (parmConstants[i] == null) {
return;
}
}
if (seen == INVOKESTATIC) {
mc = staticMethodCalls.get(className);
} else if ((reg < 0) && (field == null)) {
return;
}
if (mc != null) {
if (!signature.endsWith(Values.SIG_VOID) && methodName.equals(mc.getName()) && signature.equals(mc.getSignature())
&& !isRiskyName(className, methodName) && !commonMethods.contains(new FQMethod(className, methodName, signature))) {
Object[] parms = mc.getParms();
if (Arrays.equals(parms, parmConstants)) {
int ln = getLineNumber(pc);
if ((ln != mc.getLineNumber()) || (Math.abs(pc - mc.getPC()) < 10)) {
Statistics statistics = Statistics.getStatistics();
MethodInfo mi = statistics.getMethodStatistics(className, methodName, signature);
bugReporter.reportBug(
new BugInstance(this, BugType.PRMC_POSSIBLY_REDUNDANT_METHOD_CALLS.name(), getBugPriority(methodName, mi))
.addClass(this).addMethod(this).addSourceLine(this).addString(methodName + signature));
}
}
}
if (seen == INVOKESTATIC) {
staticMethodCalls.remove(className);
} else {
if (reg >= 0) {
localMethodCalls.remove(Integer.valueOf(reg));
} else if (fieldSource != null) {
fieldMethodCalls.remove(new FieldInfo(fieldSource, field.getName()));
}
}
} else {
int ln = getLineNumber(pc);
if (seen == INVOKESTATIC) {
staticMethodCalls.put(className, new MethodCall(methodName, signature, parmConstants, pc, ln));
} else {
if (reg >= 0) {
localMethodCalls.put(Integer.valueOf(reg), new MethodCall(methodName, signature, parmConstants, pc, ln));
} else if (field != null) {
OpcodeStack.Item obj = stack.getStackItem(parmCount);
fieldSource = (String) obj.getUserValue();
if (fieldSource == null) {
fieldSource = "";
}
fieldMethodCalls.put(new FieldInfo(fieldSource, field.getName()), new MethodCall(methodName, signature, parmConstants, pc, ln));
}
}
}
}
} else if (OpcodeUtils.isReturn(seen)) {
localMethodCalls.clear();
fieldMethodCalls.clear();
branchTargets.clear(pc);
}
} finally {
stack.sawOpcode(this, seen);
if ((userValue != null) && (stack.getStackDepth() > 0)) {
OpcodeStack.Item item = stack.getStackItem(0);
item.setUserValue(userValue);
}
}
}
private void clearFieldMethods(String fieldSource) {
Iterator it = fieldMethodCalls.keySet().iterator();
while (it.hasNext()) {
if (it.next().hasFieldSource(fieldSource)) {
it.remove();
}
}
}
/**
* returns the bug priority based on metrics about the method
*
* @param methodName
* TODO
* @param mi
* metrics about the method
* @return the bug priority
*/
private static int getBugPriority(String methodName, MethodInfo mi) {
if (Values.STATIC_INITIALIZER.equals(methodName)) {
return LOW_PRIORITY;
}
if ((mi.getNumBytes() >= highByteCountLimit) || (mi.getNumMethodCalls() >= highMethodCallLimit)) {
return HIGH_PRIORITY;
}
if ((mi.getNumBytes() >= normalByteCountLimit) || (mi.getNumMethodCalls() >= normalMethodCallLimit)) {
return NORMAL_PRIORITY;
}
if ((mi.getNumBytes() >= lowByteCountLimit) || (mi.getNumMethodCalls() >= lowMethodCallLimit)) {
return LOW_PRIORITY;
}
return EXP_PRIORITY;
}
/**
* returns true if the class or method name contains a pattern that is considered likely to be this modifying
*
* @param className
* the class name to check
* @param methodName
* the method name to check
* @return whether the method sounds like it modifies this
*/
private static boolean isRiskyName(String className, String methodName) {
if (riskyClassNames.contains(className)) {
return true;
}
String qualifiedMethodName = className + '.' + methodName;
if (riskyMethodNameContents.contains(qualifiedMethodName)) {
return true;
}
for (String riskyName : riskyMethodNameContents) {
if (methodName.indexOf(riskyName) >= 0) {
return true;
}
}
return methodName.indexOf(Values.SYNTHETIC_MEMBER_CHAR) >= 0;
}
/**
* returns the source line number for the pc, or just the pc if the line number table doesn't exist
*
* @param pc
* current pc
* @return the line number
*/
private int getLineNumber(int pc) {
LineNumberTable lnt = getMethod().getLineNumberTable();
if (lnt == null) {
return pc;
}
LineNumber[] lns = lnt.getLineNumberTable();
if (lns == null) {
return pc;
}
if (pc > lns[lns.length - 1].getStartPC()) {
return lns[lns.length - 1].getLineNumber();
}
int lo = 0;
int hi = lns.length - 2;
int mid = 0;
while (lo <= hi) {
mid = (lo + hi) >>> 1;
if (pc < lns[mid].getStartPC()) {
hi = mid - 1;
} else if (pc >= lns[mid + 1].getStartPC()) {
lo = mid + 1;
} else {
break;
}
}
return lns[mid].getLineNumber();
}
/**
* contains information about a field access
*/
static class FieldInfo {
private final String fieldSource;
private final String fieldName;
public FieldInfo(String source, String name) {
fieldSource = source;
fieldName = name;
}
public boolean hasFieldSource(String source) {
return fieldSource.equals(source);
}
@Override
public int hashCode() {
return fieldSource.hashCode() ^ fieldName.hashCode();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof FieldInfo)) {
return false;
}
FieldInfo that = (FieldInfo) o;
return fieldSource.equals(that.fieldSource) && fieldName.equals(that.fieldName);
}
@Override
public String toString() {
return ToString.build(this);
}
}
/**
* contains information about a method call
*/
static class MethodCall {
private final String methodName;
private final String methodSignature;
private final Object[] methodParms;
private final int methodPC;
private final int methodLineNumber;
public MethodCall(String name, String signature, Object[] parms, int pc, int lineNumber) {
methodName = name;
methodSignature = signature;
methodParms = parms;
methodPC = pc;
methodLineNumber = lineNumber;
}
public String getName() {
return methodName;
}
public String getSignature() {
return methodSignature;
}
public Object[] getParms() {
return methodParms;
}
public int getPC() {
return methodPC;
}
public int getLineNumber() {
return methodLineNumber;
}
}
}