Files
DeltaZern/paper-api/src/test/java/org/bukkit/AnnotationTest.java
T
Jason Penilla 7132df4810 AsyncTabCompleteEvent
Let plugins be able to control tab completion of commands and chat async.

This will be useful for frameworks like ACF so we can define async safe completion handlers,
and avoid going to main for tab completions.

Especially useful if you need to query a database in order to obtain the results for tab
completion, such as offline players.

Also Enforces mutability of the existing TabCompleteEvent.

Co-authored-by: Aikar <aikar@aikar.co>
2017-11-26 13:17:09 -05:00

369 lines
15 KiB
Java

package org.bukkit;
import static org.junit.jupiter.api.Assertions.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.ParameterNode;
public class AnnotationTest {
private static final String[] ACCEPTED_ANNOTATIONS = {
"Lorg/jetbrains/annotations/Nullable;",
"Lorg/jetbrains/annotations/NotNull;",
"Lorg/jetbrains/annotations/Contract;",
"Lorg/bukkit/UndefinedNullability;",
// Paper start
"Lorg/checkerframework/checker/nullness/qual/MonotonicNonNull;",
"Lorg/checkerframework/checker/nullness/qual/NonNull;",
"Lorg/checkerframework/checker/nullness/qual/Nullable;",
"Lorg/checkerframework/checker/nullness/qual/PolyNull;",
// Paper end
};
private static final String[] EXCLUDED_CLASSES = {
// Internal technical classes
"org/bukkit/plugin/java/JavaPluginLoader",
"org/bukkit/util/io/BukkitObjectInputStream",
"org/bukkit/util/io/BukkitObjectOutputStream",
"org/bukkit/util/io/Wrapper",
"org/bukkit/plugin/java/PluginClassLoader",
// Generic functional interface
"org/bukkit/util/Consumer",
// Paper start
"io/papermc/paper/util/TransformingRandomAccessList",
"io/papermc/paper/util/TransformingRandomAccessList$TransformedListIterator",
// Timings history is broken in terms of nullability due to guavas Function defining that the param is NonNull
"co/aikar/timings/TimingHistory$2",
"co/aikar/timings/TimingHistory$2$1",
"co/aikar/timings/TimingHistory$2$1$1",
"co/aikar/timings/TimingHistory$2$1$2",
"co/aikar/timings/TimingHistory$3",
"co/aikar/timings/TimingHistory$4",
"co/aikar/timings/TimingHistoryEntry$1"
// Paper end
};
@Test
public void testAll() throws IOException, URISyntaxException {
URL loc = Bukkit.class.getProtectionDomain().getCodeSource().getLocation();
File file = new File(loc.toURI());
// Running from jar is not supported yet
assertTrue(file.isDirectory(), "code must be in a directory");
final HashMap<String, ClassNode> foundClasses = new HashMap<>();
collectClasses(file, foundClasses);
final ArrayList<String> errors = new ArrayList<>();
for (ClassNode clazz : foundClasses.values()) {
if (!isClassIncluded(clazz, foundClasses)) {
continue;
}
// Paper start - skip class if it's @NullMarked
if (isClassNullMarked(clazz, foundClasses)) {
continue;
}
// Paper end - skip class if it's @NullMarked
for (MethodNode method : clazz.methods) {
if (!isMethodIncluded(clazz, method, foundClasses)) {
continue;
}
if (mustBeAnnotated(Type.getReturnType(method.desc)) && !isWellAnnotated(method.invisibleAnnotations)) {
// Paper start - Allow use of TYPE_USE annotations
boolean warn = true;
if (isWellAnnotated(method.visibleTypeAnnotations)) {
warn = false;
} else if (method.invisibleTypeAnnotations != null) {
dance: for (final org.objectweb.asm.tree.TypeAnnotationNode invisibleTypeAnnotation : method.invisibleTypeAnnotations) {
final org.objectweb.asm.TypeReference ref = new org.objectweb.asm.TypeReference(invisibleTypeAnnotation.typeRef);
if (ref.getSort() == org.objectweb.asm.TypeReference.METHOD_RETURN && java.util.Arrays.asList(ACCEPTED_ANNOTATIONS).contains(invisibleTypeAnnotation.desc)) {
warn = false;
break dance; // cha cha real smooth
}
}
}
if (warn)
// Paper end
warn(errors, clazz, method, "return value");
}
Type[] paramTypes = Type.getArgumentTypes(method.desc);
List<ParameterNode> parameters = method.parameters;
dancing: // Paper
for (int i = 0; i < paramTypes.length; i++) {
if (mustBeAnnotated(paramTypes[i]) ^ isWellAnnotated(method.invisibleParameterAnnotations == null ? null : method.invisibleParameterAnnotations[i])) {
// Paper start
if (method.invisibleTypeAnnotations != null) {
for (final org.objectweb.asm.tree.TypeAnnotationNode invisibleTypeAnnotation : method.invisibleTypeAnnotations) {
final org.objectweb.asm.TypeReference ref = new org.objectweb.asm.TypeReference(invisibleTypeAnnotation.typeRef);
if (ref.getSort() == org.objectweb.asm.TypeReference.METHOD_FORMAL_PARAMETER && ref.getTypeParameterIndex() == i && java.util.Arrays.asList(ACCEPTED_ANNOTATIONS).contains(invisibleTypeAnnotation.desc)) {
continue dancing;
}
}
}
if (method.visibleTypeAnnotations != null) {
for (final org.objectweb.asm.tree.TypeAnnotationNode invisibleTypeAnnotation : method.visibleTypeAnnotations) {
final org.objectweb.asm.TypeReference ref = new org.objectweb.asm.TypeReference(invisibleTypeAnnotation.typeRef);
if (ref.getSort() == org.objectweb.asm.TypeReference.METHOD_FORMAL_PARAMETER && ref.getTypeParameterIndex() == i && java.util.Arrays.asList(ACCEPTED_ANNOTATIONS).contains(invisibleTypeAnnotation.desc)) {
continue dancing;
}
}
}
// Paper end - Allow use of TYPE_USE annotations
ParameterNode paramNode = parameters == null ? null : parameters.get(i);
String paramName = paramNode == null ? null : paramNode.name;
warn(errors, clazz, method, "parameter " + i + (paramName == null ? "" : ": " + paramName));
}
}
}
}
if (errors.isEmpty()) {
// Success
return;
}
Collections.sort(errors);
StringBuilder builder = new StringBuilder()
.append("There ")
.append(errors.size() != 1 ? "are " : "is ")
.append(errors.size())
.append(" missing annotation")
.append(errors.size() != 1 ? "s:\n" : ":\n");
for (String message : errors) {
builder.append("\t").append(message).append("\n");
}
fail(builder.toString());
}
private static void collectClasses(@NotNull File from, @NotNull Map<String, ClassNode> to) throws IOException {
if (from.isDirectory()) {
// Paper start - skip packages with @NullMarked
final File packageInfo = new File(from, "package-info.class");
if (packageInfo.exists()) {
try (final FileInputStream in = new FileInputStream(packageInfo)) {
final ClassReader cr = new ClassReader(in);
final ClassNode node = new ClassNode();
cr.accept(node, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
if (isClassNullMarked0(node)) {
return; // skip packages with @NullMarked
}
}
}
// Paper end - skip packages with @NullMarked
final File[] files = from.listFiles();
assert files != null;
for (File file : files) {
collectClasses(file, to);
}
return;
}
if (!from.getName().endsWith(".class")) {
return;
}
try (FileInputStream in = new FileInputStream(from)) {
final ClassReader cr = new ClassReader(in);
final ClassNode node = new ClassNode();
cr.accept(node, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
to.put(node.name, node);
}
}
// Paper start - skip class if it's @NullMarked
private static boolean isClassNullMarked(@NotNull ClassNode clazz, @NotNull Map<String, ClassNode> allClasses) {
if (clazz.nestHostClass != null) {
final ClassNode nestHostNode = allClasses.get(clazz.nestHostClass);
if (nestHostNode != null) {
return isClassNullMarked0(nestHostNode);
}
}
return isClassNullMarked0(clazz);
}
private static boolean isClassNullMarked0(@NotNull ClassNode clazz) {
return clazz.visibleAnnotations != null && clazz.visibleAnnotations.stream().anyMatch(node -> "Lorg/jspecify/annotations/NullMarked;".equals(node.desc));
}
// Paper end - skip class if it's @NullMarked
private static boolean isClassIncluded(@NotNull ClassNode clazz, @NotNull Map<String, ClassNode> allClasses) {
// Exclude private, synthetic or deprecated classes and annotations, since their members can't be null
if ((clazz.access & (Opcodes.ACC_PRIVATE | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_DEPRECATED | Opcodes.ACC_ANNOTATION)) != 0) {
return false;
}
if (isSubclassOf(clazz, "org/bukkit/material/MaterialData", allClasses)) {
throw new AssertionError("Subclass of MaterialData must be deprecated: " + clazz.name);
}
if (isSubclassOf(clazz, "java/lang/Exception", allClasses)
|| isSubclassOf(clazz, "java/lang/RuntimeException", allClasses)) {
// Exceptions are excluded
return false;
}
// Paper start
if (isInternal(clazz.invisibleAnnotations)) {
return false;
}
// Paper end
for (String excludedClass : EXCLUDED_CLASSES) {
if (excludedClass.equals(clazz.name)) {
return false;
}
}
return true;
}
private static boolean isMethodIncluded(@NotNull ClassNode clazz, @NotNull MethodNode method, @NotNull Map<String, ClassNode> allClasses) {
// Exclude private, synthetic and deprecated methods
if ((method.access & (Opcodes.ACC_PRIVATE | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_DEPRECATED)) != 0 || (method.access & (Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED | Opcodes.ACC_PUBLIC)) == 0) { // Paper - ignore package-private
return false;
}
// Exclude Java methods
if (is(method, "toString", 0) || is(method, "clone", 0) || is(method, "equals", 1)) {
return false;
}
// Exclude generated Enum methods
if (isSubclassOf(clazz, "java/lang/Enum", allClasses) && (is(method, "values", 0) || is(method, "valueOf", 1))) {
return false;
}
// Anonymous classes have generated constructors, which can't be annotated nor invoked
if ("<init>".equals(method.name) && isAnonymous(clazz)) {
return false;
}
// Paper start
if (isInternal(method.invisibleAnnotations)) {
return false;
}
// Paper end
return true;
}
// Paper start
private static boolean isInternal(List<? extends AnnotationNode> annotations) {
if (annotations == null) {
return false;
}
for (AnnotationNode node : annotations) {
if (node.desc.equals("Lorg/jetbrains/annotations/ApiStatus$Internal;")) {
return true;
}
}
return false;
}
// Paper end
private static boolean isWellAnnotated(@Nullable List<? extends AnnotationNode> annotations) { // Paper
if (annotations == null) {
return false;
}
for (AnnotationNode node : annotations) {
for (String acceptedAnnotation : ACCEPTED_ANNOTATIONS) {
if (acceptedAnnotation.equals(node.desc)) {
return true;
}
}
}
return false;
}
private static boolean mustBeAnnotated(@NotNull Type type) {
return type.getSort() == Type.ARRAY || type.getSort() == Type.OBJECT;
}
private static boolean is(@NotNull MethodNode method, @NotNull String name, int parameters) {
final List<ParameterNode> params = method.parameters;
return method.name.equals(name) && (params == null || params.size() == parameters);
}
/**
* Checks if the class is anonymous.
*
* @param clazz the class to check
* @return true if given class is anonymous
*/
private static boolean isAnonymous(@NotNull ClassNode clazz) {
final String name = clazz.name;
if (name == null) {
return false;
}
final int nestedSeparator = name.lastIndexOf('$');
if (nestedSeparator == -1 || nestedSeparator + 1 == name.length()) {
return false;
}
// Nested classes have purely numeric names. Java classes can't begin with a number,
// so if first character is a number, the class must be anonymous
final char c = name.charAt(nestedSeparator + 1);
return c >= '0' && c <= '9';
}
private static boolean isSubclassOf(@NotNull ClassNode what, @NotNull String ofWhat, @NotNull Map<String, ClassNode> allClasses) {
if (ofWhat.equals(what.name)
// Not only optimization: Super class may not be present in allClasses, so it is checked here
|| ofWhat.equals(what.superName)) {
return true;
}
final ClassNode parent = allClasses.get(what.superName);
if (parent != null && isSubclassOf(parent, ofWhat, allClasses)) {
return true;
}
for (String superInterface : what.interfaces) {
final ClassNode interfaceParent = allClasses.get(superInterface);
if (interfaceParent != null && isSubclassOf(interfaceParent, ofWhat, allClasses)) {
return true;
}
}
return false;
}
private static void warn(@NotNull Collection<String> out, @NotNull ClassNode clazz, @NotNull MethodNode method, @NotNull String description) {
out.add(clazz.name + " \t" + method.name + " \t" + description);
}
}