#1082: Add "since" to Deprecation annotations

By: DerFrZocker <derrieple@gmail.com>
This commit is contained in:
Bukkit/Spigot
2024-11-25 07:52:33 +11:00
parent 98f6ab9a04
commit 0023e5549a
257 changed files with 1523 additions and 1176 deletions

View 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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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 {};
}

View File

@@ -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 {};
}