Skip to content

Commit

Permalink
Rework existing annotations with Filter
Browse files Browse the repository at this point in the history
also, add documentation
  • Loading branch information
pauxus committed Nov 5, 2023
1 parent a081ae1 commit a46109f
Show file tree
Hide file tree
Showing 13 changed files with 110 additions and 47 deletions.
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -285,8 +285,26 @@ class NameMustMatchCheck extends KlumCastCheck<NameMustMatch> {
}
}
```
### 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,54 @@
* The filter can be of the following types:
* <ul>
* <li>{@link ElementType[]}: The check is executed if the annotated element is one of the given types</li>
* <li>{@link Class<Filter.Function>>}: a filter implementation</li>
* <li>{@link Filter.Function}: the type of a filter implementation</li>
* <li>{@link String}: The fully qualified classname of the filter implementation</li>
* </ul>
* </p>
* <p>In order for a check to be executed, <b>all</b> Filter checks must pass.</p>
*/
@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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* The validFor parameter can be used to restrict the usage of the validator to certain elements.
* </p>
*/
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
Expand All @@ -53,6 +56,12 @@
*/
String[] parameters() default {};

/**
* The elements the validator is valid for.
* @return the elements the validator is valid for.
*/
@Filter Class<? extends Filter.Function> validFor() default Filter.All.class;

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface List {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,21 +41,15 @@
*/
@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<MustBeStatic> {

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -51,11 +52,9 @@ class Check extends KlumCastCheck<NumberOfParameters> {

@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.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,4 @@ protected void doCheck(AnnotationNode annotationToCheck, AnnotatedNode target) {
));
}
}

@Override
protected boolean isValidFor(AnnotatedNode target) {
return target instanceof MethodNode;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)) {
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -37,6 +38,9 @@ class MustBeStaticTest extends AstSpec {
@MustBeStatic
@interface MyAnnotation {}
'''
}

def "Static method works"() {
when:
createClass '''
class MyClass {
Expand All @@ -57,4 +61,16 @@ class MyClass {
then:
thrown(MultipleCompilationErrorsException)
}

def "mustBeStatic ignores classes"() {
when:
createClass '''
@MyAnnotation
class MyClass {
static List<String> myMethod() { null }
}'''

then:
notThrown(MultipleCompilationErrorsException)
}
}

0 comments on commit a46109f

Please sign in to comment.