mirror of
https://github.com/architectury/architectury-loom.git
synced 2026-03-28 04:07:01 -05:00
Add AnnotationsDataValidator (#1379)
* Add AnnotationsDataValidator * Use Constants.ASM_VERSION
This commit is contained in:
@@ -31,6 +31,7 @@ import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import org.objectweb.asm.TypePath;
|
||||
@@ -43,7 +44,8 @@ class TypeAnnotationNodeSerializer implements JsonSerializer<TypeAnnotationNode>
|
||||
AnnotationNode annotation = context.deserialize(json, AnnotationNode.class);
|
||||
JsonObject jsonObject = json.getAsJsonObject();
|
||||
int typeRef = jsonObject.getAsJsonPrimitive("type_ref").getAsInt();
|
||||
String typePath = jsonObject.getAsJsonPrimitive("type_path").getAsString();
|
||||
JsonPrimitive typePathPrimitive = jsonObject.getAsJsonPrimitive("type_path");
|
||||
String typePath = typePathPrimitive == null ? "" : typePathPrimitive.getAsString();
|
||||
TypeAnnotationNode typeAnnotation = new TypeAnnotationNode(typeRef, TypePath.fromString(typePath), annotation.desc);
|
||||
annotation.accept(typeAnnotation);
|
||||
return typeAnnotation;
|
||||
@@ -53,7 +55,11 @@ class TypeAnnotationNodeSerializer implements JsonSerializer<TypeAnnotationNode>
|
||||
public JsonElement serialize(TypeAnnotationNode src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject json = context.serialize(src, AnnotationNode.class).getAsJsonObject();
|
||||
json.addProperty("type_ref", src.typeRef);
|
||||
json.addProperty("type_path", src.typePath.toString());
|
||||
|
||||
if (src.typePath != null) {
|
||||
json.addProperty("type_path", src.typePath.toString());
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,792 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.configuration.providers.mappings.extras.annotations.validate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.TypePath;
|
||||
import org.objectweb.asm.TypeReference;
|
||||
import org.objectweb.asm.signature.SignatureReader;
|
||||
import org.objectweb.asm.signature.SignatureVisitor;
|
||||
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.ParameterNode;
|
||||
import org.objectweb.asm.tree.TypeAnnotationNode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsData;
|
||||
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.ClassAnnotationData;
|
||||
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.GenericAnnotationData;
|
||||
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.MethodAnnotationData;
|
||||
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.TypeAnnotationKey;
|
||||
import net.fabricmc.loom.util.Constants;
|
||||
|
||||
public abstract class AnnotationsDataValidator {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AnnotationsDataValidator.class);
|
||||
|
||||
@Nullable
|
||||
protected abstract ClassNode getClass(String name, boolean includeLibraries);
|
||||
|
||||
@VisibleForTesting
|
||||
protected void error(String message, Object... args) {
|
||||
LOGGER.error(message, args);
|
||||
}
|
||||
|
||||
public boolean checkData(AnnotationsData data) {
|
||||
boolean result = true;
|
||||
|
||||
for (var classEntry : data.classes().entrySet()) {
|
||||
result &= checkClassData(classEntry.getKey(), classEntry.getValue());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean checkClassData(String className, ClassAnnotationData classData) {
|
||||
ClassNode clazz = getClass(className, false);
|
||||
|
||||
if (clazz == null) {
|
||||
error("No such target class: {}", className);
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean result = true;
|
||||
|
||||
List<AnnotationNode> annotations = concatLists(clazz.visibleAnnotations, clazz.invisibleAnnotations);
|
||||
result &= checkAnnotationsToRemove(() -> className, classData.annotationsToRemove(), annotations);
|
||||
result &= checkAnnotationsToAdd(() -> className, classData.annotationsToAdd(), annotations);
|
||||
List<TypeAnnotationNode> typeAnnotations = concatLists(clazz.visibleTypeAnnotations, clazz.invisibleTypeAnnotations);
|
||||
result &= checkTypeAnnotationsToRemove(() -> className, classData.typeAnnotationsToRemove(), typeAnnotations);
|
||||
result &= checkTypeAnnotationsToAdd(() -> className, classData.typeAnnotationsToAdd(), typeAnnotations, typeAnnotation -> checkClassTypeAnnotation(typeAnnotation, clazz));
|
||||
|
||||
for (var fieldEntry : classData.fields().entrySet()) {
|
||||
FieldNode field = findField(clazz, fieldEntry.getKey());
|
||||
|
||||
if (field == null) {
|
||||
error("No such target field: {}.{}", className, fieldEntry.getKey());
|
||||
result = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
result &= checkGenericData(
|
||||
() -> className + "." + fieldEntry.getKey(),
|
||||
fieldEntry.getValue(),
|
||||
concatLists(field.visibleAnnotations, field.invisibleAnnotations),
|
||||
concatLists(field.visibleTypeAnnotations, field.invisibleTypeAnnotations),
|
||||
typeAnnotation -> checkFieldTypeAnnotation(typeAnnotation, field)
|
||||
);
|
||||
}
|
||||
|
||||
for (var methodEntry : classData.methods().entrySet()) {
|
||||
MethodNode method = findMethod(clazz, methodEntry.getKey());
|
||||
|
||||
if (method == null) {
|
||||
error("No such target method: {}.{}", className, methodEntry.getKey());
|
||||
result = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
result &= checkMethodData(() -> className + "." + methodEntry.getKey(), methodEntry.getValue(), className, method);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean checkGenericData(Supplier<String> errorKey, GenericAnnotationData data, List<AnnotationNode> annotations, List<TypeAnnotationNode> typeAnnotations, TypeAnnotationChecker typeAnnotationChecker) {
|
||||
boolean result = true;
|
||||
result &= checkAnnotationsToRemove(errorKey, data.annotationsToRemove(), annotations);
|
||||
result &= checkAnnotationsToAdd(errorKey, data.annotationsToAdd(), annotations);
|
||||
result &= checkTypeAnnotationsToRemove(errorKey, data.typeAnnotationsToRemove(), typeAnnotations);
|
||||
result &= checkTypeAnnotationsToAdd(errorKey, data.typeAnnotationsToAdd(), typeAnnotations, typeAnnotationChecker);
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean checkMethodData(Supplier<String> errorKey, MethodAnnotationData data, String className, MethodNode method) {
|
||||
boolean result = true;
|
||||
List<AnnotationNode> annotations = concatLists(method.visibleAnnotations, method.invisibleAnnotations);
|
||||
result &= checkAnnotationsToRemove(errorKey, data.annotationsToRemove(), annotations);
|
||||
result &= checkAnnotationsToAdd(errorKey, data.annotationsToAdd(), annotations);
|
||||
List<TypeAnnotationNode> typeAnnotations = concatLists(method.visibleTypeAnnotations, method.invisibleTypeAnnotations);
|
||||
result &= checkTypeAnnotationsToRemove(errorKey, data.typeAnnotationsToRemove(), typeAnnotations);
|
||||
result &= checkTypeAnnotationsToAdd(errorKey, data.typeAnnotationsToAdd(), typeAnnotations, typeAnnotation -> checkMethodTypeAnnotation(typeAnnotation, className, method));
|
||||
|
||||
int syntheticParamCount = 0;
|
||||
|
||||
if (method.parameters != null) {
|
||||
for (ParameterNode param : method.parameters) {
|
||||
if ((param.access & Opcodes.ACC_SYNTHETIC) != 0) {
|
||||
syntheticParamCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var paramEntry : data.parameters().entrySet()) {
|
||||
int paramIndex = paramEntry.getKey();
|
||||
|
||||
if (paramIndex < 0 || paramIndex >= Type.getArgumentCount(method.desc) - syntheticParamCount) {
|
||||
error("Invalid parameter index: {} for method: {}", paramIndex, errorKey.get());
|
||||
result = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
List<AnnotationNode> paramAnnotations = concatLists(
|
||||
method.visibleParameterAnnotations == null || paramIndex >= method.visibleParameterAnnotations.length ? null : method.visibleParameterAnnotations[paramIndex],
|
||||
method.invisibleParameterAnnotations == null || paramIndex >= method.invisibleParameterAnnotations.length ? null : method.invisibleParameterAnnotations[paramIndex]
|
||||
);
|
||||
result &= checkGenericData(() -> errorKey.get() + ":" + paramIndex, paramEntry.getValue(), paramAnnotations, List.of(), typeAnnotation -> true);
|
||||
|
||||
if (!paramEntry.getValue().typeAnnotationsToRemove().isEmpty() || !paramEntry.getValue().typeAnnotationsToAdd().isEmpty()) {
|
||||
error("Type annotations cannot be added directly to method parameters: {}", errorKey.get());
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean checkAnnotationsToRemove(Supplier<String> errorKey, Set<String> annotationsToRemove, List<AnnotationNode> annotations) {
|
||||
Set<String> annotationsNotRemoved = new LinkedHashSet<>(annotationsToRemove);
|
||||
|
||||
for (AnnotationNode annotation : annotations) {
|
||||
annotationsNotRemoved.remove(Type.getType(annotation.desc).getInternalName());
|
||||
}
|
||||
|
||||
if (annotationsNotRemoved.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (String annotation : annotationsNotRemoved) {
|
||||
error("Trying to remove annotation {} from {} but it's not present", annotation, errorKey.get());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean checkAnnotationsToAdd(Supplier<String> errorKey, List<AnnotationNode> annotationsToAdd, List<AnnotationNode> annotations) {
|
||||
Set<String> existingAnnotations = new HashSet<>();
|
||||
|
||||
for (AnnotationNode annotation : annotations) {
|
||||
existingAnnotations.add(annotation.desc);
|
||||
}
|
||||
|
||||
boolean result = true;
|
||||
|
||||
for (AnnotationNode annotation : annotationsToAdd) {
|
||||
if (!existingAnnotations.add(annotation.desc)) {
|
||||
error("Trying to add annotation {} to {} but it's already present", annotation.desc, errorKey.get());
|
||||
result = false;
|
||||
}
|
||||
|
||||
result &= checkAnnotation(errorKey, annotation);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean checkTypeAnnotationsToRemove(Supplier<String> errorKey, Set<TypeAnnotationKey> typeAnnotationsToRemove, List<TypeAnnotationNode> typeAnnotations) {
|
||||
Set<TypeAnnotationKey> typeAnnotationsNotRemoved = new LinkedHashSet<>(typeAnnotationsToRemove);
|
||||
|
||||
for (TypeAnnotationNode typeAnnotation : typeAnnotations) {
|
||||
typeAnnotationsNotRemoved.remove(new TypeAnnotationKey(typeAnnotation.typeRef, typePathToString(typeAnnotation.typePath), Type.getType(typeAnnotation.desc).getInternalName()));
|
||||
}
|
||||
|
||||
if (typeAnnotationsNotRemoved.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (TypeAnnotationKey typeAnnotation : typeAnnotationsNotRemoved) {
|
||||
error("Trying to remove type annotation {} (typeRef={}, typePath={}) from {} but it's not present", typeAnnotation.name(), typeAnnotation.typeRef(), typeAnnotation.typePath(), errorKey.get());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean checkTypeAnnotationsToAdd(Supplier<String> errorKey, List<TypeAnnotationNode> typeAnnotationsToAdd, List<TypeAnnotationNode> typeAnnotations, TypeAnnotationChecker checker) {
|
||||
Set<TypeAnnotationKey> existingTypeAnnotations = new HashSet<>();
|
||||
|
||||
for (TypeAnnotationNode typeAnnotation : typeAnnotations) {
|
||||
existingTypeAnnotations.add(new TypeAnnotationKey(typeAnnotation.typeRef, typePathToString(typeAnnotation.typePath), typeAnnotation.desc));
|
||||
}
|
||||
|
||||
boolean result = true;
|
||||
|
||||
for (TypeAnnotationNode typeAnnotation : typeAnnotationsToAdd) {
|
||||
if (!existingTypeAnnotations.add(new TypeAnnotationKey(typeAnnotation.typeRef, typePathToString(typeAnnotation.typePath), typeAnnotation.desc))) {
|
||||
error("Trying to add annotation {} (typeRef={}, typePath={}) to {} but it's already present", typeAnnotation.desc, typeAnnotation.typeRef, typeAnnotation.typePath, errorKey.get());
|
||||
result = false;
|
||||
}
|
||||
|
||||
result &= checker.checkTypeAnnotation(typeAnnotation);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean checkClassTypeAnnotation(TypeAnnotationNode typeAnnotation, ClassNode clazz) {
|
||||
if (!checkTypeRef(typeAnnotation.typeRef)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
TypeReference typeRef = new TypeReference(typeAnnotation.typeRef);
|
||||
TypePathCheckerVisitor typePathChecker = new TypePathCheckerVisitor(typeAnnotation.typePath);
|
||||
|
||||
switch (typeRef.getSort()) {
|
||||
case TypeReference.CLASS_TYPE_PARAMETER -> {
|
||||
return checkTypeParameterTypeAnnotation("class", typeAnnotation, clazz.signature, typeRef.getTypeParameterIndex());
|
||||
}
|
||||
case TypeReference.CLASS_TYPE_PARAMETER_BOUND -> {
|
||||
if (!checkTypeParameterBoundTypeAnnotation("class", typeAnnotation, clazz.signature, typeRef.getTypeParameterIndex(), typeRef.getTypeParameterBoundIndex(), typePathChecker)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
case TypeReference.CLASS_EXTENDS -> {
|
||||
int superTypeIndex = typeRef.getSuperTypeIndex();
|
||||
|
||||
if (superTypeIndex == -1) {
|
||||
if (clazz.signature == null) {
|
||||
typePathChecker.visitClassType(Objects.requireNonNullElse(clazz.superName, "java/lang/Object"));
|
||||
typePathChecker.visitEnd();
|
||||
} else {
|
||||
new SignatureReader(clazz.signature).accept(new SignatureVisitor(Constants.ASM_VERSION) {
|
||||
@Override
|
||||
public SignatureVisitor visitSuperclass() {
|
||||
return typePathChecker;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (superTypeIndex >= clazz.interfaces.size()) {
|
||||
error("Invalid type reference for class type annotation: {}, interface index {} out of bounds", typeAnnotation.typeRef, superTypeIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (clazz.signature == null) {
|
||||
typePathChecker.visitClassType(clazz.interfaces.get(superTypeIndex));
|
||||
typePathChecker.visitEnd();
|
||||
} else {
|
||||
new SignatureReader(clazz.signature).accept(new SignatureVisitor(Constants.ASM_VERSION) {
|
||||
int interfaceIndex = 0;
|
||||
|
||||
@Override
|
||||
public SignatureVisitor visitInterface() {
|
||||
if (interfaceIndex++ == superTypeIndex) {
|
||||
return typePathChecker;
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
error("Invalid type reference for class type annotation: {}", typeAnnotation.typeRef);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
String typePathError = typePathChecker.getError();
|
||||
|
||||
if (typePathError != null) {
|
||||
error("Invalid type path for class type annotation, typeRef: {}, error: {}", typeAnnotation.typeRef, typePathError);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean checkFieldTypeAnnotation(TypeAnnotationNode typeAnnotation, FieldNode field) {
|
||||
if (!checkTypeRef(typeAnnotation.typeRef)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (new TypeReference(typeAnnotation.typeRef).getSort() != TypeReference.FIELD) {
|
||||
error("Invalid type reference for field type annotation: {}", typeAnnotation.typeRef);
|
||||
return false;
|
||||
}
|
||||
|
||||
String signature = Objects.requireNonNullElse(field.signature, field.desc);
|
||||
TypePathCheckerVisitor typePathChecker = new TypePathCheckerVisitor(typeAnnotation.typePath);
|
||||
new SignatureReader(signature).acceptType(typePathChecker);
|
||||
String typePathError = typePathChecker.getError();
|
||||
|
||||
if (typePathError != null) {
|
||||
error("Invalid type path for field type annotation, typeRef: {}, error: {}", typeAnnotation.typeRef, typePathError);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean checkMethodTypeAnnotation(TypeAnnotationNode typeAnnotation, String className, MethodNode method) {
|
||||
if (!checkTypeRef(typeAnnotation.typeRef)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
TypeReference typeRef = new TypeReference(typeAnnotation.typeRef);
|
||||
TypePathCheckerVisitor typePathChecker = new TypePathCheckerVisitor(typeAnnotation.typePath);
|
||||
|
||||
switch (typeRef.getSort()) {
|
||||
case TypeReference.METHOD_TYPE_PARAMETER -> {
|
||||
return checkTypeParameterTypeAnnotation("method", typeAnnotation, method.signature, typeRef.getTypeParameterIndex());
|
||||
}
|
||||
case TypeReference.METHOD_TYPE_PARAMETER_BOUND -> {
|
||||
if (!checkTypeParameterBoundTypeAnnotation("method", typeAnnotation, method.signature, typeRef.getTypeParameterIndex(), typeRef.getTypeParameterBoundIndex(), typePathChecker)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
case TypeReference.METHOD_RETURN -> {
|
||||
if (method.signature == null) {
|
||||
new SignatureReader(Type.getReturnType(method.desc).getDescriptor()).acceptType(typePathChecker);
|
||||
} else {
|
||||
new SignatureReader(method.signature).accept(new SignatureVisitor(Constants.ASM_VERSION) {
|
||||
@Override
|
||||
public SignatureVisitor visitReturnType() {
|
||||
return typePathChecker;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
case TypeReference.METHOD_RECEIVER -> {
|
||||
if ((method.access & Opcodes.ACC_STATIC) != 0 || "<init>".equals(method.name)) {
|
||||
error("Invalid type reference for method type annotation: {}, method receiver used in a static context", typeAnnotation.typeRef);
|
||||
return false;
|
||||
}
|
||||
|
||||
typePathChecker.visitClassType(className);
|
||||
typePathChecker.visitEnd();
|
||||
}
|
||||
case TypeReference.METHOD_FORMAL_PARAMETER -> {
|
||||
int formalParamIndex = typeRef.getFormalParameterIndex();
|
||||
|
||||
if (method.signature == null) {
|
||||
int nonSyntheticParams = 0;
|
||||
boolean foundArgument = false;
|
||||
|
||||
for (Type argumentType : Type.getArgumentTypes(method.desc)) {
|
||||
if ((method.access & Opcodes.ACC_SYNTHETIC) == 0) {
|
||||
if (nonSyntheticParams++ == formalParamIndex) {
|
||||
foundArgument = true;
|
||||
new SignatureReader(argumentType.getDescriptor()).acceptType(typePathChecker);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundArgument) {
|
||||
error("Invalid type reference for method type annotation: {}, formal parameter index {} out of bounds", typeAnnotation.typeRef, formalParamIndex);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
var visitor = new SignatureVisitor(Constants.ASM_VERSION) {
|
||||
int paramIndex = 0;
|
||||
boolean found = false;
|
||||
|
||||
@Override
|
||||
public SignatureVisitor visitParameterType() {
|
||||
if (paramIndex++ == formalParamIndex) {
|
||||
found = true;
|
||||
return typePathChecker;
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
};
|
||||
new SignatureReader(method.signature).accept(visitor);
|
||||
|
||||
if (!visitor.found) {
|
||||
error("Invalid type reference for method type annotation: {}, formal parameter index {} out of bounds", typeAnnotation.typeRef, formalParamIndex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
case TypeReference.THROWS -> {
|
||||
int throwsIndex = typeRef.getExceptionIndex();
|
||||
|
||||
if (method.signature == null) {
|
||||
if (method.exceptions == null || throwsIndex >= method.exceptions.size()) {
|
||||
error("Invalid type reference for method type annotation: {}, exception index {} out of bounds", typeAnnotation.typeRef, throwsIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
typePathChecker.visitClassType(method.exceptions.get(throwsIndex));
|
||||
typePathChecker.visitEnd();
|
||||
} else {
|
||||
var visitor = new SignatureVisitor(Constants.ASM_VERSION) {
|
||||
int exceptionIndex = 0;
|
||||
boolean found = false;
|
||||
|
||||
@Override
|
||||
public SignatureVisitor visitExceptionType() {
|
||||
if (exceptionIndex++ == throwsIndex) {
|
||||
found = true;
|
||||
return typePathChecker;
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
};
|
||||
new SignatureReader(method.signature).accept(visitor);
|
||||
|
||||
if (!visitor.found) {
|
||||
error("Invalid type reference for method type annotation: {}, exception index {} out of bounds", typeAnnotation.typeRef, throwsIndex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
error("Invalid type reference for method type annotation: {}", typeAnnotation.typeRef);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
String typePathError = typePathChecker.getError();
|
||||
|
||||
if (typePathError != null) {
|
||||
error("Invalid type path for method type annotation, typeRef: {}, error: {}", typeAnnotation.typeRef, typePathError);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean checkTypeParameterTypeAnnotation(String memberType, TypeAnnotationNode typeAnnotation, @Nullable String signature, int typeParamIndex) {
|
||||
int formalParamCount;
|
||||
|
||||
if (signature == null) {
|
||||
formalParamCount = 0;
|
||||
} else {
|
||||
var formalParamCounter = new SignatureVisitor(Constants.ASM_VERSION) {
|
||||
int count = 0;
|
||||
|
||||
@Override
|
||||
public void visitFormalTypeParameter(String name) {
|
||||
count++;
|
||||
}
|
||||
};
|
||||
new SignatureReader(signature).accept(formalParamCounter);
|
||||
formalParamCount = formalParamCounter.count;
|
||||
}
|
||||
|
||||
boolean result = true;
|
||||
|
||||
if (typeParamIndex >= formalParamCount) {
|
||||
error("Invalid type reference for {} type annotation: {}, formal parameter index {} out of bounds", memberType, typeAnnotation.typeRef, typeParamIndex);
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (typeAnnotation.typePath != null && typeAnnotation.typePath.getLength() != 0) {
|
||||
error("Non-empty type path for annotation doesn't make sense for {}_TYPE_PARAMETER", memberType.toUpperCase(Locale.ROOT));
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean checkTypeParameterBoundTypeAnnotation(String memberType, TypeAnnotationNode typeAnnotation, @Nullable String signature, int typeParamIndex, int typeParamBoundIndex, TypePathCheckerVisitor typePathChecker) {
|
||||
var visitor = new SignatureVisitor(Constants.ASM_VERSION) {
|
||||
boolean found = false;
|
||||
int formalParamIndex = -1;
|
||||
int boundIndex = 0;
|
||||
|
||||
@Override
|
||||
public void visitFormalTypeParameter(String name) {
|
||||
formalParamIndex++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureVisitor visitClassBound() {
|
||||
if (formalParamIndex == typeParamIndex) {
|
||||
if (boundIndex++ == typeParamBoundIndex) {
|
||||
found = true;
|
||||
return typePathChecker;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureVisitor visitInterfaceBound() {
|
||||
return visitClassBound();
|
||||
}
|
||||
};
|
||||
|
||||
if (signature != null) {
|
||||
new SignatureReader(signature).accept(visitor);
|
||||
}
|
||||
|
||||
if (!visitor.found) {
|
||||
error("Invalid type reference for {} type annotation: {}, formal parameter index {} bound index {} out of bounds", memberType, typeAnnotation.typeRef, typeParamIndex, typeParamBoundIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// copied from CheckClassAdapter
|
||||
private boolean checkTypeRef(int typeRef) {
|
||||
int mask = switch (typeRef >>> 24) {
|
||||
case TypeReference.CLASS_TYPE_PARAMETER, TypeReference.METHOD_TYPE_PARAMETER,
|
||||
TypeReference.METHOD_FORMAL_PARAMETER -> 0xFFFF0000;
|
||||
case TypeReference.FIELD, TypeReference.METHOD_RETURN, TypeReference.METHOD_RECEIVER,
|
||||
TypeReference.LOCAL_VARIABLE, TypeReference.RESOURCE_VARIABLE, TypeReference.INSTANCEOF,
|
||||
TypeReference.NEW, TypeReference.CONSTRUCTOR_REFERENCE, TypeReference.METHOD_REFERENCE -> 0xFF000000;
|
||||
case TypeReference.CLASS_EXTENDS, TypeReference.CLASS_TYPE_PARAMETER_BOUND,
|
||||
TypeReference.METHOD_TYPE_PARAMETER_BOUND, TypeReference.THROWS, TypeReference.EXCEPTION_PARAMETER -> 0xFFFFFF00;
|
||||
case TypeReference.CAST, TypeReference.CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT,
|
||||
TypeReference.METHOD_INVOCATION_TYPE_ARGUMENT, TypeReference.CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT,
|
||||
TypeReference.METHOD_REFERENCE_TYPE_ARGUMENT -> 0xFF0000FF;
|
||||
default -> 0;
|
||||
};
|
||||
|
||||
if (mask == 0 || (typeRef & ~mask) != 0) {
|
||||
error("Invalid type reference {}", typeRef);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean checkAnnotation(Supplier<String> errorKey, AnnotationNode annotation) {
|
||||
if (!annotation.desc.startsWith("L") || !annotation.desc.endsWith(";")) {
|
||||
error("Invalid annotation descriptor: {}", annotation.desc);
|
||||
return false;
|
||||
}
|
||||
|
||||
String internalName = annotation.desc.substring(1, annotation.desc.length() - 1);
|
||||
ClassNode annotationClass = getClass(internalName, true);
|
||||
|
||||
if (annotationClass == null || (annotationClass.access & Opcodes.ACC_ANNOTATION) == 0) {
|
||||
error("No such annotation class: {}", internalName);
|
||||
return false;
|
||||
}
|
||||
|
||||
Set<String> missingRequiredAttributes = new LinkedHashSet<>();
|
||||
Map<String, Type> attributeTypes = new HashMap<>();
|
||||
|
||||
for (MethodNode method : annotationClass.methods) {
|
||||
if ((method.access & Opcodes.ACC_ABSTRACT) != 0) {
|
||||
attributeTypes.put(method.name, Type.getReturnType(method.desc));
|
||||
|
||||
if (method.annotationDefault == null) {
|
||||
missingRequiredAttributes.add(method.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean result = true;
|
||||
|
||||
if (annotation.values != null) {
|
||||
for (int i = 0; i < annotation.values.size(); i += 2) {
|
||||
String key = (String) annotation.values.get(i);
|
||||
Object value = annotation.values.get(i + 1);
|
||||
|
||||
Type expectedType = attributeTypes.get(key);
|
||||
|
||||
if (expectedType == null) {
|
||||
error("Unknown annotation attribute: {}.{}", internalName, key);
|
||||
result = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
result &= checkAnnotationValue(errorKey, key, value, expectedType);
|
||||
|
||||
missingRequiredAttributes.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingRequiredAttributes.isEmpty()) {
|
||||
result = false;
|
||||
error("Annotation applied to {} is missing required attributes: {}", errorKey.get(), missingRequiredAttributes);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean checkAnnotationValue(Supplier<String> errorKey, String name, Object value, Type expectedType) {
|
||||
if (expectedType.getSort() == Type.ARRAY) {
|
||||
if (!(value instanceof List<?> values)) {
|
||||
error("Annotation value is of type {}, expected array for attribute {}", getTypeName(value), name);
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean result = true;
|
||||
|
||||
for (Object element : values) {
|
||||
result &= checkAnnotationValue(errorKey, name, element, expectedType.getElementType());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
boolean result = true;
|
||||
|
||||
boolean wrongType = switch (value) {
|
||||
case Boolean ignored -> expectedType.getSort() != Type.BOOLEAN;
|
||||
case Byte ignored -> expectedType.getSort() != Type.BYTE;
|
||||
case Character ignored -> expectedType.getSort() != Type.CHAR;
|
||||
case Short ignored -> expectedType.getSort() != Type.SHORT;
|
||||
case Integer ignored -> expectedType.getSort() != Type.INT;
|
||||
case Long ignored -> expectedType.getSort() != Type.LONG;
|
||||
case Float ignored -> expectedType.getSort() != Type.FLOAT;
|
||||
case Double ignored -> expectedType.getSort() != Type.DOUBLE;
|
||||
case String ignored -> !expectedType.getDescriptor().equals("Ljava/lang/String;");
|
||||
case Type ignored -> !expectedType.getDescriptor().equals("Ljava/lang/Class;");
|
||||
case String[] enumValue -> {
|
||||
if (!enumValue[0].startsWith("L") || !enumValue[0].endsWith(";")) {
|
||||
error("Invalid enum descriptor: {}", enumValue[0]);
|
||||
result = false;
|
||||
yield false;
|
||||
}
|
||||
|
||||
boolean wrongEnumType = !expectedType.getDescriptor().equals(enumValue[0]);
|
||||
|
||||
ClassNode enumClass = getClass(enumValue[0].substring(1, enumValue[0].length() - 1), true);
|
||||
|
||||
if (enumClass == null) {
|
||||
error("No such enum class: {}", enumValue[0]);
|
||||
result = false;
|
||||
yield wrongEnumType;
|
||||
}
|
||||
|
||||
if (!enumValueExists(enumClass, enumValue[1])) {
|
||||
error("Enum value {} does not exist in class {}", enumValue[1], enumValue[0]);
|
||||
result = false;
|
||||
yield wrongEnumType;
|
||||
}
|
||||
|
||||
yield wrongEnumType;
|
||||
}
|
||||
case AnnotationNode annotation -> {
|
||||
result &= checkAnnotation(errorKey, annotation);
|
||||
yield !expectedType.getDescriptor().equals(annotation.desc);
|
||||
}
|
||||
case List<?> ignored -> true;
|
||||
default -> throw new AssertionError("Unexpected annotation value type: " + value.getClass().getName());
|
||||
};
|
||||
|
||||
if (wrongType) {
|
||||
error("Annotation value is of type {}, expected {} for attribute {}", getTypeName(value), expectedType.getClassName(), name);
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static FieldNode findField(ClassNode clazz, String nameAndDesc) {
|
||||
for (FieldNode field : clazz.fields) {
|
||||
if (nameAndDesc.equals(field.name + ":" + field.desc)) {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static MethodNode findMethod(ClassNode clazz, String nameAndDesc) {
|
||||
for (MethodNode method : clazz.methods) {
|
||||
if (nameAndDesc.equals(method.name + method.desc)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean enumValueExists(ClassNode enumClass, String name) {
|
||||
for (FieldNode field : enumClass.fields) {
|
||||
if (field.name.equals(name) && (field.access & Opcodes.ACC_ENUM) != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String getTypeName(Object value) {
|
||||
return switch (value) {
|
||||
case Boolean ignored -> "boolean";
|
||||
case Byte ignored -> "byte";
|
||||
case Character ignored -> "char";
|
||||
case Short ignored -> "short";
|
||||
case Integer ignored -> "int";
|
||||
case Long ignored -> "long";
|
||||
case Float ignored -> "float";
|
||||
case Double ignored -> "double";
|
||||
case String ignored -> "java.lang.String";
|
||||
case Type ignored -> "java.lang.Class";
|
||||
case String[] enumValue -> getSafeClassNameFromDesc(enumValue[0]);
|
||||
case AnnotationNode annotation -> getSafeClassNameFromDesc(annotation.desc);
|
||||
case List<?> ignored -> "array";
|
||||
default -> throw new AssertionError("Unexpected annotation value type: " + value.getClass().getName());
|
||||
};
|
||||
}
|
||||
|
||||
private static String getSafeClassNameFromDesc(String desc) {
|
||||
return desc.startsWith("L") && desc.endsWith(";") ? desc.substring(1, desc.length() - 1).replace('/', '.') : desc;
|
||||
}
|
||||
|
||||
private static String typePathToString(@Nullable TypePath typePath) {
|
||||
return typePath == null ? "" : typePath.toString();
|
||||
}
|
||||
|
||||
private static <T> List<T> concatLists(@Nullable List<T> list1, @Nullable List<T> list2) {
|
||||
List<T> result = new ArrayList<>();
|
||||
|
||||
if (list1 != null) {
|
||||
result.addAll(list1);
|
||||
}
|
||||
|
||||
if (list2 != null) {
|
||||
result.addAll(list2);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface TypeAnnotationChecker {
|
||||
boolean checkTypeAnnotation(TypeAnnotationNode typeAnnotation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.configuration.providers.mappings.extras.annotations.validate;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.objectweb.asm.TypePath;
|
||||
import org.objectweb.asm.signature.SignatureVisitor;
|
||||
|
||||
import net.fabricmc.loom.util.Constants;
|
||||
|
||||
public final class TypePathCheckerVisitor extends SignatureVisitor {
|
||||
private final TypePath path;
|
||||
private final int pathLen;
|
||||
private int pathIndex = 0;
|
||||
private boolean reached = false;
|
||||
private String error = null;
|
||||
|
||||
private final Deque<Integer> argIndexStack = new ArrayDeque<>();
|
||||
private final SignatureVisitor sink = new SignatureVisitor(Constants.ASM_VERSION) {
|
||||
};
|
||||
|
||||
public TypePathCheckerVisitor(final TypePath path) {
|
||||
super(Constants.ASM_VERSION);
|
||||
this.path = path;
|
||||
this.pathLen = path == null ? 0 : path.getLength();
|
||||
|
||||
if (this.pathLen == 0) {
|
||||
reached = true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasMoreSteps() {
|
||||
return pathIndex < pathLen;
|
||||
}
|
||||
|
||||
private int nextStepKind() {
|
||||
return path.getStep(pathIndex);
|
||||
}
|
||||
|
||||
private int nextStepArgumentIndex() {
|
||||
return path.getStepArgument(pathIndex);
|
||||
}
|
||||
|
||||
private String stepRepr(int i) {
|
||||
return switch (path.getStep(i)) {
|
||||
case TypePath.ARRAY_ELEMENT -> "[";
|
||||
case TypePath.INNER_TYPE -> ".";
|
||||
case TypePath.WILDCARD_BOUND -> "*";
|
||||
case TypePath.TYPE_ARGUMENT -> path.getStepArgument(i) + ";";
|
||||
default -> throw new AssertionError("Unexpected type path step kind: " + path.getStep(i));
|
||||
};
|
||||
}
|
||||
|
||||
private String remainingSteps() {
|
||||
if (path == null || !hasMoreSteps()) {
|
||||
return "<none>";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (int i = pathIndex; i < pathLen; i++) {
|
||||
sb.append(stepRepr(i));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void consumeStep() {
|
||||
pathIndex++;
|
||||
|
||||
if (pathIndex == pathLen) {
|
||||
reached = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getError() {
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (!reached) {
|
||||
return "TypePath not fully consumed at index " + pathIndex + ", remaining steps: '" + remainingSteps() + "'";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitBaseType(final char descriptor) {
|
||||
if (hasMoreSteps()) {
|
||||
error = "TypePath has extra steps starting at index " + pathIndex + " ('" + remainingSteps()
|
||||
+ "') but reached base type descriptor '" + descriptor + "'.";
|
||||
} else {
|
||||
reached = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitTypeVariable(final String name) {
|
||||
if (hasMoreSteps()) {
|
||||
error = "TypePath has extra steps starting at index " + pathIndex + " ('" + remainingSteps()
|
||||
+ "') but reached type variable '" + name + "'.";
|
||||
} else {
|
||||
reached = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureVisitor visitArrayType() {
|
||||
if (hasMoreSteps()) {
|
||||
if (nextStepKind() == TypePath.ARRAY_ELEMENT) {
|
||||
consumeStep();
|
||||
} else {
|
||||
error = "At step " + pathIndex + " expected array element '[' but found '" + stepRepr(pathIndex) + "'.";
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitClassType(final String name) {
|
||||
argIndexStack.push(0);
|
||||
|
||||
String[] innerParts = name.split("\\$");
|
||||
|
||||
for (int i = 1; i < innerParts.length; i++) {
|
||||
visitInnerClassType(innerParts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitInnerClassType(final String name) {
|
||||
if (hasMoreSteps()) {
|
||||
if (nextStepKind() == TypePath.INNER_TYPE) {
|
||||
consumeStep();
|
||||
} else {
|
||||
error = "At step " + pathIndex + " expected inner type '.' but found '" + stepRepr(pathIndex) + "'.";
|
||||
}
|
||||
}
|
||||
|
||||
argIndexStack.push(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitTypeArgument() {
|
||||
// unbounded wildcard '*' — terminal
|
||||
if (error != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (argIndexStack.isEmpty()) {
|
||||
error = "Type signature has no enclosing class type for an unbounded wildcard at step " + pathIndex + ".";
|
||||
return;
|
||||
}
|
||||
|
||||
int idx = argIndexStack.pop();
|
||||
boolean targeted = hasMoreSteps() && nextStepKind() == TypePath.TYPE_ARGUMENT && nextStepArgumentIndex() == idx;
|
||||
|
||||
if (targeted) {
|
||||
consumeStep();
|
||||
|
||||
if (hasMoreSteps()) {
|
||||
error = "TypePath targets unbounded wildcard '*' at step " + (pathIndex - 1)
|
||||
+ " but contains further steps; '*' is terminal.";
|
||||
} else {
|
||||
reached = true;
|
||||
}
|
||||
}
|
||||
|
||||
argIndexStack.push(idx + 1);
|
||||
// no nested visits for unbounded wildcard
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureVisitor visitTypeArgument(final char wildcard) {
|
||||
if (error != null) {
|
||||
return sink;
|
||||
}
|
||||
|
||||
if (argIndexStack.isEmpty()) {
|
||||
error = "Type signature has no enclosing class type for a type argument at step " + pathIndex + ".";
|
||||
return sink;
|
||||
}
|
||||
|
||||
int idx = argIndexStack.pop();
|
||||
boolean targeted = hasMoreSteps() && nextStepKind() == TypePath.TYPE_ARGUMENT && nextStepArgumentIndex() == idx;
|
||||
|
||||
if (targeted) {
|
||||
consumeStep();
|
||||
|
||||
if (pathIndex == pathLen) {
|
||||
// Path targets the whole type argument; success if no further navigation needed.
|
||||
argIndexStack.push(idx + 1);
|
||||
return sink;
|
||||
}
|
||||
}
|
||||
|
||||
argIndexStack.push(idx + 1);
|
||||
|
||||
if (hasMoreSteps() && nextStepKind() == TypePath.WILDCARD_BOUND) {
|
||||
if (wildcard == SignatureVisitor.EXTENDS || wildcard == SignatureVisitor.SUPER) {
|
||||
consumeStep();
|
||||
} else {
|
||||
error = "At step " + pathIndex + " found wildcard bound '*' but the type argument is exact ('=').";
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitEnd() {
|
||||
if (argIndexStack.isEmpty()) {
|
||||
if (error == null) {
|
||||
error = "visitEnd encountered with no matching class/inner-class at step " + pathIndex + ".";
|
||||
}
|
||||
} else {
|
||||
argIndexStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,951 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.test.unit.processor
|
||||
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.objectweb.asm.ClassReader
|
||||
import org.objectweb.asm.TypeReference
|
||||
import org.objectweb.asm.tree.ClassNode
|
||||
import spock.lang.Specification
|
||||
|
||||
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.AnnotationsData
|
||||
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.validate.AnnotationsDataValidator
|
||||
|
||||
@SuppressWarnings("JsonStandardCompliance")
|
||||
class AnnotationsDataValidatorTest extends Specification {
|
||||
private static final String TEST_CLASSES_PACKAGE_INTERNAL = "net/fabricmc/loom/test/unit/processor/classes/"
|
||||
|
||||
private static String internalName(String simpleName) {
|
||||
return TEST_CLASSES_PACKAGE_INTERNAL + simpleName
|
||||
}
|
||||
|
||||
private static ClassNode loadClassNode(String internalName) {
|
||||
ClassReader cr = new ClassReader(internalName)
|
||||
ClassNode cn = new ClassNode()
|
||||
cr.accept(cn, 0)
|
||||
return cn
|
||||
}
|
||||
|
||||
class TestValidator extends AnnotationsDataValidator {
|
||||
final List<String> errors
|
||||
|
||||
TestValidator(List<String> errors) {
|
||||
this.errors = errors
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ClassNode getClass(String name, boolean includeLibraries) {
|
||||
try {
|
||||
return loadClassNode(name)
|
||||
} catch (Exception ignored) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void error(String message, Object... args) {
|
||||
errors << formatLog(message, args)
|
||||
}
|
||||
|
||||
private String formatLog(String message, Object... args) {
|
||||
if (message == null) return ""
|
||||
if (!args || args.length == 0) return message
|
||||
StringBuilder sb = new StringBuilder()
|
||||
int last = 0
|
||||
int argIdx = 0
|
||||
while (true) {
|
||||
int idx = message.indexOf("{}", last)
|
||||
if (idx == -1) {
|
||||
sb.append(message.substring(last))
|
||||
break
|
||||
}
|
||||
sb.append(message.substring(last, idx))
|
||||
if (argIdx < args.length) {
|
||||
sb.append(String.valueOf(args[argIdx++]))
|
||||
} else {
|
||||
sb.append("{}")
|
||||
}
|
||||
last = idx + 2
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
def "valid: removing an existing annotation should pass (class, field, method, param)"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithAnnotations")
|
||||
String annInternal = internalName("AnnPresent")
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"remove": [
|
||||
"${annInternal}"
|
||||
],
|
||||
"fields": {
|
||||
"bar:I": {
|
||||
"remove": ["${annInternal}"]
|
||||
}
|
||||
},
|
||||
"methods": {
|
||||
"method(Ljava/lang/String;)V": {
|
||||
"remove": ["${annInternal}"],
|
||||
"params": {
|
||||
"0": {
|
||||
"remove": ["${annInternal}"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
ok
|
||||
errors.isEmpty()
|
||||
}
|
||||
|
||||
def "valid: removing type parameter annotation on implements and return type"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithImplements")
|
||||
String classWithReturnTypeInternal = internalName("ClassWithReturnType")
|
||||
String annInternal = internalName("AnnPresent")
|
||||
def implTypeRef = TypeReference.newSuperTypeReference(0).value
|
||||
def returnTypeRef = TypeReference.newTypeReference(TypeReference.METHOD_RETURN).value
|
||||
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"type_remove": [
|
||||
{
|
||||
"name": "${annInternal}",
|
||||
"type_ref": ${implTypeRef},
|
||||
"type_path": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"${classWithReturnTypeInternal}": {
|
||||
"methods": {
|
||||
"annotatedGenericReturn()Ljava/util/List;": {
|
||||
"type_remove": [
|
||||
{
|
||||
"name": "${annInternal}",
|
||||
"type_ref": ${returnTypeRef},
|
||||
"type_path": "0;"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
ok
|
||||
errors.isEmpty()
|
||||
}
|
||||
|
||||
def "valid: adding annotations and type annotations to class, field, method, and parameter"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithoutAnnotations")
|
||||
String genericInternal = internalName("AdvancedGenericTargetClass")
|
||||
String addAnn = internalName("AnnAdd")
|
||||
def classTypeParamRef = TypeReference.newTypeParameterReference(TypeReference.CLASS_TYPE_PARAMETER, 0).value
|
||||
def fieldTypeRef = TypeReference.newTypeReference(TypeReference.FIELD).value
|
||||
def methodReturnRef = TypeReference.newTypeReference(TypeReference.METHOD_RETURN).value
|
||||
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"add": [
|
||||
{
|
||||
"desc": "L${addAnn};"
|
||||
}
|
||||
],
|
||||
"fields": {
|
||||
"otherField:I": {
|
||||
"add": [
|
||||
{
|
||||
"desc": "L${addAnn};"
|
||||
}
|
||||
],
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${addAnn};",
|
||||
"type_ref": ${fieldTypeRef},
|
||||
"type_path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"methods": {
|
||||
"otherMethodWithParams(I)V": {
|
||||
"add": [
|
||||
{
|
||||
"desc": "L${addAnn};"
|
||||
}
|
||||
],
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${addAnn};",
|
||||
"type_ref": ${methodReturnRef},
|
||||
"type_path": ""
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
"0": {
|
||||
"add": [
|
||||
{
|
||||
"desc": "L${addAnn};"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"${genericInternal}": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${addAnn};",
|
||||
"type_ref": ${classTypeParamRef},
|
||||
"type_path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
ok
|
||||
errors.isEmpty()
|
||||
}
|
||||
|
||||
def "invalid: removing an annotation that isn't present should produce an error"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithoutAnnotations")
|
||||
String annNotPresentInternal = internalName("AnnNotPresent")
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"remove": [
|
||||
"${annNotPresentInternal}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
String expected = "Trying to remove annotation ${annNotPresentInternal} from ${classInternal} but it's not present"
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
!ok
|
||||
errors.contains(expected)
|
||||
}
|
||||
|
||||
def "invalid: adding an annotation already present should produce an error"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithAnnotations")
|
||||
String annPresentInternal = internalName("AnnPresent")
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"add": [
|
||||
{
|
||||
"desc": "L${annPresentInternal};"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
String expected = "Trying to add annotation L${annPresentInternal}; to ${classInternal} but it's already present"
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
!ok
|
||||
errors.contains(expected)
|
||||
}
|
||||
|
||||
def "invalid: adding/removing annotations to fields/methods/classes/params that don't exist should produce errors"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithoutAnnotations")
|
||||
String annAddInternal = internalName("AnnAdd")
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"fields": {
|
||||
"noSuchField:I": {
|
||||
"remove": ["${annAddInternal}"]
|
||||
}
|
||||
},
|
||||
"methods": {
|
||||
"otherMethod()V": {
|
||||
"add": [
|
||||
{"desc": "L${annAddInternal};"}
|
||||
],
|
||||
"parameters": {
|
||||
"5": {
|
||||
"add": [{"desc": "L${annAddInternal};"}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"noSuchMethod()V": {
|
||||
"add": [
|
||||
{"desc": "L${annAddInternal};"}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"net/fabricmc/loom/test/unit/processor/classes/NonExistentClass": {
|
||||
"add": [
|
||||
{"desc": "L${annAddInternal};"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
String expectedFieldErr = "No such target field: ${classInternal}.noSuchField:I"
|
||||
String expectedMethodErr = "No such target method: ${classInternal}.noSuchMethod()V"
|
||||
String expectedParamErr = "Invalid parameter index: 5 for method: ${classInternal}.otherMethod()V"
|
||||
String expectedClassNotFound = "No such target class: net/fabricmc/loom/test/unit/processor/classes/NonExistentClass"
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
!ok
|
||||
errors.contains(expectedFieldErr)
|
||||
errors.contains(expectedMethodErr)
|
||||
errors.contains(expectedParamErr)
|
||||
errors.contains(expectedClassNotFound)
|
||||
}
|
||||
|
||||
def "invalid: adding annotation with attribute that doesn't exist; valid: adding with attribute that does exist"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithoutAnnotations")
|
||||
String annAttr = internalName("AnnWithAttr")
|
||||
@Language("JSON")
|
||||
String jsonBad = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"add": [
|
||||
{
|
||||
"desc": "L${annAttr};",
|
||||
"values": {
|
||||
"nonexistent": { "type": "int", "value": 5 }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def readerBad = new BufferedReader(new StringReader(jsonBad))
|
||||
def dataBad = AnnotationsData.read(readerBad)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
String expectedBad = "Unknown annotation attribute: ${annAttr}.nonexistent"
|
||||
String expectedBadMissing = "Annotation applied to ${classInternal} is missing required attributes: [value]"
|
||||
|
||||
when:
|
||||
boolean okBad = validator.checkData(dataBad)
|
||||
|
||||
then:
|
||||
!okBad
|
||||
errors.contains(expectedBad)
|
||||
errors.contains(expectedBadMissing)
|
||||
|
||||
when: "now add correct attribute"
|
||||
errors.clear()
|
||||
@Language("JSON")
|
||||
String jsonGood = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"add": [
|
||||
{
|
||||
"desc": "L${annAttr};",
|
||||
"values": {
|
||||
"value": { "type": "int", "value": 5 }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def readerGood = new BufferedReader(new StringReader(jsonGood))
|
||||
def dataGood = AnnotationsData.read(readerGood)
|
||||
|
||||
boolean okGood = validator.checkData(dataGood)
|
||||
|
||||
then:
|
||||
okGood
|
||||
errors.isEmpty()
|
||||
}
|
||||
|
||||
def "invalid: adding annotation with attribute value of wrong type"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithoutAnnotations")
|
||||
String annAttr = internalName("AnnWithAttr")
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"add": [
|
||||
{
|
||||
"desc": "L${annAttr};",
|
||||
"values": {
|
||||
"value": { "type": "string", "value": "not-an-int" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
String expected = "Annotation value is of type java.lang.String, expected int for attribute value"
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
!ok
|
||||
errors.contains(expected)
|
||||
}
|
||||
|
||||
def "invalid: adding/removing type-parameters that don't exist"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithGenericParams")
|
||||
String annInternal = internalName("AnnAdd")
|
||||
|
||||
def classTypeParamRef = TypeReference.newTypeParameterReference(TypeReference.CLASS_TYPE_PARAMETER, 5).value
|
||||
def methodTypeParamRef = TypeReference.newTypeParameterReference(TypeReference.METHOD_TYPE_PARAMETER, 3).value
|
||||
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${annInternal};",
|
||||
"type_ref": ${classTypeParamRef},
|
||||
"type_path": ""
|
||||
}
|
||||
],
|
||||
"methods": {
|
||||
"methodWithTypeParam()V": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${annInternal};",
|
||||
"type_ref": ${methodTypeParamRef},
|
||||
"type_path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
String expectedClassTP = "Invalid type reference for class type annotation: ${classTypeParamRef}, formal parameter index 5 out of bounds"
|
||||
String expectedMethodTP = "Invalid type reference for method type annotation: ${methodTypeParamRef}, formal parameter index 3 out of bounds"
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
!ok
|
||||
errors.contains(expectedClassTP)
|
||||
errors.contains(expectedMethodTP)
|
||||
}
|
||||
|
||||
def "valid: type annotation on class type parameter passes"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithGenericParams")
|
||||
String ann = internalName("AnnPresent")
|
||||
def typeRef = TypeReference.newTypeParameterReference(TypeReference.CLASS_TYPE_PARAMETER, 1).value
|
||||
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${ann};",
|
||||
"type_ref": ${typeRef},
|
||||
"type_path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
ok
|
||||
errors.isEmpty()
|
||||
}
|
||||
|
||||
def "valid: type annotation on class type parameter bound passes"() {
|
||||
given:
|
||||
String classInternal = internalName("SelfGenericInterface")
|
||||
String ann = internalName("AnnPresent")
|
||||
def typeRef = TypeReference.newTypeParameterBoundReference(TypeReference.CLASS_TYPE_PARAMETER_BOUND, 0, 0).value
|
||||
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${ann};",
|
||||
"type_ref": ${typeRef},
|
||||
"type_path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
ok
|
||||
errors.isEmpty()
|
||||
}
|
||||
|
||||
def "valid: type annotation on extends (superclass) passes"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithGenericParams")
|
||||
String ann = internalName("AnnPresent")
|
||||
def extendsRef = TypeReference.newSuperTypeReference(-1).value
|
||||
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${ann};",
|
||||
"type_ref": ${extendsRef},
|
||||
"type_path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
ok
|
||||
errors.isEmpty()
|
||||
}
|
||||
|
||||
def "valid: field type annotation with type path passes"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithFieldTypes")
|
||||
String ann = internalName("AnnPresent")
|
||||
def fieldRef = TypeReference.newTypeReference(TypeReference.FIELD).value
|
||||
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"fields": {
|
||||
"listField:Ljava/util/List;": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${ann};",
|
||||
"type_ref": ${fieldRef},
|
||||
"type_path": "0;"
|
||||
}
|
||||
]
|
||||
},
|
||||
"arrayField:[Ljava/lang/String;": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${ann};",
|
||||
"type_ref": ${fieldRef},
|
||||
"type_path": "["
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
ok
|
||||
errors.isEmpty()
|
||||
}
|
||||
|
||||
def "valid: method return type annotation with type path passes"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithReturnType")
|
||||
String ann = internalName("AnnPresent")
|
||||
def returnRef = TypeReference.newTypeReference(TypeReference.METHOD_RETURN).value
|
||||
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"methods": {
|
||||
"genericReturn()Ljava/util/List;": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${ann};",
|
||||
"type_ref": ${returnRef},
|
||||
"type_path": "0;"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
ok
|
||||
errors.isEmpty()
|
||||
}
|
||||
|
||||
def "receiver type annotation: valid on instance methods, invalid on static methods and constructors"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithReceiverAndParams")
|
||||
String ann = internalName("AnnPresent")
|
||||
def receiverRef = TypeReference.newTypeReference(TypeReference.METHOD_RECEIVER).value
|
||||
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"methods": {
|
||||
"instanceMethod(ILjava/lang/String;)V": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${ann};",
|
||||
"type_ref": ${receiverRef},
|
||||
"type_path": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"staticMethod(I)V": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${ann};",
|
||||
"type_ref": ${receiverRef},
|
||||
"type_path": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"<init>(I)V": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${ann};",
|
||||
"type_ref": ${receiverRef},
|
||||
"type_path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
String expectedStaticReceiverErr = "Invalid type reference for method type annotation: ${receiverRef}, method receiver used in a static context"
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
!ok
|
||||
errors.size() == 2
|
||||
errors.contains(expectedStaticReceiverErr)
|
||||
}
|
||||
|
||||
def "valid: type annotation on method formal parameter passes"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithReceiverAndParams")
|
||||
String ann = internalName("AnnPresent")
|
||||
def formalParamRef = TypeReference.newFormalParameterReference(0).value
|
||||
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"methods": {
|
||||
"instanceMethod(ILjava/lang/String;)V": {
|
||||
"params": {
|
||||
"0": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${ann};",
|
||||
"type_ref": ${formalParamRef},
|
||||
"type_path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
ok
|
||||
errors.isEmpty()
|
||||
}
|
||||
|
||||
def "invalid: type annotation referring to out-of-bounds checked-exception index should produce an error"() {
|
||||
given:
|
||||
String classInternal = internalName("ClassWithoutAnnotations")
|
||||
String annTypeInternal = internalName("AnnAdd")
|
||||
int typeRef = TypeReference.newExceptionReference(0).value
|
||||
@Language("JSON")
|
||||
String json = """
|
||||
{
|
||||
"version": 1,
|
||||
"classes": {
|
||||
"${classInternal}": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${annTypeInternal};",
|
||||
"type_ref": ${typeRef},
|
||||
"type_path": "["
|
||||
}
|
||||
],
|
||||
"methods": {
|
||||
"otherMethod()V": {
|
||||
"type_add": [
|
||||
{
|
||||
"desc": "L${annTypeInternal};",
|
||||
"type_ref": ${typeRef},
|
||||
"type_path": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespace": "test"
|
||||
}
|
||||
"""
|
||||
def reader = new BufferedReader(new StringReader(json))
|
||||
def data = AnnotationsData.read(reader)
|
||||
|
||||
List<String> errors = []
|
||||
def validator = new TestValidator(errors)
|
||||
|
||||
String expectedClassTypeRefErr = "Invalid type reference for class type annotation: ${typeRef}"
|
||||
String expectedMethodTypeRefErr = "Invalid type reference for method type annotation: ${typeRef}, exception index 0 out of bounds"
|
||||
|
||||
when:
|
||||
boolean ok = validator.checkData(data)
|
||||
|
||||
then:
|
||||
!ok
|
||||
errors.contains(expectedClassTypeRefErr)
|
||||
errors.contains(expectedMethodTypeRefErr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.test.unit.processor
|
||||
|
||||
import org.jetbrains.annotations.Nullable
|
||||
import org.objectweb.asm.TypePath
|
||||
import org.objectweb.asm.signature.SignatureReader
|
||||
import spock.lang.Specification
|
||||
|
||||
import net.fabricmc.loom.configuration.providers.mappings.extras.annotations.validate.TypePathCheckerVisitor
|
||||
|
||||
class TypePathCheckerVisitorTest extends Specification {
|
||||
def "empty / null TypePath targets the whole type"() {
|
||||
expect:
|
||||
validate(null, "Ljava/lang/String;") == null
|
||||
validate("", "Ljava/lang/String;") == null
|
||||
}
|
||||
|
||||
def "array element steps validate correctly"() {
|
||||
expect:
|
||||
validate("[", "[Ljava/lang/String;") == null
|
||||
validate("[[", "[Ljava/lang/String;") == "TypePath not fully consumed at index 1, remaining steps: '['"
|
||||
}
|
||||
|
||||
def "type argument indexing and bounds"() {
|
||||
expect:
|
||||
// List<String>
|
||||
validate("0;", "Ljava/util/List<Ljava/lang/String;>;") == null
|
||||
// second arg doesn't exist
|
||||
validate("1;", "Ljava/util/List<Ljava/lang/String;>;") == "TypePath not fully consumed at index 0, remaining steps: '1;'"
|
||||
|
||||
// List<?> -> unbounded wildcard is a valid terminal target for type-argument 0
|
||||
validate("0;", "Ljava/util/List<*>;") == null
|
||||
// but attempting to follow with a wildcard-bound step (0;*) is invalid for an unbounded wildcard
|
||||
validate("0;*", "Ljava/util/List<*>;") == "TypePath targets unbounded wildcard '*' at step 0 but contains further steps; '*' is terminal."
|
||||
|
||||
// List<? extends Number> -> wildcard bound (*) after selecting arg 0 is valid
|
||||
validate("0;*", "Ljava/util/List<+Ljava/lang/Number;>;") == null
|
||||
}
|
||||
|
||||
def "inner class stepping"() {
|
||||
expect:
|
||||
// Simple inner class descriptor
|
||||
validate(".", "Lcom/example/Outer\$Inner;") == null
|
||||
|
||||
// Wrong step (trying to use array step on a non-array)
|
||||
validate("[", "Lcom/example/Outer\$Inner;") == "At step 0 expected inner type '.' but found '['."
|
||||
}
|
||||
|
||||
def "type variable cannot be followed by extra steps"() {
|
||||
expect:
|
||||
validate("0;", "TMyType;") == "TypePath has extra steps starting at index 0 ('0;') but reached type variable 'MyType'."
|
||||
}
|
||||
|
||||
def "complex: nested generics and bounds"() {
|
||||
expect:
|
||||
// Map<String, List<? extends Number>>
|
||||
String sig = "Ljava/util/Map<Ljava/lang/String;Ljava/util/List<+Ljava/lang/Number;>;>;"
|
||||
// target Map value type argument (index 1) and then the List's type argument (index 0) and its bound:
|
||||
// path: 1;0;* -> TYPE_ARGUMENT(1) then TYPE_ARGUMENT(0) then WILDCARD_BOUND
|
||||
String path = "1;0;*"
|
||||
validate(path, sig) == null
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String validate(@Nullable String typePath, String signature) {
|
||||
TypePath typePathObj = typePath == null ? null : TypePath.fromString(typePath)
|
||||
TypePathCheckerVisitor visitor = new TypePathCheckerVisitor(typePathObj)
|
||||
new SignatureReader(signature).acceptType(visitor)
|
||||
return visitor.error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.test.unit.processor.classes;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
|
||||
public @interface AnnAdd { }
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.test.unit.processor.classes;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
|
||||
public @interface AnnNotPresent { }
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.test.unit.processor.classes;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD, ElementType.TYPE_USE })
|
||||
public @interface AnnPresent { }
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.test.unit.processor.classes;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
// annotation with a single int attribute named `value`
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD, ElementType.TYPE_USE })
|
||||
public @interface AnnWithAttr {
|
||||
int value();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.test.unit.processor.classes;
|
||||
|
||||
@AnnPresent
|
||||
public class ClassWithAnnotations {
|
||||
@AnnPresent
|
||||
public int bar;
|
||||
|
||||
@AnnPresent
|
||||
public void method(@AnnPresent String param) { }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.test.unit.processor.classes;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ClassWithFieldTypes {
|
||||
public List<String> listField;
|
||||
|
||||
public String[] arrayField;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.test.unit.processor.classes;
|
||||
|
||||
public class ClassWithGenericParams<@AnnPresent T, U> {
|
||||
public <@AnnPresent V> void methodWithTypeParam() { }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.test.unit.processor.classes;
|
||||
|
||||
public class ClassWithImplements implements @AnnPresent SimpleInterface {
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.test.unit.processor.classes;
|
||||
|
||||
public class ClassWithReceiverAndParams {
|
||||
public void instanceMethod(int a, String b) { }
|
||||
|
||||
public static void staticMethod(int a) { }
|
||||
|
||||
public ClassWithReceiverAndParams(int a) { }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.test.unit.processor.classes;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ClassWithReturnType {
|
||||
public List<String> genericReturn() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<@AnnPresent String> annotatedGenericReturn() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2025 FabricMC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.fabricmc.loom.test.unit.processor.classes;
|
||||
|
||||
public class ClassWithoutAnnotations {
|
||||
public int otherField;
|
||||
|
||||
public void otherMethod() {
|
||||
}
|
||||
|
||||
public void otherMethodWithParams(int param) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user