Skip to content

Commit

Permalink
#361 Create test for missing nullability annotations (work in progress)
Browse files Browse the repository at this point in the history
- One problem is that javassist does not recognize `String @nullable []` as the annotation being for the whole type, leading to a lot of false positives
  • Loading branch information
ljacqu committed Aug 31, 2023
1 parent 499a7c8 commit e13fcb4
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 0 deletions.
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@
<scope>provided</scope>
</dependency>

<!-- org.reflections: For introspection in tests -->
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.10.2</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version> <!-- keep in sync with reflections -->
</dependency>

<!-- Unit testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package ch.jalu.configme;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;
import javassist.bytecode.AccessFlag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.reflections.Reflections;
import org.reflections.scanners.Scanners;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

class NullabilityAnnotationConsistencyTest {

public static void main(String... args) throws Exception {
Reflections reflections = new Reflections("ch.jalu.configme",
Scanners.SubTypes.filterResultsBy(c -> true));

Set<String> packageBlacklist = excludedPackages();

List<Class<?>> classes = reflections.getSubTypesOf(Object.class)
.stream()
.filter(clz -> !isTestClassOrWithin(clz))
.filter(clz -> packageBlacklist.stream().noneMatch(bl -> clz.getName().startsWith(bl)))
.collect(Collectors.toList());

ClassPool pool = ClassPool.getDefault();

Map<Class<?>, List<CtMethod>> declaredMethodsByClass = new HashMap<>();
Map<Class<?>, List<CtConstructor>> declaredConstructorsByClass = new HashMap<>();


for (Class<?> clazz : classes) {
CtClass result = pool.get(clazz.getName());
declaredMethodsByClass.put(clazz, Arrays.asList(result.getDeclaredMethods()));
declaredConstructorsByClass.put(clazz, Arrays.asList(result.getDeclaredConstructors()));
}

List<String> errors = new ArrayList<>();
for (Map.Entry<Class<?>, List<CtMethod>> classAndMethods : declaredMethodsByClass.entrySet()) {

for (CtMethod method : classAndMethods.getValue()) {
if ((method.getMethodInfo().getAccessFlags() & AccessFlag.SYNTHETIC) != 0) {
continue;
}


List<String> errorsForMethod = new ArrayList<>();
if (!method.hasAnnotation(NotNull.class)
&& !method.hasAnnotation(Nullable.class)
&& !method.getReturnType().isPrimitive()) {
errorsForMethod.add("missing annotation on return value");
}

Object[][] annotations = method.getParameterAnnotations();
CtClass[] parameterTypes = method.getParameterTypes();
errorsForMethod.addAll(findErrorsForParams(annotations, parameterTypes));

if (!errorsForMethod.isEmpty()) {
String methodString = method.getDeclaringClass().getName() +"#" + method.getName() + "("
+ Arrays.stream(method.getParameterTypes()).map(CtClass::getSimpleName).collect(Collectors.joining(", "))
+ ")";
errors.addAll(errorsForMethod.stream()
.map(err -> methodString + ": " + err)
.collect(Collectors.toList()));

}
}
}

for (Map.Entry<Class<?>, List<CtConstructor>> constructorsByClass : declaredConstructorsByClass.entrySet()) {
for (CtConstructor ctConstructor : constructorsByClass.getValue()) {
Object[][] annotations = ctConstructor.getParameterAnnotations();
CtClass[] parameterTypes = ctConstructor.getParameterTypes();
List<String> errorsForConstructor = findErrorsForParams(annotations, parameterTypes);
if (!errorsForConstructor.isEmpty()) {
String constructorString = "Constructor " + ctConstructor.getLongName();
errors.addAll(errorsForConstructor.stream()
.map(err -> constructorString + ": " + err)
.collect(Collectors.toList()));
}
}
}

System.out.println(errors.size() + " errors");
System.out.println(String.join("\n- ", errors));
}

private static List<String> findErrorsForParams(Object[][] annotations, CtClass[] parameterTypes) {
List<String> errors = new ArrayList<>();
for (int i = 0; i < parameterTypes.length; i++) {
CtClass parameterType = parameterTypes[i];
boolean hasNullabilityAnnotation = Arrays.stream(annotations[i])
.map(anno -> ((Annotation) anno).annotationType())
.anyMatch(annoType -> annoType == NotNull.class || annoType == Nullable.class);

if (parameterType.isPrimitive() && hasNullabilityAnnotation) {
errors.add("param " + i + " is primitive but has a nullability annotation");
} else if (!parameterType.isPrimitive() && !hasNullabilityAnnotation) {
errors.add("param " + i + " does not have a nullability annotation");
}
}
return errors;
}

private static Set<String> excludedPackages() {
Set<String> blacklist = new HashSet<>();
blacklist.add("ch.jalu.configme.beanmapper.command.");
blacklist.add("ch.jalu.configme.beanmapper.typeissues.");
blacklist.add("ch.jalu.configme.beanmapper.worldgroup.");
blacklist.add("ch.jalu.configme.demo.");
blacklist.add("ch.jalu.configme.samples.");
return blacklist;
}

private static boolean isTestClassOrWithin(Class<?> clazz) {
return clazz.getName().endsWith("Test")
|| clazz.getEnclosingClass() != null && clazz.getEnclosingClass().getName().endsWith("Test");
}
}

0 comments on commit e13fcb4

Please sign in to comment.