src.main.java.com.mebigfatguy.fbcontrib.detect.AnnotationIssues 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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.bcel.Constants;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import com.mebigfatguy.fbcontrib.collect.MethodInfo;
import com.mebigfatguy.fbcontrib.collect.Statistics;
import com.mebigfatguy.fbcontrib.utils.BugType;
import com.mebigfatguy.fbcontrib.utils.CodeByteUtils;
import com.mebigfatguy.fbcontrib.utils.FQMethod;
import com.mebigfatguy.fbcontrib.utils.OpcodeUtils;
import com.mebigfatguy.fbcontrib.utils.SignatureBuilder;
import com.mebigfatguy.fbcontrib.utils.StopOpcodeParsingException;
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.XMethod;
import edu.umd.cs.findbugs.internalAnnotations.SlashedClassName;
/**
* looks for common problems with the application of annotations
*/
@CustomUserValue
public class AnnotationIssues extends BytecodeScanningDetector {
private static final String USER_NULLABLE_ANNOTATIONS = "fb-contrib.ai.annotations";
private static final Set IS_EMPTY_SIGNATURES = UnmodifiableSet.create(
// @formatter:off
new SignatureBuilder().withParamTypes(Collection.class).withReturnType(boolean.class).build(),
new SignatureBuilder().withParamTypes(Map.class).withReturnType(boolean.class).build()
// @formatter:on
);
private static final Set NULLABLE_ANNOTATIONS = new HashSet<>();
static {
NULLABLE_ANNOTATIONS.add("Lorg/jetbrains/annotations/Nullable;");
NULLABLE_ANNOTATIONS.add("Ljavax/annotation/Nullable;");
NULLABLE_ANNOTATIONS.add("Ljavax/annotation/CheckForNull;");
NULLABLE_ANNOTATIONS.add("Lcom/sun/istack/Nullable;");
NULLABLE_ANNOTATIONS.add("Ledu/umd/cs/findbugs/annotations/Nullable;");
NULLABLE_ANNOTATIONS.add("Lorg/springframework/lang/Nullable;");
NULLABLE_ANNOTATIONS.add("Landroid/support/annotation/Nullable");
String userAnnotations = System.getProperty(USER_NULLABLE_ANNOTATIONS);
if ((userAnnotations != null) && !userAnnotations.isEmpty()) {
String[] annotations = userAnnotations.split(Values.WHITESPACE_COMMA_SPLIT);
for (String annotation : annotations) {
NULLABLE_ANNOTATIONS.add("L" + annotation.replace('.', '/') + ";");
}
}
}
private static final Set NOTABLE_EXCEPTIONS = UnmodifiableSet.create(
// @formatter:off
new FQMethod(Values.SLASHED_JAVA_LANG_CLASS, "newInstance", SignatureBuilder.SIG_VOID_TO_OBJECT)
// @formatter:on
);
public static class AIUserValue {
int reg;
public AIUserValue(int reg) {
this.reg = reg;
}
@Override
public String toString() {
return ToString.build(this);
}
};
private BugReporter bugReporter;
private Map assumedNullTill;
private Map assumedNonNullTill;
private Set noAssumptionsPossible;
private List branchTargets;
private OpcodeStack stack;
private boolean methodIsNullable;
/**
* constructs a AI detector given the reporter to report bugs on
*
* @param bugReporter
* the sync of bug reports
*/
public AnnotationIssues(BugReporter bugReporter) {
this.bugReporter = bugReporter;
}
public boolean isCollecting() {
return false;
}
@Override
public void visitClassContext(ClassContext classContext) {
try {
JavaClass cls = classContext.getJavaClass();
if (cls.getMajor() >= Constants.MAJOR_1_5) {
if (isCollecting() || !cls.isAnonymous()) {
stack = new OpcodeStack();
assumedNullTill = new HashMap<>();
assumedNonNullTill = new HashMap<>();
noAssumptionsPossible = new HashSet<>();
branchTargets = new ArrayList<>();
super.visitClassContext(classContext);
}
}
} finally {
stack = null;
assumedNullTill = null;
assumedNonNullTill = null;
noAssumptionsPossible = null;
branchTargets = null;
}
}
@Override
public void visitCode(Code obj) {
Method method = getMethod();
String sig = method.getSignature();
String returnType = sig.substring(sig.indexOf(')') + 1);
char returnTypeChar = returnType.charAt(0);
if ((returnTypeChar != 'L') && (returnTypeChar != '[')) {
return;
}
if (method.isSynthetic() && !isCollecting()) {
return;
}
if (Values.SIG_JAVA_LANG_VOID.equals(returnType)) {
return;
}
if (NOTABLE_EXCEPTIONS.contains(new FQMethod(getClassName(), method.getName(), sig))) {
MethodInfo methodInfo = Statistics.getStatistics().getMethodStatistics(getClassName(), method.getName(), method.getSignature());
methodInfo.setCanReturnNull(false);
return;
}
if (methodHasNullableAnnotation(method)) {
if (isCollecting()) {
MethodInfo methodInfo = Statistics.getStatistics().getMethodStatistics(getClassName(), method.getName(), method.getSignature());
methodInfo.setCanReturnNull(true);
}
return;
}
MethodInfo methodInfo = Statistics.getStatistics().getMethodStatistics(getClassName(), method.getName(), method.getSignature());
if (!isCollecting() && methodInfo.getCanReturnNull()) {
bugReporter.reportBug(new BugInstance(this, BugType.AI_ANNOTATION_ISSUES_NEEDS_NULLABLE.name(), LOW_PRIORITY).addClass(this).addMethod(this));
} else {
methodIsNullable = false;
stack.resetForMethodEntry(this);
assumedNullTill.clear();
assumedNonNullTill.clear();
noAssumptionsPossible.clear();
branchTargets.clear();
try {
super.visitCode(obj);
} catch (StopOpcodeParsingException e) {
}
if (methodIsNullable) {
if (isCollecting()) {
methodInfo.setCanReturnNull(true);
} else {
bugReporter
.reportBug(new BugInstance(this, BugType.AI_ANNOTATION_ISSUES_NEEDS_NULLABLE.name(), LOW_PRIORITY).addClass(this).addMethod(this));
}
}
}
}
@Override
public void sawOpcode(int seen) {
AIUserValue userValue = null;
if (OpcodeUtils.isBranch(seen) && (getBranchOffset() > 0)) {
branchTargets.add(getBranchTarget());
Collections.sort(branchTargets);
}
clearBranchTargets(getPC());
convertNullToNonNull(getPC());
clearAssumptions(assumedNullTill, getPC());
clearAssumptions(assumedNonNullTill, getPC());
if (OpcodeUtils.isBranch(seen) && (getBranchOffset() > 0)) {
branchTargets.add(getBranchTarget());
Collections.sort(branchTargets);
}
try {
switch (seen) {
case ARETURN: {
if (!methodIsNullable && (stack.getStackDepth() > 0)) {
OpcodeStack.Item itm = stack.getStackItem(0);
Integer reg = Integer.valueOf(itm.getRegisterNumber());
methodIsNullable = !assumedNonNullTill.containsKey(reg) && (!noAssumptionsPossible.contains(reg)
&& ((assumedNullTill.containsKey(reg)) || isStackElementNullable(getClassName(), getMethod(), itm)));
if (methodIsNullable) {
throw new StopOpcodeParsingException();
}
}
break;
}
case IFNONNULL:
if (getBranchOffset() > 0) {
if (stack.getStackDepth() > 0) {
OpcodeStack.Item itm = stack.getStackItem(0);
int reg = itm.getRegisterNumber();
if (reg >= 0) {
assumedNullTill.put(reg, getBranchTarget());
}
}
}
break;
case IFNULL:
if (getBranchOffset() > 0) {
if (stack.getStackDepth() > 0) {
OpcodeStack.Item itm = stack.getStackItem(0);
int reg = itm.getRegisterNumber();
if (reg >= 0) {
assumedNonNullTill.put(reg, getBranchTarget());
}
}
}
break;
case IFEQ:
if ((getBranchOffset() > 0) && (stack.getStackDepth() > 0)) {
OpcodeStack.Item itm = stack.getStackItem(0);
AIUserValue uv = (AIUserValue) itm.getUserValue();
if ((uv != null) && (uv.reg >= 0)) {
assumedNullTill.put(uv.reg, getBranchTarget());
}
}
break;
case INVOKESTATIC:
if (stack.getStackDepth() > 0) {
String signature = getSigConstantOperand();
if (IS_EMPTY_SIGNATURES.contains(signature)) {
String methodName = getNameConstantOperand();
if (methodName.equals("isEmpty")) {
OpcodeStack.Item item = stack.getStackItem(0);
int reg = item.getRegisterNumber();
if (reg >= 0) {
userValue = new AIUserValue(reg);
break;
}
}
}
}
// $FALL-THROUGH$
case INVOKEINTERFACE:
case INVOKEVIRTUAL: {
boolean resultIsNullable = (isMethodNullable(getClassConstantOperand(), getNameConstantOperand(), getSigConstantOperand()));
if (resultIsNullable) {
userValue = new AIUserValue(-1);
}
break;
}
case ATHROW: {
removeAssumptions(assumedNonNullTill);
removeAssumptions(assumedNullTill);
break;
}
}
} finally {
stack.sawOpcode(this, seen);
if ((userValue != null) && (stack.getStackDepth() > 0)) {
OpcodeStack.Item itm = stack.getStackItem(0);
itm.setUserValue(userValue);
}
}
}
public static boolean methodHasNullableAnnotation(Method m) {
for (AnnotationEntry entry : m.getAnnotationEntries()) {
String annotationType = entry.getAnnotationType();
if (NULLABLE_ANNOTATIONS.contains(annotationType)) {
return true;
}
}
return false;
}
public static boolean isStackElementNullable(String className, Method method, OpcodeStack.Item itm) {
if (itm.isNull() || (itm.getUserValue() != null)) {
MethodInfo mi = Statistics.getStatistics().getMethodStatistics(className, method.getName(), method.getSignature());
if (mi != null) {
mi.setCanReturnNull(true);
}
return true;
} else {
XMethod xm = itm.getReturnValueOf();
if (xm != null) {
MethodInfo mi = Statistics.getStatistics().getMethodStatistics(xm.getClassName().replace('.', '/'), xm.getName(), xm.getSignature());
if ((mi != null) && mi.getCanReturnNull()) {
mi = Statistics.getStatistics().getMethodStatistics(className, method.getName(), method.getSignature());
if (mi != null) {
mi.setCanReturnNull(true);
}
return true;
}
}
}
return false;
}
public static boolean isMethodNullable(@SlashedClassName String className, String methodName, String methodSignature) {
char returnTypeChar = methodSignature.charAt(methodSignature.indexOf(')') + 1);
if ((returnTypeChar != 'L') && (returnTypeChar != '[')) {
return false;
}
MethodInfo mi = Statistics.getStatistics().getMethodStatistics(className, methodName, methodSignature);
return ((mi != null) && mi.getCanReturnNull());
// can we check if it has @Nullable on it? hmm need to convert to Method
}
/**
* the map is keyed by register, and value by when an assumption holds to a byte offset if we have passed when the assumption holds, clear the item from the
* map
*
* @param assumptionTill
* the map of assumptions
* @param pc
* // * the current pc
*/
public static void clearAssumptions(Map assumptionTill, int pc) {
Iterator it = assumptionTill.values().iterator();
while (it.hasNext()) {
if (it.next().intValue() <= pc) {
it.remove();
}
}
}
public void convertNullToNonNull(int pc) {
for (Map.Entry entry : assumedNullTill.entrySet()) {
if (entry.getValue().intValue() == pc) {
int lastOp = getPrevOpcode(1);
if ((lastOp == ARETURN) || (lastOp == ATHROW)) {
int nonNullTill = getNextBranchTarget();
assumedNonNullTill.put(entry.getKey(), nonNullTill);
} else if (OpcodeUtils.isBranch(lastOp)) {
int branchOffset = CodeByteUtils.getshort(getCode().getCode(), getPC() - 2);
if (branchOffset > 0) {
assumedNonNullTill.put(entry.getKey(), getPC() + branchOffset);
}
}
}
}
}
/**
* remove branch targets that have been passed
*
* @param pc
* the current pc
*/
public void clearBranchTargets(int pc) {
Iterator it = branchTargets.iterator();
while (it.hasNext()) {
int target = it.next().intValue();
if (target <= pc) {
it.remove();
}
}
}
public int getNextBranchTarget() {
if (branchTargets.isEmpty()) {
return Integer.MAX_VALUE;
}
return branchTargets.get(0);
}
public void removeAssumptions(Map assumptionsTill) {
noAssumptionsPossible.addAll(assumptionsTill.keySet());
}
}