From 15a5ff0e56957deac6e5d5e643b09c00c5d05dd4 Mon Sep 17 00:00:00 2001 From: Andriy Dmytruk Date: Mon, 16 Oct 2023 21:28:58 -0400 Subject: [PATCH] Fix type argument annotation inheritance --- .../validation/visitor/ValidationVisitor.java | 73 ++++++++++++++++--- .../visitor/ValidatedParseSpec.groovy | 32 ++++++++ 2 files changed, 96 insertions(+), 9 deletions(-) diff --git a/validation-processor/src/main/java/io/micronaut/validation/visitor/ValidationVisitor.java b/validation-processor/src/main/java/io/micronaut/validation/visitor/ValidationVisitor.java index 85e77cb8..388b81d0 100644 --- a/validation-processor/src/main/java/io/micronaut/validation/visitor/ValidationVisitor.java +++ b/validation-processor/src/main/java/io/micronaut/validation/visitor/ValidationVisitor.java @@ -21,20 +21,15 @@ import io.micronaut.core.annotation.Introspected; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Vetoed; -import io.micronaut.inject.ast.ClassElement; -import io.micronaut.inject.ast.ConstructorElement; -import io.micronaut.inject.ast.FieldElement; -import io.micronaut.inject.ast.MethodElement; -import io.micronaut.inject.ast.ParameterElement; -import io.micronaut.inject.ast.TypedElement; +import io.micronaut.inject.ast.*; import io.micronaut.inject.ast.annotation.MutableAnnotationMetadataDelegate; import io.micronaut.inject.processing.ProcessingException; import io.micronaut.inject.validation.RequiresValidation; import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; -import java.util.HashSet; -import java.util.Set; +import java.util.*; +import java.util.stream.Stream; /** * The visitor creates annotations utilized by the Validator. @@ -78,7 +73,6 @@ public void visitClass(ClassElement element, VisitorContext context) { classElement.annotate(Introspected.class); } classElement.getMethods().forEach(m -> visitMethod(m, context)); -// classElement.getFields().forEach(f -> visitField(f, context)); } @Override @@ -105,6 +99,9 @@ public void visitMethod(MethodElement element, VisitorContext context) { if (!visited.add(element)) { return; } + + getOverriddenMethods(element).forEach(m -> inheritAnnotationsForMethod(element, m)); + boolean isPrivate = element.isPrivate(); boolean isAbstract = element.getOwningType().isInterface() || element.getOwningType().isAbstract(); boolean requireOnConstraint = isAbstract || !isPrivate; @@ -182,4 +179,62 @@ private boolean visitTypedElementValidationAndMarkForValidationIfNeeded(TypedEle } return requires; } + + /** + * Method that makes sure that all the annotations are inherited from parent. + * In particular, type arguments annotations are not inherited by default. + */ + private void inheritAnnotationsForMethod(MethodElement method, MethodElement parent) { + ParameterElement[] methodParameters = method.getParameters(); + ParameterElement[] parentParameters = parent.getParameters(); + + for (int i = 0; i < methodParameters.length; ++i) { + inheritAnnotationsForParameter(methodParameters[i], parentParameters[i]); + } + } + + /** + * Method that makes sure that all the annotations are inherited from parent. + * In particular, type arguments annotations are not inherited by default. + */ + private void inheritAnnotationsForParameter(TypedElement element, TypedElement parentElement) { + if (element.getType().equals(parentElement.getType())) { + Stream parentAnnotations = Stream.concat( + parentElement.getAnnotationNamesByStereotype(ANN_CONSTRAINT).stream(), + parentElement.getAnnotationNamesByStereotype(ANN_VALID).stream() + ); + parentAnnotations + .filter(name -> !element.hasAnnotation(name)) + .flatMap(name -> parentElement.getAnnotationValuesByName(name).stream()) + .forEach(element::annotate); + + Map typeArguments = element.getGenericType().getTypeArguments(); + Map parentTypeArguments = parentElement.getGenericType().getTypeArguments(); + if (typeArguments.size() != parentTypeArguments.size()) { + return; + } + for (String parameter: typeArguments.keySet()) { + inheritAnnotationsForParameter( + typeArguments.get(parameter), + parentTypeArguments.get(parameter) + ); + } + } + } + + /** + * Get all the methods that current method overrides. + */ + private Collection getOverriddenMethods(MethodElement element) { + List results = new ArrayList<>(); + ClassElement classElement = element.getOwningType(); + classElement.getSuperType() + .flatMap(t -> t.getMethods().stream().filter(element::overrides).findFirst()) + .ifPresent(results::add); + classElement.getInterfaces().forEach(i -> + i.getMethods().stream().filter(element::overrides).findFirst().ifPresent(results::add) + ); + return results; + } + } diff --git a/validation-processor/src/test/groovy/io/micronaut/validation/visitor/ValidatedParseSpec.groovy b/validation-processor/src/test/groovy/io/micronaut/validation/visitor/ValidatedParseSpec.groovy index 36abc0c5..9477242f 100644 --- a/validation-processor/src/test/groovy/io/micronaut/validation/visitor/ValidatedParseSpec.groovy +++ b/validation-processor/src/test/groovy/io/micronaut/validation/visitor/ValidatedParseSpec.groovy @@ -6,6 +6,7 @@ import io.micronaut.inject.ValidatedBeanDefinition import io.micronaut.inject.validation.RequiresValidation import io.micronaut.inject.writer.BeanDefinitionVisitor import jakarta.validation.Valid +import jakarta.validation.constraints.NotBlank import java.time.LocalDate @@ -85,6 +86,37 @@ class Test { method.arguments[0].annotationMetadata.hasAnnotation("io.micronaut.validation.annotation.ValidatedElement") } + void "test constraints on inherited generic parameters make method @Validated"() { + given: + def definition = buildBeanDefinition('test.Test',''' +package test; + +import java.util.List; +import jakarta.validation.constraints.NotBlank; + +@jakarta.inject.Singleton +class Test implements TestBase { + @Override + public void setList(List list) { + } +} + +interface TestBase { + @io.micronaut.context.annotation.Executable + void setList(List<@NotBlank String> list); +} +''') + when: + def method = definition.getRequiredMethod("setList", List); + + then: + method.hasStereotype(VALIDATED_ANN) + method.arguments.size() == 1 + method.arguments[0].annotationMetadata.hasAnnotation("io.micronaut.validation.annotation.ValidatedElement") + method.arguments[0].typeParameters.size() == 1 + method.arguments[0].typeParameters[0].annotationMetadata.hasAnnotation(NotBlank) + } + void "test constraints on a controller operation make method @Validated"() { given: def definition = buildBeanDefinition('test.Test', '''