src.main.java.com.mebigfatguy.fbcontrib.detect.ImmatureClass 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.
package com.mebigfatguy.fbcontrib.detect;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.LocalVariable;
import org.apache.bcel.classfile.LocalVariableTable;
import org.apache.bcel.classfile.Method;
import com.mebigfatguy.fbcontrib.collect.Statistics;
import com.mebigfatguy.fbcontrib.utils.BugType;
import com.mebigfatguy.fbcontrib.utils.SignatureBuilder;
import com.mebigfatguy.fbcontrib.utils.SignatureUtils;
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.ba.ClassContext;
/**
* looks for classes that aren't fully flushed out to be easily usable for various reasons. While the class will most likely work fine, it is more difficult to
* use than necessary.
*/
public class ImmatureClass extends BytecodeScanningDetector {
private static final Pattern ARG_PATTERN = Pattern.compile("(arg|parm|param)\\d");
private static final String PACKAGE_INFO = "package-info";
private static final int MAX_EMPTY_METHOD_SIZE = 2; // ACONST_NULL, ARETURN
enum HEStatus {
NOT_NEEDED, UNKNOWN, NEEDED
};
enum FieldStatus {
NONE, SAW_INSTANCE, REPORTED
}
private BugReporter bugReporter;
private FieldStatus fieldStatus = FieldStatus.NONE;
private boolean classIsJPAEntity;
public ImmatureClass(BugReporter reporter) {
bugReporter = reporter;
}
/**
* overrides the visitor to report on classes without toStrings that have fields
*
* @param classContext
* the context object of the currently parsed class
*/
@Override
public void visitClassContext(ClassContext classContext) {
JavaClass cls = classContext.getJavaClass();
fieldStatus = FieldStatus.NONE;
String packageName = cls.getPackageName();
if (packageName.isEmpty()) {
bugReporter.reportBug(new BugInstance(this, BugType.IMC_IMMATURE_CLASS_NO_PACKAGE.name(), LOW_PRIORITY).addClass(cls));
}
if (!packageName.equals(packageName.toLowerCase(Locale.ENGLISH))) {
bugReporter.reportBug(new BugInstance(this, BugType.IMC_IMMATURE_CLASS_UPPER_PACKAGE.name(), LOW_PRIORITY).addClass(cls));
}
String simpleClassName = cls.getClassName();
int dotPos = simpleClassName.lastIndexOf('.');
if (dotPos >= 0) {
simpleClassName = simpleClassName.substring(dotPos + 1);
}
if (!Character.isUpperCase(simpleClassName.charAt(0)) && (simpleClassName.indexOf(Values.INNER_CLASS_SEPARATOR) < 0)
&& !PACKAGE_INFO.equals(simpleClassName)) {
bugReporter.reportBug(new BugInstance(this, BugType.IMC_IMMATURE_CLASS_LOWER_CLASS.name(), LOW_PRIORITY).addClass(cls));
}
if ((!cls.isAbstract()) && (!cls.isEnum()) && (cls.getClassName().indexOf(Values.INNER_CLASS_SEPARATOR) < 0) && !isTestClass(cls)) {
try {
boolean clsHasRuntimeAnnotation = classHasRuntimeVisibleAnnotation(cls);
if (clsHasRuntimeAnnotation) {
classIsJPAEntity = classIsJPAEntity(cls);
} else {
classIsJPAEntity = false;
}
HEStatus heStatus = HEStatus.UNKNOWN;
checkIDEGeneratedParmNames(cls);
for (Field f : cls.getFields()) {
if (!f.isStatic() && !f.isSynthetic()) {
boolean fieldHasRuntimeAnnotation = fieldHasRuntimeVisibleAnnotation(f);
if (!fieldHasRuntimeAnnotation) {
/* only report one of these, so as not to flood the report */
if (!classIsJPAEntity && !hasMethodInHierarchy(cls, Values.TOSTRING, SignatureBuilder.SIG_VOID_TO_STRING)) {
bugReporter.reportBug(new BugInstance(this, BugType.IMC_IMMATURE_CLASS_NO_TOSTRING.name(), LOW_PRIORITY).addClass(cls));
heStatus = HEStatus.NOT_NEEDED;
break;
}
if (heStatus != HEStatus.NOT_NEEDED) {
String fieldSig = f.getSignature();
if (fieldSig.startsWith(Values.SIG_QUALIFIED_CLASS_PREFIX)) {
if (!fieldSig.startsWith("Ljava")) {
JavaClass fieldClass = Repository.lookupClass(SignatureUtils.trimSignature(fieldSig));
if (!hasMethodInHierarchy(fieldClass, "equals", SignatureBuilder.SIG_OBJECT_TO_BOOLEAN)) {
heStatus = HEStatus.NOT_NEEDED;
}
} else if (!fieldSig.startsWith("Ljava/lang/") && !fieldSig.startsWith("Ljava/util/")) {
heStatus = HEStatus.NOT_NEEDED;
}
} else if (!fieldSig.startsWith(Values.SIG_ARRAY_PREFIX)) {
heStatus = HEStatus.NEEDED;
}
}
} else {
heStatus = HEStatus.NOT_NEEDED;
}
}
}
if (!clsHasRuntimeAnnotation && (heStatus == HEStatus.NEEDED)) {
if (!hasMethodInHierarchy(cls, "equals", SignatureBuilder.SIG_OBJECT_TO_BOOLEAN)) {
bugReporter.reportBug(new BugInstance(this, BugType.IMC_IMMATURE_CLASS_NO_EQUALS.name(), LOW_PRIORITY).addClass(cls));
} else if (!hasMethodInHierarchy(cls, Values.HASHCODE, SignatureBuilder.SIG_VOID_TO_INT)) {
bugReporter.reportBug(new BugInstance(this, BugType.IMC_IMMATURE_CLASS_NO_HASHCODE.name(), LOW_PRIORITY).addClass(cls));
}
}
} catch (ClassNotFoundException cnfe) {
bugReporter.reportMissingClass(cnfe);
}
}
super.visitClassContext(classContext);
}
@Override
public void visitField(Field f) {
if (!f.isSynthetic() && (f.getName().indexOf(Values.SYNTHETIC_MEMBER_CHAR) < 0)) {
switch (fieldStatus) {
case NONE:
if (!f.isStatic()) {
fieldStatus = FieldStatus.SAW_INSTANCE;
}
break;
case SAW_INSTANCE:
if (f.isStatic()) {
bugReporter.reportBug(
new BugInstance(this, BugType.IMC_IMMATURE_CLASS_WRONG_FIELD_ORDER.name(), LOW_PRIORITY).addClass(this).addField(this));
fieldStatus = FieldStatus.REPORTED;
}
break;
case REPORTED:
break;
}
}
}
/**
* implements the visitor to check for calls to Throwable.printStackTrace()
*
* @param seen
* the currently parsed opcode
*/
@Override
public void sawOpcode(int seen) {
if ((seen == INVOKEVIRTUAL) && "printStackTrace".equals(getNameConstantOperand())
&& SignatureBuilder.SIG_VOID_TO_VOID.equals(getSigConstantOperand())) {
bugReporter.reportBug(new BugInstance(this, BugType.IMC_IMMATURE_CLASS_PRINTSTACKTRACE.name(), NORMAL_PRIORITY).addClass(this).addMethod(this)
.addSourceLine(this));
}
}
/**
* looks to see if this class (or some class in its hierarchy (besides Object) has implemented the specified method.
*
* @param cls
* the class to look in
* @param methodName
* the method name to look for
* @param methodSig
* the method signature to look for
*
* @return when toString is found
*
* @throws ClassNotFoundException
* if a super class can't be found
*/
private static boolean hasMethodInHierarchy(JavaClass cls, String methodName, String methodSig) throws ClassNotFoundException {
String clsName = cls.getClassName();
if (Values.DOTTED_JAVA_LANG_OBJECT.equals(clsName)) {
return false;
}
if (Statistics.getStatistics().getMethodStatistics(clsName.replace('.', '/'), methodName, methodSig).getNumBytes() == 0) {
return hasMethodInHierarchy(cls.getSuperClass(), methodName, methodSig);
}
return true;
}
/**
* determines if class has a runtime annotation. If it does it is likely to be a singleton, or handled specially where hashCode/equals isn't of importance.
*
* @param cls
* the class to check
*
* @return if runtime annotations are found
*/
private static boolean classHasRuntimeVisibleAnnotation(JavaClass cls) {
AnnotationEntry[] annotations = cls.getAnnotationEntries();
if (annotations != null) {
for (AnnotationEntry annotation : annotations) {
if (annotation.isRuntimeVisible()) {
return true;
}
}
}
return false;
}
/**
* returns whether this class is a JPA Entity, as such it shouldn't really have a toString()
*
* @param cls
* the class to check
* @return if the class is a jpa entity
*/
private static boolean classIsJPAEntity(JavaClass cls) {
AnnotationEntry[] annotations = cls.getAnnotationEntries();
if (annotations != null) {
for (AnnotationEntry annotation : annotations) {
if ("Ljavax/persistence/Entity;".equals(annotation.getAnnotationType())) {
return true;
}
}
}
return false;
}
/**
* looks to see the field has a runtime visible annotation, if it does it might be autowired or some other mechanism attached that makes them less
* interesting for a toString call.
*
* @param f
* the field to check
* @return if the field has a runtime visible annotation
*/
private static boolean fieldHasRuntimeVisibleAnnotation(Field f) {
AnnotationEntry[] annotations = f.getAnnotationEntries();
if (annotations != null) {
for (AnnotationEntry annotation : annotations) {
if (annotation.isRuntimeVisible()) {
return true;
}
}
}
return false;
}
/**
* checks to see if it this class has unit test related annotations attached to methods
*
* @param cls
* the class to check
* @return if a unit test annotation was found
*/
private static boolean isTestClass(JavaClass cls) {
for (Method m : cls.getMethods()) {
for (AnnotationEntry entry : m.getAnnotationEntries()) {
String type = entry.getAnnotationType();
if (type.startsWith("Lorg/junit/") || type.startsWith("Lorg/testng/")) {
return true;
}
}
}
return false;
}
/**
* looks for methods that have it's parameters all follow the form arg0, arg1, arg2, or parm0, parm1, parm2 etc, where the method actually has code in it
*
* @param cls
* the class to check
*/
private void checkIDEGeneratedParmNames(JavaClass cls) {
for (Method m : cls.getMethods()) {
if (isIDEGeneratedMethodWithCode(m)) {
bugReporter.reportBug(new BugInstance(this, BugType.IMC_IMMATURE_CLASS_IDE_GENERATED_PARAMETER_NAMES.name(), NORMAL_PRIORITY).addClass(cls)
.addMethod(cls, m));
return;
}
}
}
private boolean isIDEGeneratedMethodWithCode(Method m) {
if (!m.isPublic()) {
return false;
}
String name = m.getName();
if (Values.CONSTRUCTOR.equals(name) || Values.STATIC_INITIALIZER.equals(name)) {
return false;
}
LocalVariableTable lvt = m.getLocalVariableTable();
if (lvt == null) {
return false;
}
if (m.getCode().getCode().length <= MAX_EMPTY_METHOD_SIZE) {
return false;
}
int numArgs = m.getArgumentTypes().length;
if (numArgs == 0) {
return false;
}
int offset = m.isStatic() ? 0 : 1;
for (int i = 0; i < numArgs; i++) {
LocalVariable lv = lvt.getLocalVariable(offset + i, 0);
if ((lv == null) || (lv.getName() == null)) {
return false;
}
Matcher ma = ARG_PATTERN.matcher(lv.getName());
if (!ma.matches()) {
return false;
}
}
return true;
}
}