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

com.igormaznitsa.meta.checker.extracheck.MethodParameterChecker Maven / Gradle / Ivy

Go to download

Maven plugin to make check of compiled classes contain annotations from meta-annotations module and print some useful info (it doesn't check jr-305 annotations, use FindBugs plugin!).

The newest version!
/*
 * Copyright 2016 Igor Maznitsa.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.igormaznitsa.meta.checker.extracheck;

import com.igormaznitsa.meta.annotation.MayContainNull;
import com.igormaznitsa.meta.annotation.MustNotContainNull;
import com.igormaznitsa.meta.checker.Context;
import com.igormaznitsa.meta.checker.Utils;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.ParameterAnnotationEntry;
import org.apache.bcel.generic.Type;

public final class MethodParameterChecker {

  private static final Class[] CLASSES_WHERE_POSSIBLE_NULL = new Class[]{java.util.List.class, java.util.Vector.class, java.util.Queue.class};

  private static final Set CACHED_CLASS_NAMES_POSITIVE_RECOGNIZED = new HashSet<>();
  private static final Set CACHED_CLASS_NAMES_NEGATIVE_RECOGNIZED = new HashSet<>();

  private static final Set ANNOTATIONS_FOR_OBJECT = classesToNameSet(javax.annotation.Nullable.class, javax.annotation.Nonnull.class, "org.jetbrains.annotations.Nullable", "org.jetbrains.annotations.NotNull");
  private static final Set ANNOTATIONS_FOR_ARRAY_OR_COLLECTION = classesToNameSet(MayContainNull.class, MustNotContainNull.class);

  private static final String RETURN_TYPE_NOTIFICATION = "Return type must be marked by either @%s or @%s";
  private static final String RETURN_TYPE_NOTIFICATION_WRONG_TYPE = "Non-object result type can't be marked by either @%s or @%s";
  private static final String ARG_TYPE_NOTIFICATION = "Arg. #%d must be marked by either @%s or @%s";

  public static void checkReturnTypeForNullable(final Context context, final Method method) {
    final Type returnType = method.getReturnType();
    final AnnotationEntry[] annotations = method.getAnnotationEntries();
    if (isNullableType(returnType)) {
      if (!hasAnnotationFromSet(annotations, ANNOTATIONS_FOR_OBJECT)) {
        context.error(String.format(RETURN_TYPE_NOTIFICATION, Utils.extractClassName(Nullable.class.getName()), Utils.extractClassName(Nonnull.class.getName())), true);
      }
    } else {
      if (hasAnnotationFromSet(annotations, ANNOTATIONS_FOR_OBJECT)) {
        context.error(String.format(RETURN_TYPE_NOTIFICATION_WRONG_TYPE, Utils.extractClassName(Nullable.class.getName()), Utils.extractClassName(Nonnull.class.getName())), true);
      }
    }
  }

  public static void checkReturnTypeForMayContainNull(final Context context, final Method method) {
    final Type returnType = method.getReturnType();
    if (isNullableType(returnType)) {
      final AnnotationEntry[] annotations = method.getAnnotationEntries();

      if (isArrayOfObjectsOrCollection(context, returnType) && !hasAnnotationFromSet(annotations, ANNOTATIONS_FOR_ARRAY_OR_COLLECTION)) {
        context.error(String.format(RETURN_TYPE_NOTIFICATION, Utils.extractClassName(MayContainNull.class.getName()), Utils.extractClassName(MustNotContainNull.class.getName())), true);
      }
    }
  }

  private static boolean shouldSkipFirstMethodArg(final JavaClass klazz, final Method method) {
    if (klazz.isNested() && "".equals(method.getName()) && method.getArgumentTypes().length>0){
      if (klazz.isStatic()) return false;
      final String firstArgType = method.getArgumentTypes()[0].getSignature();
      final String outerKlazzSignature = 'L'+Utils.extractOuterClassName(klazz.getClassName())+';';
      return firstArgType.equals(outerKlazzSignature);
    }
    return method.getName().equals("") && klazz.isNested() && !klazz.isStatic();
  }

  public static void checkParamsTypeForNullable(final Context context, final Method method) {
    final Type[] arguments = method.getArgumentTypes();
    final ParameterAnnotationEntry[] paramAnnotations = method.getParameterAnnotationEntries();

    final boolean ignoreFirst = shouldSkipFirstMethodArg(context.getProcessingClass(), method);
    final int argLength = arguments.length;
    final int realArgLength = ignoreFirst ? argLength - 1 : argLength;

    int paramAnnoIndex = 0;
    for (int argIndex = 0; argIndex < argLength; argIndex++) {
      if (ignoreFirst && argIndex == 0) {
        continue;
      }
      if (isNullableType(arguments[argIndex])
          && (paramAnnoIndex >= paramAnnotations.length || !hasParameterAnnotationFromSet(realArgLength, paramAnnoIndex, paramAnnotations, ANNOTATIONS_FOR_OBJECT))) {
        context.error(String.format(ARG_TYPE_NOTIFICATION, argIndex + 1, Utils.extractClassName(Nullable.class.getName()), Utils.extractClassName(Nonnull.class.getName())), true);
      }
      paramAnnoIndex++;
    }
  }

  public static void checkParamsTypeForMayContainNull(final Context context, final Method method) {
    final Type[] arguments = method.getArgumentTypes();
    final ParameterAnnotationEntry[] paramAnnotations = method.getParameterAnnotationEntries();

    final boolean ignoreFirst = shouldSkipFirstMethodArg(context.getProcessingClass(), method);
    final int argLength = arguments.length;
    final int realArgLength = ignoreFirst ? argLength - 1 : argLength;

    int paramAnnoIndex = 0;
    for (int argIndex = 0; argIndex < argLength; argIndex++) {
      if (ignoreFirst && argIndex == 0) {
        continue;
      }

      if (isNullableType(arguments[argIndex]) && isArrayOfObjectsOrCollection(context, arguments[argIndex])
          && (paramAnnoIndex >= paramAnnotations.length || !hasParameterAnnotationFromSet(realArgLength, paramAnnoIndex, paramAnnotations, ANNOTATIONS_FOR_ARRAY_OR_COLLECTION))) {
        context.error(String.format(ARG_TYPE_NOTIFICATION, argIndex + 1, Utils.extractClassName(MayContainNull.class.getName()), Utils.extractClassName(MustNotContainNull.class.getName())), true);
      }
      paramAnnoIndex++;
    }
  }

  private static Set classesToNameSet(final Object... classObjects) {
    final Set result = new HashSet<>();
    for (final Object a : classObjects) {
      if (a instanceof Class) {
        result.add(Utils.makeSignatureForClass((Class) a));
      } else if (a instanceof String) {
        result.add(Utils.makeSignatureForClass((String) a));
      } else {
        throw new IllegalArgumentException("Unexpected object type [" + a + ']');
      }
    }
    return result;
  }

  private static boolean isNullableType(final Type type) {
    final String signature = type.getSignature();
    return !signature.isEmpty() && (signature.charAt(0) == '[' || signature.charAt(0) == 'L');
  }

  private static boolean isArrayOfObjectsOrCollection(final Context context, final Type type) {
    final String signature = type.getSignature();

    final int arrayLastChar = signature.lastIndexOf('[');

    if (arrayLastChar > 0) {
      return true;
    }

    if (signature.endsWith(";")) {
      if (arrayLastChar == 0) {
        return true;
      }
    } else {
      return false;
    }

    if (CACHED_CLASS_NAMES_POSITIVE_RECOGNIZED.contains(signature)) {
      return true;
    }
    if (CACHED_CLASS_NAMES_NEGATIVE_RECOGNIZED.contains(signature)) {
      return false;
    }

    return investigateClassThatCanContainNull(context, signature);
  }

  private static boolean investigateClassThatCanContainNull(final Context context, final String classTypeInInsideFormat) {
    final String klazzName = Utils.classNameToCanonical(classTypeInInsideFormat);
    final File classFile = new File(context.getTargetDirectoryFolder(), klazzName.replace('.', '/') + ".class");
    if (classFile.isFile()) {
      try {
        final JavaClass jclazz = new ClassParser(classFile.getAbsolutePath()).parse();

        for (final String interfaceName : jclazz.getInterfaceNames()) {
          if (investigateClassThatCanContainNull(context, Utils.makeSignatureForClass(interfaceName))) {
            CACHED_CLASS_NAMES_POSITIVE_RECOGNIZED.add(classTypeInInsideFormat);
            return true;
          }
        }

        final String superclassName = jclazz.getSuperclassName();
        if (superclassName != null && !superclassName.equals("java.lang.Object")) {
          final String superClassInInsideFormat = Utils.makeSignatureForClass(superclassName);
          if (investigateClassThatCanContainNull(context, superClassInInsideFormat)) {
            CACHED_CLASS_NAMES_POSITIVE_RECOGNIZED.add(classTypeInInsideFormat);
            return true;
          }
        }
      } catch (Exception ex) {
        context.warning("Can't parse class file : " + classFile.getAbsolutePath(), false);
      }
    } else {
      // it is possible that it is standard class
      final String normalClassName = klazzName.replace('$', '.');
      try {
        final Class foundClass = Class.forName(normalClassName);
        for (final Class classWhereNullPossible : CLASSES_WHERE_POSSIBLE_NULL) {
          if (classWhereNullPossible.isAssignableFrom(foundClass)) {
            CACHED_CLASS_NAMES_POSITIVE_RECOGNIZED.add(classTypeInInsideFormat);
            return true;
          }
        }
      } catch (ClassNotFoundException ignored) {
      }
    }
    CACHED_CLASS_NAMES_NEGATIVE_RECOGNIZED.add(classTypeInInsideFormat);
    return false;
  }

  private static boolean hasAnnotationFromSet(final AnnotationEntry[] annotations, final Set namesToFind) {
    for (final AnnotationEntry a : annotations) {
      if (namesToFind.contains(a.getAnnotationType())) {
        return true;
      }
    }
    return false;
  }

  private static boolean hasParameterAnnotationFromSet(final int numberOfArguments, int indexInParamAnnotations, final ParameterAnnotationEntry[] paramAnnotations, final Set namesToFind) {
    while (indexInParamAnnotations < paramAnnotations.length) {
      for (final AnnotationEntry a : paramAnnotations[indexInParamAnnotations].getAnnotationEntries()) {
        if (namesToFind.contains(a.getAnnotationType())) {
          return true;
        }
      }
      indexInParamAnnotations += numberOfArguments;
    }
    return false;
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy