Add AnnotationsDataValidator (#1379)

* Add AnnotationsDataValidator

* Use Constants.ASM_VERSION
This commit is contained in:
Joseph Burton
2025-10-03 18:08:09 +01:00
committed by GitHub
parent 7f95c3c60f
commit dd90d7bd29
16 changed files with 2462 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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() { }
}

View File

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

View File

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

View File

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

View File

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