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

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

/*
 * fb-contrib - Auxiliary detectors for Java programs
 * Copyright (C) 2005-2019 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.Const;
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.
 * 
 * - missing @Nullable 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<>();

	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
	);

	private BugReporter bugReporter;
	private Map assumedNullTill;
	private Map assumedNonNullTill;
	private Set noAssumptionsPossible;
	private List branchTargets;
	private OpcodeStack stack;
	private boolean methodIsNullable;

	static {
		NULLABLE_ANNOTATIONS.add("Lorg/jetbrains/annotations/Nullable;");
		NULLABLE_ANNOTATIONS.add("Ljavax/annotation/Nullable;");
		NULLABLE_ANNOTATIONS.add("Ljakarta/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");
		NULLABLE_ANNOTATIONS.add("Lorg/jspecify/annotations/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('.', '/') + ";");
			}
		}
	}

	public static class AIUserValue {

		int reg;

		public AIUserValue(int reg) {
			this.reg = reg;
		}

		@Override
		public String toString() {
			return ToString.build(this);
		}
	};

	/**
	 * 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() >= Const.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() && !methodInfo.isDerived()) {
			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 if (!methodInfo.isDerived()) {
					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 Const.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 Const.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 Const.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 Const.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 Const.INVOKESTATIC:
				if (stack.getStackDepth() > 0) {
					String signature = getSigConstantOperand();
					if (IS_EMPTY_SIGNATURES.contains(signature)) {
						String methodName = getNameConstantOperand();
						if ("isEmpty".equals(methodName)) {
							OpcodeStack.Item item = stack.getStackItem(0);
							int reg = item.getRegisterNumber();
							if (reg >= 0) {
								userValue = new AIUserValue(reg);
								break;
							}
						}
					}
				}

				// $FALL-THROUGH$
			case Const.INVOKEINTERFACE:
			case Const.INVOKEVIRTUAL: {
				boolean resultIsNullable = (isMethodNullable(getClassConstantOperand(), getNameConstantOperand(),
						getSigConstantOperand()));
				if (resultIsNullable) {
					userValue = new AIUserValue(-1);
				}
				break;
			}

			case Const.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 == Const.ARETURN) || (lastOp == Const.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());
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy