/*
* 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.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.bcel.Constants;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import com.mebigfatguy.fbcontrib.utils.BugType;
import com.mebigfatguy.fbcontrib.utils.SignatureUtils;
import com.mebigfatguy.fbcontrib.utils.StopOpcodeParsingException;
import com.mebigfatguy.fbcontrib.utils.TernaryPatcher;
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.XField;
/**
* looks for private methods that can only return one constant value. either the class should not return a value, or perhaps a branch was missed.
*/
@CustomUserValue
public class MethodReturnsConstant extends BytecodeScanningDetector {
private final BugReporter bugReporter;
private OpcodeStack stack;
private Integer returnRegister;
private Map<Integer, Object> registerConstants;
private Set<Method> overloadedMethods;
private Object returnConstant;
private int returnPC;
/**
* constructs a MRC detector given the reporter to report bugs on
*
* @param bugReporter
* the sync of bug reports
*/
public MethodReturnsConstant(BugReporter bugReporter) {
this.bugReporter = bugReporter;
}
/**
* implements the visitor to collect all methods that are overloads. These methods should be ignored, as you may differentiate constants based on parameter
* type, or value.
*
* @param classContext
* the currently parsed class object
*/
@Override
public void visitClassContext(ClassContext classContext) {
try {
stack = new OpcodeStack();
registerConstants = new HashMap<>();
overloadedMethods = collectOverloadedMethods(classContext.getJavaClass());
super.visitClassContext(classContext);
} finally {
stack = null;
registerConstants = null;
overloadedMethods = null;
}
}
/**
* implements the visitor to reset the stack and proceed for private methods
*
* @param obj
* the context object of the currently parsed code block
*/
@Override
public void visitCode(Code obj) {
Method m = getMethod();
if (overloadedMethods.contains(m)) {
return;
}
int aFlags = m.getAccessFlags();
if ((((aFlags & Constants.ACC_PRIVATE) != 0) || ((aFlags & Constants.ACC_STATIC) != 0)) && ((aFlags & Constants.ACC_SYNTHETIC) == 0)
&& (!m.getSignature().endsWith(")Z"))) {
stack.resetForMethodEntry(this);
returnRegister = Values.NEGATIVE_ONE;
returnConstant = null;
registerConstants.clear();
returnPC = -1;
try {
super.visitCode(obj);
if ((returnConstant != null)) {
BugInstance bi = new BugInstance(this, BugType.MRC_METHOD_RETURNS_CONSTANT.name(),
((aFlags & Constants.ACC_PRIVATE) != 0) ? NORMAL_PRIORITY : LOW_PRIORITY).addClass(this).addMethod(this);
if (returnPC >= 0) {
bi.addSourceLine(this, returnPC);
}
bi.addString(returnConstant.toString());
bugReporter.reportBug(bi);
}
} catch (StopOpcodeParsingException e) {
// method was not suspect
}
}
}
/**
* implements the visitor to look for methods that return a constant
*
* @param seen
* the opcode of the currently parsed instruction
*/
@Override
public void sawOpcode(int seen) {
boolean sawSBToString = false;
try {
stack.precomputation(this);
if ((seen >= IRETURN) && (seen <= ARETURN)) {
if (stack.getStackDepth() > 0) {
OpcodeStack.Item item = stack.getStackItem(0);
Integer register = Integer.valueOf(item.getRegisterNumber());
if (registerConstants.containsKey(register) && (registerConstants.get(register) == null)) {
throw new StopOpcodeParsingException();
}
String returnSig = item.getSignature();
if ((returnSig != null) && returnSig.startsWith(Values.SIG_ARRAY_PREFIX)) {
XField f = item.getXField();
if ((f == null) || (!f.isStatic())) {
throw new StopOpcodeParsingException();
}
}
Object constant = item.getConstant();
if (constant == null) {
throw new StopOpcodeParsingException();
}
if (Boolean.TRUE.equals(item.getUserValue()) && ("".equals(constant))) {
throw new StopOpcodeParsingException();
}
if ((returnConstant != null) && (!returnConstant.equals(constant))) {
throw new StopOpcodeParsingException();
}
returnRegister = Integer.valueOf(item.getRegisterNumber());
returnConstant = constant;
returnPC = getPC();
}
} else if ((seen == GOTO) || (seen == GOTO_W)) {
if (stack.getStackDepth() > 0) {
// Trinaries confuse us too much, if the code has a ternary well - oh well
throw new StopOpcodeParsingException();
}
} else if (seen == INVOKEVIRTUAL) {
String clsName = getClassConstantOperand();
if (SignatureUtils.isPlainStringConvertableClass(clsName)) {
sawSBToString = Values.TOSTRING.equals(getNameConstantOperand());
}
} else if (((seen >= ISTORE) && (seen <= ASTORE_3)) || (seen == IINC)) {
Integer register = Integer.valueOf(getRegisterOperand());
if ((returnRegister.intValue() != -1) && (register.equals(returnRegister))) {
throw new StopOpcodeParsingException();
}
if (stack.getStackDepth() > 0) {
OpcodeStack.Item item = stack.getStackItem(0);
Object constant = item.getConstant();
if (registerConstants.containsKey(register)) {
if ((constant == null) || !constant.equals(registerConstants.get(register))) {
registerConstants.put(register, null);
}
} else {
if (item.getSignature().contains(Values.SIG_ARRAY_PREFIX)) {
registerConstants.put(register, null);
} else {
registerConstants.put(register, constant);
}
}
} else {
registerConstants.put(register, null);
}
if (returnRegister.equals(register)) {
Object constant = registerConstants.get(returnRegister);
if (constant != null) {
throw new StopOpcodeParsingException();
}
}
}
} finally {
TernaryPatcher.pre(stack, seen);
stack.sawOpcode(this, seen);
TernaryPatcher.post(stack, seen);
if (sawSBToString && (stack.getStackDepth() > 0)) {
OpcodeStack.Item item = stack.getStackItem(0);
item.setUserValue(Boolean.TRUE);
}
}
}
/**
* adds all methods of a class that are overloaded to a set. This method is O(nlogn) so for large classes might be slow. Assuming on average it's better
* than other choices. When a match is found in the j index, it is removed from the array, so it is not scanned with the i index
*
* @param cls
* the class to look for overloaded methods
* @return the set of methods that are overloaded
*/
private Set<Method> collectOverloadedMethods(JavaClass cls) {
Set<Method> overloads = new HashSet<>();
Method[] methods = cls.getMethods();
int numMethods = methods.length;
for (int i = 0; i < numMethods; i++) {
boolean foundOverload = false;
for (int j = i + 1; j < numMethods; j++) {
if (methods[i].getName().equals(methods[j].getName())) {
overloads.add(methods[j]);
foundOverload = true;
}
}
if (foundOverload) {
overloads.add(methods[i]);
}
}
return overloads;
}
}