diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationNodeSerializer.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationNodeSerializer.java index 928902686..3eda04a25 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationNodeSerializer.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/TypeAnnotationNodeSerializer.java @@ -31,6 +31,7 @@ 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 @@ public TypeAnnotationNode deserialize(JsonElement json, Type typeOfT, JsonDeseri 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 @@ public TypeAnnotationNode deserialize(JsonElement json, Type typeOfT, JsonDeseri 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; } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/validate/AnnotationsDataValidator.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/validate/AnnotationsDataValidator.java new file mode 100644 index 000000000..5450cc34e --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/validate/AnnotationsDataValidator.java @@ -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 annotations = concatLists(clazz.visibleAnnotations, clazz.invisibleAnnotations); + result &= checkAnnotationsToRemove(() -> className, classData.annotationsToRemove(), annotations); + result &= checkAnnotationsToAdd(() -> className, classData.annotationsToAdd(), annotations); + List 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 errorKey, GenericAnnotationData data, List annotations, List 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 errorKey, MethodAnnotationData data, String className, MethodNode method) { + boolean result = true; + List annotations = concatLists(method.visibleAnnotations, method.invisibleAnnotations); + result &= checkAnnotationsToRemove(errorKey, data.annotationsToRemove(), annotations); + result &= checkAnnotationsToAdd(errorKey, data.annotationsToAdd(), annotations); + List 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 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 errorKey, Set annotationsToRemove, List annotations) { + Set 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 errorKey, List annotationsToAdd, List annotations) { + Set 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 errorKey, Set typeAnnotationsToRemove, List typeAnnotations) { + Set 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 errorKey, List typeAnnotationsToAdd, List typeAnnotations, TypeAnnotationChecker checker) { + Set 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 || "".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 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 missingRequiredAttributes = new LinkedHashSet<>(); + Map 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 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 List concatLists(@Nullable List list1, @Nullable List list2) { + List 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); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/validate/TypePathCheckerVisitor.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/validate/TypePathCheckerVisitor.java new file mode 100644 index 000000000..bc3650588 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/annotations/validate/TypePathCheckerVisitor.java @@ -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 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 ""; + } + + 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(); + } + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/processor/AnnotationsDataValidatorTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/processor/AnnotationsDataValidatorTest.groovy new file mode 100644 index 000000000..903927270 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/processor/AnnotationsDataValidatorTest.groovy @@ -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 errors + + TestValidator(List 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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": "" + } + ] + }, + "(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 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 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 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) + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/processor/TypePathCheckerVisitorTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/processor/TypePathCheckerVisitorTest.groovy new file mode 100644 index 000000000..1419d8039 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/processor/TypePathCheckerVisitorTest.groovy @@ -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 + validate("0;", "Ljava/util/List;") == null + // second arg doesn't exist + validate("1;", "Ljava/util/List;") == "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 -> 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 sig = "Ljava/util/Map;>;" + // 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 + } +} \ No newline at end of file diff --git a/src/test/java/net/fabricmc/loom/test/unit/processor/classes/AnnAdd.java b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/AnnAdd.java new file mode 100644 index 000000000..a4c73780c --- /dev/null +++ b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/AnnAdd.java @@ -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 { } diff --git a/src/test/java/net/fabricmc/loom/test/unit/processor/classes/AnnNotPresent.java b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/AnnNotPresent.java new file mode 100644 index 000000000..883caf9ec --- /dev/null +++ b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/AnnNotPresent.java @@ -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 { } diff --git a/src/test/java/net/fabricmc/loom/test/unit/processor/classes/AnnPresent.java b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/AnnPresent.java new file mode 100644 index 000000000..fa6491758 --- /dev/null +++ b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/AnnPresent.java @@ -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 { } diff --git a/src/test/java/net/fabricmc/loom/test/unit/processor/classes/AnnWithAttr.java b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/AnnWithAttr.java new file mode 100644 index 000000000..be6c6f37d --- /dev/null +++ b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/AnnWithAttr.java @@ -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(); +} diff --git a/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithAnnotations.java b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithAnnotations.java new file mode 100644 index 000000000..150581d86 --- /dev/null +++ b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithAnnotations.java @@ -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) { } +} diff --git a/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithFieldTypes.java b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithFieldTypes.java new file mode 100644 index 000000000..6a0d20456 --- /dev/null +++ b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithFieldTypes.java @@ -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 listField; + + public String[] arrayField; +} diff --git a/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithGenericParams.java b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithGenericParams.java new file mode 100644 index 000000000..cc0396dd6 --- /dev/null +++ b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithGenericParams.java @@ -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() { } +} + diff --git a/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithImplements.java b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithImplements.java new file mode 100644 index 000000000..2d4a9ac96 --- /dev/null +++ b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithImplements.java @@ -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 { +} diff --git a/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithReceiverAndParams.java b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithReceiverAndParams.java new file mode 100644 index 000000000..9baba5ef2 --- /dev/null +++ b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithReceiverAndParams.java @@ -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) { } +} diff --git a/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithReturnType.java b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithReturnType.java new file mode 100644 index 000000000..6870538e0 --- /dev/null +++ b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithReturnType.java @@ -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 genericReturn() { + return null; + } + + public List<@AnnPresent String> annotatedGenericReturn() { + return null; + } +} diff --git a/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithoutAnnotations.java b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithoutAnnotations.java new file mode 100644 index 000000000..f5416cc97 --- /dev/null +++ b/src/test/java/net/fabricmc/loom/test/unit/processor/classes/ClassWithoutAnnotations.java @@ -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) { + } +}