#1082: Add "since" to Deprecation annotations
By: DerFrZocker <derrieple@gmail.com>
This commit is contained in:
147
paper-api/src/test/java/org/bukkit/DeprecatedTest.java
Normal file
147
paper-api/src/test/java/org/bukkit/DeprecatedTest.java
Normal file
@@ -0,0 +1,147 @@
|
||||
package org.bukkit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import com.google.common.base.Joiner;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.bukkit.support.test.ClassNodeTest;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.AnnotationNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.FieldNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
import org.objectweb.asm.tree.RecordComponentNode;
|
||||
|
||||
public class DeprecatedTest {
|
||||
|
||||
private static final String DEPRECATED_DESC = Type.getDescriptor(Deprecated.class);
|
||||
|
||||
@ClassNodeTest
|
||||
public void testIfSinceIsPresent(ClassNode classNode) {
|
||||
List<String> missingReason = new ArrayList<>();
|
||||
|
||||
// Check class annotation
|
||||
checkAnnotation(missingReason, classNode.invisibleAnnotations, "Class");
|
||||
checkAnnotation(missingReason, classNode.visibleAnnotations, "Class");
|
||||
checkAnnotation(missingReason, classNode.invisibleTypeAnnotations, "Class");
|
||||
checkAnnotation(missingReason, classNode.visibleTypeAnnotations, "Class");
|
||||
|
||||
if (classNode.recordComponents != null) {
|
||||
for (RecordComponentNode recordComponentNode : classNode.recordComponents) {
|
||||
checkAnnotation(missingReason, recordComponentNode.invisibleAnnotations, "RecordComponent '%s'".formatted(recordComponentNode.name));
|
||||
checkAnnotation(missingReason, recordComponentNode.visibleAnnotations, "RecordComponent '%s'".formatted(recordComponentNode.name));
|
||||
checkAnnotation(missingReason, recordComponentNode.invisibleTypeAnnotations, "RecordComponent '%s'".formatted(recordComponentNode.name));
|
||||
checkAnnotation(missingReason, recordComponentNode.visibleTypeAnnotations, "RecordComponent '%s'".formatted(recordComponentNode.name));
|
||||
}
|
||||
}
|
||||
|
||||
if (classNode.fields != null) {
|
||||
for (FieldNode fieldNode : classNode.fields) {
|
||||
checkAnnotation(missingReason, fieldNode.invisibleAnnotations, "Field '%s'".formatted(fieldNode.name));
|
||||
checkAnnotation(missingReason, fieldNode.visibleAnnotations, "Field '%s'".formatted(fieldNode.name));
|
||||
checkAnnotation(missingReason, fieldNode.invisibleTypeAnnotations, "Field '%s'".formatted(fieldNode.name));
|
||||
checkAnnotation(missingReason, fieldNode.visibleTypeAnnotations, "Field '%s'".formatted(fieldNode.name));
|
||||
}
|
||||
}
|
||||
|
||||
if (classNode.methods != null) {
|
||||
for (MethodNode methodNode : classNode.methods) {
|
||||
checkAnnotation(missingReason, methodNode.invisibleAnnotations, "Method '%s'".formatted(methodNode.name));
|
||||
checkAnnotation(missingReason, methodNode.visibleAnnotations, "Method '%s'".formatted(methodNode.name));
|
||||
checkAnnotation(missingReason, methodNode.invisibleTypeAnnotations, "Method '%s'".formatted(methodNode.name));
|
||||
checkAnnotation(missingReason, methodNode.visibleTypeAnnotations, "Method '%s'".formatted(methodNode.name));
|
||||
|
||||
if (methodNode.visibleParameterAnnotations != null) {
|
||||
for (int i = 0; i < methodNode.visibleParameterAnnotations.length; i++) {
|
||||
checkAnnotation(missingReason, methodNode.visibleParameterAnnotations[i], "Method Parameter '%d' for Method '%s'".formatted(i, methodNode.name));
|
||||
}
|
||||
}
|
||||
|
||||
if (methodNode.invisibleParameterAnnotations != null) {
|
||||
for (int i = 0; i < methodNode.invisibleParameterAnnotations.length; i++) {
|
||||
checkAnnotation(missingReason, methodNode.invisibleParameterAnnotations[i], "Method Parameter '%d' for Method '%s'".formatted(i, methodNode.name));
|
||||
}
|
||||
}
|
||||
|
||||
checkAnnotation(missingReason, methodNode.visibleLocalVariableAnnotations, "Local variable in Method '%s'".formatted(methodNode.name));
|
||||
checkAnnotation(missingReason, methodNode.invisibleLocalVariableAnnotations, "Local variable in Method '%s'".formatted(methodNode.name));
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(missingReason.isEmpty(), """
|
||||
Missing or wrongly formatted (only format 'number.number[.number]' is supported) 'since' value in 'Deprecated' annotation found.
|
||||
In Class '%s'.
|
||||
|
||||
Following places where found:
|
||||
%s""".formatted(classNode.name, Joiner.on('\n').join(missingReason)));
|
||||
}
|
||||
|
||||
private void checkAnnotation(List<String> missingReason, List<? extends AnnotationNode> annotationNodes, String where) {
|
||||
if (annotationNodes == null || annotationNodes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (AnnotationNode annotationNode : annotationNodes) {
|
||||
if (!annotationNode.desc.equals(DEPRECATED_DESC)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hasSince(annotationNode)) {
|
||||
missingReason.add(where);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasSince(AnnotationNode annotationNode) {
|
||||
if (annotationNode.values == null || annotationNode.values.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < annotationNode.values.size(); i = i + 2) {
|
||||
if ("since".equals(annotationNode.values.get(i))) {
|
||||
String other = (String) annotationNode.values.get(i + 1);
|
||||
|
||||
if (other == null || other.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidVersion(other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isValidVersion(String version) {
|
||||
String[] versionParts = version.split("\\.");
|
||||
|
||||
if (versionParts.length != 2 && versionParts.length != 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isNumber(versionParts[0])) {
|
||||
return false;
|
||||
}
|
||||
if (!isNumber(versionParts[1])) {
|
||||
return false;
|
||||
}
|
||||
if (versionParts.length == 3 && !isNumber(versionParts[2])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isNumber(String number) {
|
||||
try {
|
||||
Integer.parseInt(number);
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.bukkit.support.provider;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.stream.Stream;
|
||||
import org.bukkit.support.test.ClassNodeTest;
|
||||
import org.bukkit.support.test.ClassReaderTest;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.ArgumentsProvider;
|
||||
import org.junit.jupiter.params.support.AnnotationConsumer;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
|
||||
public class ClassNodeArgumentProvider implements ArgumentsProvider, AnnotationConsumer<ClassNodeTest> {
|
||||
|
||||
private Class<?>[] excludedClasses;
|
||||
private String[] excludedPackages;
|
||||
|
||||
@Override
|
||||
public void accept(ClassNodeTest classNodeTest) {
|
||||
this.excludedClasses = classNodeTest.excludedClasses();
|
||||
this.excludedPackages = classNodeTest.excludedPackages();
|
||||
|
||||
for (int i = 0; i < excludedPackages.length; i++) {
|
||||
this.excludedPackages[i] = this.excludedPackages[i].replace('.', '/');
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception {
|
||||
ClassReaderArgumentProvider classReaderArgumentProvider = new ClassReaderArgumentProvider();
|
||||
classReaderArgumentProvider.accept(new ClassReaderArguments(excludedClasses, excludedPackages));
|
||||
|
||||
return classReaderArgumentProvider.getClassReaders().map(this::toClassNode).map(Arguments::of);
|
||||
}
|
||||
|
||||
private ClassNode toClassNode(ClassReader classReader) {
|
||||
ClassNode classNode = new ClassNode(Opcodes.ASM9);
|
||||
|
||||
classReader.accept(classNode, Opcodes.ASM9);
|
||||
|
||||
return classNode;
|
||||
}
|
||||
|
||||
private record ClassReaderArguments(Class<?>[] excludedClasses, String[] excludedPackages) implements ClassReaderTest {
|
||||
|
||||
@Override
|
||||
public Class<? extends Annotation> annotationType() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package org.bukkit.support.provider;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.stream.Stream;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.support.test.ClassReaderTest;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.ArgumentsProvider;
|
||||
import org.junit.jupiter.params.support.AnnotationConsumer;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
|
||||
public class ClassReaderArgumentProvider implements ArgumentsProvider, AnnotationConsumer<ClassReaderTest> {
|
||||
|
||||
// Needs to be a class, which is from the bukkit package and not a CraftBukkit class
|
||||
private static final URI BUKKIT_CLASSES;
|
||||
|
||||
static {
|
||||
try {
|
||||
BUKKIT_CLASSES = Bukkit.class.getProtectionDomain().getCodeSource().getLocation().toURI();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Class<?>[] excludedClasses;
|
||||
private String[] excludedPackages;
|
||||
|
||||
@Override
|
||||
public void accept(ClassReaderTest classReaderTest) {
|
||||
this.excludedClasses = classReaderTest.excludedClasses();
|
||||
this.excludedPackages = classReaderTest.excludedPackages();
|
||||
|
||||
for (int i = 0; i < excludedPackages.length; i++) {
|
||||
this.excludedPackages[i] = this.excludedPackages[i].replace('.', '/');
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception {
|
||||
return getClassReaders().map(Arguments::of);
|
||||
}
|
||||
|
||||
public Stream<ClassReader> getClassReaders() {
|
||||
return readBukkitClasses().map(this::toClassReader);
|
||||
}
|
||||
|
||||
private ClassReader toClassReader(InputStream stream) {
|
||||
try (stream) {
|
||||
return new ClassReader(stream);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<InputStream> readBukkitClasses() {
|
||||
try {
|
||||
return Files.walk(Path.of(BUKKIT_CLASSES))
|
||||
.map(Path::toFile)
|
||||
.filter(File::isFile)
|
||||
.filter(file -> file.getName().endsWith(".class"))
|
||||
.filter(file -> filterPackageNames(removeHomeDirectory(file)))
|
||||
.filter(file -> filterClass(removeHomeDirectory(file)))
|
||||
.map(file -> {
|
||||
try {
|
||||
return new FileInputStream(file);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String removeHomeDirectory(File file) {
|
||||
return file.getAbsolutePath().substring(BUKKIT_CLASSES.getPath().length());
|
||||
}
|
||||
|
||||
private boolean filterPackageNames(String name) {
|
||||
for (String packageName : excludedPackages) {
|
||||
if (name.startsWith(packageName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean filterClass(String name) {
|
||||
for (Class<?> clazz : excludedClasses) {
|
||||
if (name.equals(clazz.getName().replace('.', '/') + ".class")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.bukkit.support.test;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import org.bukkit.support.provider.ClassNodeArgumentProvider;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ArgumentsSource;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@ArgumentsSource(ClassNodeArgumentProvider.class)
|
||||
@ParameterizedTest
|
||||
public @interface ClassNodeTest {
|
||||
|
||||
Class<?>[] excludedClasses() default {};
|
||||
|
||||
String[] excludedPackages() default {};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.bukkit.support.test;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import org.bukkit.support.provider.ClassReaderArgumentProvider;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ArgumentsSource;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@ArgumentsSource(ClassReaderArgumentProvider.class)
|
||||
@ParameterizedTest
|
||||
public @interface ClassReaderTest {
|
||||
|
||||
Class<?>[] excludedClasses() default {};
|
||||
|
||||
String[] excludedPackages() default {};
|
||||
}
|
||||
Reference in New Issue
Block a user