Skip to content

Commit

Permalink
Needs generics
Browse files Browse the repository at this point in the history
  • Loading branch information
pauxus committed Nov 22, 2023
1 parent d35cbf0 commit e5e12fc
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2023 Stephan Pauxberger
*
* 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 com.blackbuild.klum.cast.checks;

import com.blackbuild.klum.cast.KlumCastValidator;

import java.lang.annotation.*;

/**
* Checks that the parameters, return types, the annotated type, and the type of the annotated field (if applicable) have
* their generics set, i.e. if the respective type has a generic type parameter, it must be set.
* For methods, checking of return type and parameters can be disabled (for fields and types, this can
* simply be done using the default mechanisms, i.e. {@link com.blackbuild.klum.cast.Filter} or {@link Target}).
*/
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@KlumCastValidator("com.blackbuild.klum.cast.checks.impl.NeedsGenericsCheck")
@Repeatable(NeedsGenerics.List.class)
public @interface NeedsGenerics {
/**
* If set to false, the return type of the annotated method will not be checked. Has no effect on fields and types.
* @return true if the return type should be checked, false otherwise
*/
boolean checkReturnType() default true;
/**
* If set to false, the parameters of the annotated method will not be checked. Has no effect on fields and types.
* @return true if the parameters should be checked, false otherwise
*/
boolean checkParameters() default true;

/**
* If set to true, the check will not fail if the type has bounds on its generic type parameters. If
* false (the default), generic types must be explicit types.
* @return true if bounds are allowed, false otherwise
*/
boolean allowBounds() default false;

@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface List {
NeedsGenerics[] value();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

/**
* If the validated annotation is placed on a field, the type of that field
* must match value(). Has no effect, if the annotation is placed on any other element.
* must match value(). Has no effect if the annotation is placed on any other element.
*/
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2023 Stephan Pauxberger
*
* 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 com.blackbuild.klum.cast.checks.impl;

import com.blackbuild.klum.cast.checks.NeedsGenerics;
import org.codehaus.groovy.ast.*;

import java.util.Objects;

public class NeedsGenericsCheck extends KlumCastCheck<NeedsGenerics> {
@Override
protected void doCheck(AnnotationNode annotationToCheck, AnnotatedNode target) throws ValidationException {
Objects.requireNonNull(controlAnnotation);
if (target instanceof ClassNode) {
ClassNode classNode = (ClassNode) target;
if (missingGenericsForClassDefinition(classNode))
throw new ValidationException("Class " + (classNode).getName() + " is missing generics", annotationToCheck);
} else if (target instanceof FieldNode) {
if (missesGenerics(((FieldNode) target).getType()))
throw new ValidationException("Field " + ((FieldNode) target).getName() + " is missing generics", annotationToCheck);
} else if (target instanceof MethodNode) {
if (controlAnnotation.checkReturnType() && missesGenerics(((MethodNode) target).getReturnType()))
throw new ValidationException("Return type of method " + ((MethodNode) target).getName() + " is missing generics", annotationToCheck);

if (controlAnnotation.checkParameters())
for (Parameter parameter : ((MethodNode) target).getParameters())
if (missesGenerics(parameter.getType()))
throw new ValidationException("Parameter " + parameter.getName() + " of method " + ((MethodNode) target).getName() + " is missing generics", annotationToCheck);
}
}

private boolean missesGenerics(ClassNode type) {
if (!type.redirect().isUsingGenerics())
return false;

if (type.getGenericsTypes() == null || type.getGenericsTypes().length == 0)
return true;

if (Objects.requireNonNull(controlAnnotation).allowBounds()) return false;

for (GenericsType genericsType : type.getGenericsTypes())
if (genericsType.getUpperBounds() != null || genericsType.getLowerBound() != null)
return true;

return false;
}

private boolean missingGenericsForClassDefinition(ClassNode node) {
ClassNode superClass = node.getUnresolvedSuperClass(false);
return missesGenerics(superClass);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2023 Stephan Pauxberger
*
* 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 com.blackbuild.klum.cast.checks

import com.blackbuild.klum.cast.validation.AstSpec
import org.codehaus.groovy.control.MultipleCompilationErrorsException

class NeedsGenericsTest extends AstSpec {

@Override
def setup() {
createClass '''
import java.lang.annotation.ElementType
@Target([ElementType.METHOD, ElementType.TYPE, ElementType.FIELD])
@Retention(RetentionPolicy.RUNTIME)
@KlumCastValidated
@NeedsGenerics
@interface MyAnnotation {}
'''
}

def "Works if Generics are specified"() {

when: 'on field'
createClass '''
class MyClass {
@MyAnnotation
List<String> myField
}'''
then:
notThrown(MultipleCompilationErrorsException)

when: 'type is subtype'
createClass '''
@MyAnnotation
class MyClass extends ArrayList<String> {
}'''
then:
notThrown(MultipleCompilationErrorsException)

when: 'on method'
createClass '''
class MyClass {
@MyAnnotation
List<String> doIt(String value, Closure<String> closure) {}
}'''
then:
notThrown(MultipleCompilationErrorsException)
}

def "Fails if Generics are not specified"() {

when: 'on field'
createClass '''
class MyClass {
@MyAnnotation
List myField
}'''
then:
thrown(MultipleCompilationErrorsException)

when: 'type is subtype'
createClass '''
@MyAnnotation
class MyClass extends ArrayList {
}'''
then:
thrown(MultipleCompilationErrorsException)

when: 'on method'
createClass '''
class MyClass {
@MyAnnotation
List doIt(String value, Closure closure) {}
}'''
then:
thrown(MultipleCompilationErrorsException)
}

def "Works if Generics are specified with bounds"() {
given:
createClass '''
import java.lang.annotation.ElementType
@Target([ElementType.METHOD, ElementType.TYPE, ElementType.FIELD])
@Retention(RetentionPolicy.RUNTIME)
@KlumCastValidated
@NeedsGenerics(allowBounds = true)
@interface MyAnnotationWithBounds {}
'''

when:
createClass '''
class MyClass {
@MyAnnotation
List<? extends CharSequence> myField
}'''
then:
thrown(MultipleCompilationErrorsException)

when:
createClass '''
class MyClass {
@MyAnnotationWithBounds
List<? extends CharSequence> myField
}'''
then:
notThrown(MultipleCompilationErrorsException)
}

}

0 comments on commit e5e12fc

Please sign in to comment.