diff --git a/README.md b/README.md index 07c5bc1..6980823 100644 --- a/README.md +++ b/README.md @@ -119,11 +119,11 @@ class HueBase extends BaseStation { The Annotations in the above example all have to follow additional placement rules that need to be checked by the AST-Transformation consuming them: -- Field.members is only valid on Collections and Maps -- Key and Owner annotations are only valid on classes annotated with @DSL -- @Key is only valid on String fields -- @Owner is only valid on fields or on single argument methods -- @DSL.stripSuffix is only valid on non-final classes +- `@Field.members` is only valid on Collections and Maps +- `@Key` and `@Owner` annotations are only valid on classes annotated with `@DSL` +- `@Key` is only valid on String fields +- `@Owner` is only valid on fields or on single argument methods +- `@DSL.stripSuffix` is only valid on non-final classes # Usage @@ -285,8 +285,26 @@ class NameMustMatchCheck extends KlumCastCheck { } } ``` +### Filtering Checks -Additionally, the method `isValidFor(AnnotatedNode target)` can be overridden quickly skip the check if necessary. +Filters can be set to determine that a check is only valid in certain condition. This can be done in three ways: + +#### KlumCastCheck.isValidFor(AnnotatedNode target) +By overriding the `isValidFor(AnnotatedNode target)` method, custom checks can be implemented directly in the check implementation. + +### KlumCastValidator.validFor() + +The KlumCastValidator Annotation has a member `validFor` of type `ElementType[]`. Only if the annotated target is one of the listed types here, the Check is executed. + +### @Filter annotation-members on annotations + +By annotation a member of an annotation with `@Filter`, that member becomes a filter for its annotation chain. Filter members can either be + +- An `ElementType[]`, in which case the filter behaves exactly as `KlumCastValidator.validFor` +- A Class object containing a subclass of `Filter.Function` which acts as a custom filter +- A String containing the fully qualified Class-name of the filter implementation + +Note that in order for a check to be executed, all checks of the annotation chain must match. ## Check as inner class diff --git a/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/Filter.java b/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/Filter.java index 5f0f633..3166de9 100644 --- a/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/Filter.java +++ b/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/Filter.java @@ -14,17 +14,54 @@ * The filter can be of the following types: * *

+ *

In order for a check to be executed, all Filter checks must pass.

*/ @Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Filter { - @FunctionalInterface - interface Function { - boolean isValidFor(AnnotatedNode target); + abstract class Function { + protected Filter annotation; + + public abstract boolean isValidFor(AnnotatedNode target); + + public void setAnnotation(Filter annotation) { + this.annotation = annotation; + } + } + + /** Default filter that always matches */ + class All extends Function { + @Override + public boolean isValidFor(AnnotatedNode target) { + return true; + } + } + + /** Filter that matches if the annotated element is a method */ + class Methods extends Function { + @Override + public boolean isValidFor(AnnotatedNode target) { + return target instanceof org.codehaus.groovy.ast.MethodNode; + } + } + + /** Filter that matches if the annotated element is a field */ + class Fields extends Function { + @Override + public boolean isValidFor(AnnotatedNode target) { + return target instanceof org.codehaus.groovy.ast.FieldNode; + } + } + /** Filter that matches if the annotated element is a Class */ + class Classes extends Function { + @Override + public boolean isValidFor(AnnotatedNode target) { + return target instanceof org.codehaus.groovy.ast.ClassNode; + } } } diff --git a/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/KlumCastValidator.java b/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/KlumCastValidator.java index 1e547cb..a2936c1 100644 --- a/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/KlumCastValidator.java +++ b/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/KlumCastValidator.java @@ -30,6 +30,9 @@ /** * Meta-Annotation that defines the validator to validate the usage of the annotated annotation. * Either value or type must be set, but not both. + *

+ * The validFor parameter can be used to restrict the usage of the validator to certain elements. + *

*/ @Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -53,6 +56,12 @@ */ String[] parameters() default {}; + /** + * The elements the validator is valid for. + * @return the elements the validator is valid for. + */ + @Filter Class validFor() default Filter.All.class; + @Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) @interface List { diff --git a/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/MustBeStatic.java b/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/MustBeStatic.java index 2300303..668e9a9 100644 --- a/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/MustBeStatic.java +++ b/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/MustBeStatic.java @@ -23,6 +23,7 @@ */ package com.blackbuild.klum.cast.checks; +import com.blackbuild.klum.cast.Filter; import com.blackbuild.klum.cast.KlumCastValidator; import com.blackbuild.klum.cast.checks.impl.KlumCastCheck; import org.codehaus.groovy.ast.AnnotatedNode; @@ -40,9 +41,8 @@ */ @Target({ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) -@KlumCastValidator(type = MustBeStatic.Check.class) +@KlumCastValidator(type = MustBeStatic.Check.class, validFor = Filter.Methods.class) public @interface MustBeStatic { - class Check extends KlumCastCheck { @Override @@ -50,11 +50,6 @@ protected void doCheck(AnnotationNode annotationToCheck, AnnotatedNode target) { if (!((MethodNode) target).isStatic()) throw new IllegalStateException("Annotation " + annotationToCheck.getClassNode().getName() + " must be placed on a static method."); } - - @Override - protected boolean isValidFor(AnnotatedNode target) { - return target instanceof MethodNode; - } } } diff --git a/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/NeedsOneOf.java b/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/NeedsOneOf.java index 06e61d2..94cb25a 100644 --- a/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/NeedsOneOf.java +++ b/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/NeedsOneOf.java @@ -23,6 +23,7 @@ */ package com.blackbuild.klum.cast.checks; +import com.blackbuild.klum.cast.Filter; import com.blackbuild.klum.cast.KlumCastValidator; import java.lang.annotation.*; @@ -44,7 +45,7 @@ /** * If set, the annotation is only checked when placed on one of the given types. */ - ElementType[] whenOn() default {}; + @Filter ElementType[] whenOn() default {}; /** * If set to true, exactly one of the given members must be set. If set to false, more than one member can be set. diff --git a/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/NeedsReturnType.java b/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/NeedsReturnType.java index b3ca61e..7209ef9 100644 --- a/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/NeedsReturnType.java +++ b/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/NeedsReturnType.java @@ -23,6 +23,7 @@ */ package com.blackbuild.klum.cast.checks; +import com.blackbuild.klum.cast.Filter; import com.blackbuild.klum.cast.KlumCastValidator; import java.lang.annotation.ElementType; @@ -36,7 +37,7 @@ */ @Target({ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) -@KlumCastValidator("com.blackbuild.klum.cast.checks.impl.NeedsReturnTypeCheck") +@KlumCastValidator(value = "com.blackbuild.klum.cast.checks.impl.NeedsReturnTypeCheck", validFor = Filter.Methods.class) public @interface NeedsReturnType { Class[] value(); } diff --git a/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/NumberOfParameters.java b/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/NumberOfParameters.java index 0e4014e..bcf79da 100644 --- a/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/NumberOfParameters.java +++ b/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/NumberOfParameters.java @@ -23,6 +23,7 @@ */ package com.blackbuild.klum.cast.checks; +import com.blackbuild.klum.cast.Filter; import com.blackbuild.klum.cast.KlumCastValidator; import com.blackbuild.klum.cast.checks.impl.KlumCastCheck; import org.codehaus.groovy.ast.AnnotatedNode; @@ -39,7 +40,7 @@ */ @Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) -@KlumCastValidator(type = NumberOfParameters.Check.class) +@KlumCastValidator(type = NumberOfParameters.Check.class, validFor = Filter.Methods.class) public @interface NumberOfParameters { /** * The number of parameters the annotated method must have. @@ -51,11 +52,9 @@ class Check extends KlumCastCheck { @Override protected void doCheck(AnnotationNode annotationToCheck, AnnotatedNode target) { - if (target instanceof MethodNode) { - MethodNode methodNode = (MethodNode) target; - if (methodNode.getParameters().length != controlAnnotation.value()) - throw new RuntimeException("Method " + methodNode.getName() + " must have " + controlAnnotation.value() + " parameters."); - } + MethodNode methodNode = (MethodNode) target; + if (methodNode.getParameters().length != controlAnnotation.value()) + throw new RuntimeException("Method " + methodNode.getName() + " must have " + controlAnnotation.value() + " parameters."); } } } diff --git a/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/ParameterTypes.java b/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/ParameterTypes.java index cb57b97..dd91ab8 100644 --- a/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/ParameterTypes.java +++ b/klum-cast-annotations/src/main/java/com/blackbuild/klum/cast/checks/ParameterTypes.java @@ -23,6 +23,7 @@ */ package com.blackbuild.klum.cast.checks; +import com.blackbuild.klum.cast.Filter; import com.blackbuild.klum.cast.KlumCastValidator; import java.lang.annotation.ElementType; @@ -37,7 +38,7 @@ */ @Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) -@KlumCastValidator("com.blackbuild.klum.cast.checks.impl.ParameterTypesCheck") +@KlumCastValidator(value = "com.blackbuild.klum.cast.checks.impl.ParameterTypesCheck", validFor = Filter.Methods.class) public @interface ParameterTypes { Class[] value(); boolean strict() default false; diff --git a/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/checks/impl/NeedsOneOfCheck.java b/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/checks/impl/NeedsOneOfCheck.java index e0f3a0d..324bf02 100644 --- a/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/checks/impl/NeedsOneOfCheck.java +++ b/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/checks/impl/NeedsOneOfCheck.java @@ -24,7 +24,6 @@ package com.blackbuild.klum.cast.checks.impl; import com.blackbuild.klum.cast.checks.NeedsOneOf; -import com.blackbuild.klum.cast.validation.AstSupport; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.AnnotationNode; @@ -41,9 +40,4 @@ protected void doCheck(AnnotationNode annotationToCheck, AnnotatedNode target) { if (!controlAnnotation.exclusive() && matchingMembers.isEmpty()) throw new RuntimeException("At least one of " + Arrays.asList(controlAnnotation.value()) + " must be set"); } - - @Override - protected boolean isValidFor(AnnotatedNode target) { - return AstSupport.matchesOneOf(controlAnnotation.whenOn(), target); - } } diff --git a/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/checks/impl/NeedsReturnTypeCheck.java b/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/checks/impl/NeedsReturnTypeCheck.java index 99a0221..8b3c75d 100644 --- a/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/checks/impl/NeedsReturnTypeCheck.java +++ b/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/checks/impl/NeedsReturnTypeCheck.java @@ -37,9 +37,4 @@ protected void doCheck(AnnotationNode annotationToCheck, AnnotatedNode target) { if (Arrays.stream(controlAnnotation.value()).map(ClassHelper::make).noneMatch(r -> AstSupport.isAssignable(actualReturnType, r))) throw new IllegalStateException("Method " + ((MethodNode) target).getName() + " must return one of " + Arrays.toString(controlAnnotation.value()) + "."); } - - @Override - protected boolean isValidFor(AnnotatedNode target) { - return target instanceof MethodNode; - } } diff --git a/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/checks/impl/ParameterTypesCheck.java b/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/checks/impl/ParameterTypesCheck.java index 432fa8a..3e7325d 100644 --- a/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/checks/impl/ParameterTypesCheck.java +++ b/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/checks/impl/ParameterTypesCheck.java @@ -58,10 +58,4 @@ protected void doCheck(AnnotationNode annotationToCheck, AnnotatedNode target) { )); } } - - @Override - protected boolean isValidFor(AnnotatedNode target) { - return target instanceof MethodNode; - } - } diff --git a/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/validation/FilterHandler.java b/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/validation/FilterHandler.java index 6c25481..d0fd585 100644 --- a/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/validation/FilterHandler.java +++ b/klum-cast-compile/src/main/java/com/blackbuild/klum/cast/validation/FilterHandler.java @@ -8,8 +8,11 @@ import java.lang.annotation.ElementType; import java.lang.reflect.InvocationTargetException; +/** Varios methods for handling filters. */ public class FilterHandler { + private FilterHandler() {} + /** * Determines if the given annotation is valid for the given target. This is done by checking all * members on the target annotated with {@link Filter}. The annotation is only valid if all filters @@ -25,7 +28,7 @@ public static boolean isValidFor(Annotation annotation, AnnotatedNode target) { .allMatch(f -> f.isValidFor(target)); } - public static Filter.Function createFrom(Object memberValue, AnnotatedNode target) { + private static Filter.Function createFrom(Object memberValue, AnnotatedNode target) { if (memberValue instanceof Class) { Class filterClass = (Class) memberValue; if (Filter.Function.class.isAssignableFrom(filterClass)) { @@ -65,10 +68,10 @@ private static Filter.Function doCreateFrom(Class filterClass) { } } - public static class ElementTypeFilter implements Filter.Function { + static class ElementTypeFilter extends Filter.Function { private final ElementType[] elementTypes; - public ElementTypeFilter(ElementType[] elementTypes) { + ElementTypeFilter(ElementType[] elementTypes) { this.elementTypes = elementTypes; } diff --git a/klum-cast-compile/src/test/groovy/com/blackbuild/klum/cast/checks/MustBeStaticTest.groovy b/klum-cast-compile/src/test/groovy/com/blackbuild/klum/cast/checks/MustBeStaticTest.groovy index c3923d9..9de7d19 100644 --- a/klum-cast-compile/src/test/groovy/com/blackbuild/klum/cast/checks/MustBeStaticTest.groovy +++ b/klum-cast-compile/src/test/groovy/com/blackbuild/klum/cast/checks/MustBeStaticTest.groovy @@ -28,7 +28,8 @@ import org.codehaus.groovy.control.MultipleCompilationErrorsException class MustBeStaticTest extends AstSpec { - def "Static method works"() { + @Override + def setup() { given: createClass ''' @Target([ElementType.METHOD, ElementType.TYPE]) @@ -37,6 +38,9 @@ class MustBeStaticTest extends AstSpec { @MustBeStatic @interface MyAnnotation {} ''' + } + + def "Static method works"() { when: createClass ''' class MyClass { @@ -57,4 +61,16 @@ class MyClass { then: thrown(MultipleCompilationErrorsException) } + + def "mustBeStatic ignores classes"() { + when: + createClass ''' +@MyAnnotation +class MyClass { + static List myMethod() { null } + }''' + + then: + notThrown(MultipleCompilationErrorsException) + } }