All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.main.java.com.mebigfatguy.fbcontrib.detect.ImmatureClass Maven / Gradle / Ivy

package com.mebigfatguy.fbcontrib.detect;

import java.io.IOException;
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.Constant;
import org.apache.bcel.classfile.ConstantLong;
import org.apache.bcel.classfile.ConstantValue;
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.SerialVersionCalc;
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

	private static final int MANUAL_SERIALVERSION_ID_LOWER_BOUND = 0;
	private static final int MANUAL_SERIALVERSION_ID_UPPER_BOUND = 10000;

	private static JavaClass serializableClass;

	static {
		try {
			serializableClass = Repository.lookupClass("java.io.Serializable");
		} catch (ClassNotFoundException e) {
		}
	}

	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 {
										heStatus = (fieldSig.equals("Ljava/lang/Double;") || fieldSig.equals("Ljava/lang/Float;")) ? HEStatus.NOT_NEEDED : HEStatus.NEEDED;
									}
								} else if (!fieldSig.startsWith(Values.SIG_ARRAY_PREFIX)) {
									heStatus = fieldSig.equals("D") || fieldSig.equals("F") ? HEStatus.NOT_NEEDED : 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 ("var".equals(f.getName())) {
			bugReporter.reportBug(new BugInstance(this, BugType.IMC_IMMATURE_CLASS_VAR_NAME.name(), NORMAL_PRIORITY)
					.addClass(this).addField(this));
		}
		
		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;
			}

			try {
				if ("serialVersionUID".equals(f.getName())
						&& getClassContext().getJavaClass().instanceOf(serializableClass)) {
					ConstantValue cv = f.getConstantValue();
					if (cv != null) {
						Constant c = cv.getConstantPool().getConstant(cv.getConstantValueIndex());
						if (c instanceof ConstantLong) {
							long definedUUID = ((ConstantLong) c).getBytes();
							if (definedUUID < MANUAL_SERIALVERSION_ID_LOWER_BOUND
									|| definedUUID > MANUAL_SERIALVERSION_ID_UPPER_BOUND) {
								try {
									long computedUUID = SerialVersionCalc.uuid(getClassContext().getJavaClass());
									if (computedUUID != definedUUID) {
										bugReporter.reportBug(new BugInstance(this,
												BugType.IMC_IMMATURE_CLASS_BAD_SERIALVERSIONUID.name(), NORMAL_PRIORITY)
														.addClass(this).addField(this));
									}
								} catch (IOException e) {
								}
							}
						}
					}
				}
			} catch (ClassNotFoundException e) {
				bugReporter.reportMissingClass(e);
			}
		}
	}

	public void visitMethod(Method m) {
		LocalVariableTable lvt = m.getLocalVariableTable();
		if (lvt != null) {
			LocalVariable[] lv = lvt.getLocalVariableTable();
			if (lv != null) {
				for (LocalVariable l : lv) {
					if ("var".equals(l.getName())) {
						bugReporter.reportBug(new BugInstance(this, BugType.IMC_IMMATURE_CLASS_VAR_NAME.name(), NORMAL_PRIORITY)
								.addClass(this).addMethod(this).addSourceLine(this, l.getStartPC()));
					}
				}
			}
		}
	}
	/**
	 * 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;
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy