Skip to content

Commit 6ddae64

Browse files
authored
Add support for repeatable annotations for local extensions (#1118)
fixes #1030
1 parent 36a2430 commit 6ddae64

File tree

8 files changed

+427
-2
lines changed

8 files changed

+427
-2
lines changed

docs/extensions.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,14 @@ Your annotation can be applied to a specification, a feature method, a fixture m
678678
like helper methods or other places if the `@Target` is set accordingly, the annotation will be ignored and has no
679679
effect other than being visible in the source code.
680680

681+
Since Spock 2.0 your annotation can also be defined as `@Repeatable` and applied multiple times to the same target.
682+
Spock will handle this appropriately and call your extension once for each annotation. If you want a repeatable
683+
annotation that is compatible with Spock before 2.0 you need to make the container annotation an extension annotation
684+
itself and handle all cases accordingly, but you need to make sure to only handle the container annotation if Spock
685+
version is before 2.0 or your annotations might be handled twice. Be aware that the repeatable annotation can be
686+
attached to the target directly, inside the container annotation or even both if the user added the container
687+
annotation manually and also attached one annotation directly.
688+
681689
`IAnnotationDrivenExtension` has the following five methods, where in each you can prepare a specification with your
682690
extension magic, like attaching interceptors to various interception points as described in the chapter
683691
<<Interceptors>>:

docs/release_notes.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ include::include.adoc[]
55

66
- Add a way to register `ConfigurationObject` globally without the need for a Global Extension
77

8+
- Annotations for local extensions can now be defined as `@Repeatable` and applied multiple times to the same target.
9+
Spock will handle this appropriately and call the extension once for each annotation.
10+
11+
812
== 2.0-M3 (2020-06-11)
913

1014
=== Breaking Changes

spock-core/src/main/java/org/spockframework/compiler/AstNodeCache.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import org.codehaus.groovy.runtime.wrappers.PojoWrapper;
2020
import org.spockframework.runtime.*;
21+
import org.spockframework.runtime.extension.RepeatedExtensionAnnotations;
2122
import org.spockframework.runtime.model.*;
2223
import org.spockframework.util.Identifiers;
2324
import spock.lang.Specification;
@@ -68,6 +69,7 @@ public class AstNodeCache {
6869
ErrorCollector.getDeclaredMethods(org.spockframework.runtime.ErrorCollector.VALIDATE_COLLECTED_ERRORS).get(0);
6970

7071
// annotations and annotation elements
72+
public final ClassNode RepeatedExtensionAnnotations = ClassHelper.makeWithoutCaching(RepeatedExtensionAnnotations.class);
7173
public final ClassNode SpecMetadata = ClassHelper.makeWithoutCaching(SpecMetadata.class);
7274
public final ClassNode FieldMetadata = ClassHelper.makeWithoutCaching(FieldMetadata.class);
7375
public final ClassNode FeatureMetadata = ClassHelper.makeWithoutCaching(FeatureMetadata.class);

spock-core/src/main/java/org/spockframework/compiler/SpecAnnotator.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,24 @@
1717
package org.spockframework.compiler;
1818

1919
import org.spockframework.compiler.model.*;
20+
import org.spockframework.runtime.extension.ExtensionAnnotation;
21+
import org.spockframework.runtime.extension.RepeatedExtensionAnnotations;
2022
import org.spockframework.runtime.model.*;
2123

2224
import java.io.File;
25+
import java.lang.annotation.Repeatable;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.stream.Collectors;
29+
import java.util.stream.Stream;
2330

2431
import org.codehaus.groovy.ast.*;
2532
import org.codehaus.groovy.ast.expr.*;
2633

34+
import static java.util.stream.Collectors.*;
35+
import static org.spockframework.compiler.AstUtil.*;
36+
import static org.spockframework.util.ObjectUtil.asInstance;
37+
2738
/**
2839
* Puts all spec information required at runtime into annotations
2940
* attached to class members.
@@ -41,6 +52,7 @@ public SpecAnnotator(AstNodeCache nodeCache) {
4152
@Override
4253
public void visitSpec(Spec spec) throws Exception {
4354
addSpecMetadata(spec);
55+
addRepeatedExtensionAnnotations(spec.getAst());
4456
}
4557

4658
private void addSpecMetadata(Spec spec) {
@@ -55,6 +67,7 @@ private void addSpecMetadata(Spec spec) {
5567
@Override
5668
public void visitField(Field field) throws Exception {
5769
addFieldMetadata(field);
70+
addRepeatedExtensionAnnotations(field.getAst());
5871
}
5972

6073
private void addFieldMetadata(Field field) {
@@ -70,6 +83,103 @@ private void addFieldMetadata(Field field) {
7083
public void visitMethod(Method method) throws Exception {
7184
if (method instanceof FeatureMethod)
7285
addFeatureMetadata((FeatureMethod)method);
86+
if ((method instanceof FeatureMethod) || (method instanceof FixtureMethod))
87+
addRepeatedExtensionAnnotations(method.getAst());
88+
}
89+
90+
private void addRepeatedExtensionAnnotations(AnnotatedNode annotatedNode) {
91+
ListExpression repeatedExtensionAnnotations = annotatedNode
92+
// get all repeatable extension annotations flattened
93+
.getAnnotations()
94+
.stream()
95+
.flatMap(this::flattenRepeatableExtensionAnnotationContainer)
96+
.filter(this::isRepeatableExtensionAnnotation)
97+
// find the annotations that occur multiple times
98+
.collect(groupingBy(AnnotationNode::getClassNode))
99+
.entrySet()
100+
.stream()
101+
.filter(entry -> entry.getValue().size() > 1)
102+
// put them to a ListExpression
103+
.map(Map.Entry::getKey)
104+
.map(ClassExpression::new)
105+
.collect(collectingAndThen(Collectors.<Expression>toList(), ListExpression::new));
106+
107+
// if any were found, put them to a RepeatedExtensionAnnotations annotation as runtime hint
108+
if (repeatedExtensionAnnotations.getExpressions().size() != 0) {
109+
AnnotationNode ann = new AnnotationNode(nodeCache.RepeatedExtensionAnnotations);
110+
ann.setMember(RepeatedExtensionAnnotations.VALUE, repeatedExtensionAnnotations);
111+
annotatedNode.addAnnotation(ann);
112+
}
113+
}
114+
115+
private Stream<? extends AnnotationNode> flattenRepeatableExtensionAnnotationContainer(AnnotationNode container) {
116+
// supply the container itself, it could also be a valid extension annotation
117+
Stream<? extends AnnotationNode> result = Stream.of(container);
118+
119+
Expression value = container.getMember("value");
120+
if (value instanceof ListExpression) {
121+
List<Expression> valueExpressions = asInstance(value, ListExpression.class).getExpressions();
122+
switch (valueExpressions.size()) {
123+
case 0:
124+
// no value, nothing to do
125+
break;
126+
127+
case 1:
128+
result = handleSingleContainedAnnotation(container, result, valueExpressions.get(0));
129+
break;
130+
131+
default:
132+
result = handleMultipleContainedAnnotations(container, result, valueExpressions);
133+
break;
134+
}
135+
} else if (value instanceof AnnotationConstantExpression) {
136+
result = handleSingleContainedAnnotation(container, result, value);
137+
}
138+
139+
return result;
140+
}
141+
142+
private Stream<? extends AnnotationNode> handleSingleContainedAnnotation(AnnotationNode container,
143+
Stream<? extends AnnotationNode> result,
144+
Expression value) {
145+
if (notRepeatedAnnotation(value, container)) {
146+
return result;
147+
}
148+
149+
AnnotationNode annotationNode = asInstance(
150+
asInstance(value, AnnotationConstantExpression.class).getValue(),
151+
AnnotationNode.class);
152+
// supply annotation twice in case there is only the container annotation
153+
// with one contained annotation and no annotation besides it
154+
// in that case we also need the RepeatedExtensionAnnotations at runtime
155+
// to unwrap the container
156+
return Stream.concat(result, Stream.of(annotationNode, annotationNode));
157+
}
158+
159+
private Stream<? extends AnnotationNode> handleMultipleContainedAnnotations(AnnotationNode container,
160+
Stream<? extends AnnotationNode> result,
161+
List<Expression> valueExpressions) {
162+
if (notRepeatedAnnotation(valueExpressions.get(0), container)) {
163+
return result;
164+
}
165+
166+
return Stream.concat(result, valueExpressions
167+
.stream()
168+
.map(AnnotationConstantExpression.class::cast)
169+
.map(AnnotationConstantExpression::getValue)
170+
.map(AnnotationNode.class::cast));
171+
}
172+
173+
private boolean notRepeatedAnnotation(Expression clazz, AnnotationNode container) {
174+
AnnotationNode repeatableAnnotation = getAnnotation(clazz.getType(), Repeatable.class);
175+
return (repeatableAnnotation == null) ||
176+
!repeatableAnnotation.getMember("value").getType().equals(container.getClassNode());
177+
}
178+
179+
private boolean isRepeatableExtensionAnnotation(AnnotationNode annotation) {
180+
ClassNode annotationClass = annotation.getClassNode();
181+
return hasAnnotation(annotationClass, ExtensionAnnotation.class) &&
182+
hasAnnotation(annotationClass, Repeatable.class);
73183
}
74184

75185
private void addFeatureMetadata(FeatureMethod feature) {

spock-core/src/main/java/org/spockframework/runtime/ExtensionRunner.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,35 @@ private void doRunAnnotationDrivenExtensions(Iterable<MethodInfo> nodes) {
8181
}
8282
}
8383

84-
@SuppressWarnings("unchecked")
8584
private void doRunAnnotationDrivenExtensions(NodeInfo<?, ?> node) {
86-
for (Annotation ann : node.getAnnotations()) {
85+
RepeatedExtensionAnnotations repeatedExtensionAnnotations = node.getAnnotation(RepeatedExtensionAnnotations.class);
86+
87+
List<Annotation> annotations;
88+
if (repeatedExtensionAnnotations == null) {
89+
annotations = Arrays.asList(node.getAnnotations());
90+
} else {
91+
annotations = new ArrayList<>();
92+
List<Class<? extends Annotation>> repeatedAnnotations = Arrays.asList(repeatedExtensionAnnotations.value());
93+
94+
// add all direct annotations except those marked as repeated
95+
Arrays.stream(node.getAnnotations())
96+
.filter(annotation -> repeatedAnnotations
97+
.stream()
98+
.noneMatch(repeatedAnnotation -> repeatedAnnotation.isInstance(annotation)))
99+
.forEach(annotations::add);
100+
101+
// add all annotations marked as repeated
102+
for (Class<? extends Annotation> repeatedAnnotation : repeatedAnnotations) {
103+
annotations.addAll(Arrays.asList(node.getAnnotationsByType(repeatedAnnotation)));
104+
}
105+
}
106+
107+
doRunAnnotationDrivenExtensions(node, annotations);
108+
}
109+
110+
@SuppressWarnings("unchecked")
111+
private void doRunAnnotationDrivenExtensions(NodeInfo<?, ?> node, List<Annotation> annotations) {
112+
for (Annotation ann : annotations) {
87113
ExtensionAnnotation extAnn = ann.annotationType().getAnnotation(ExtensionAnnotation.class);
88114
if (extAnn == null) continue;
89115
IAnnotationDrivenExtension extension = getOrCreateExtension(extAnn.value());
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.spockframework.runtime.extension;
18+
19+
import org.spockframework.util.Beta;
20+
21+
import java.lang.annotation.*;
22+
import java.lang.reflect.AnnotatedElement;
23+
24+
/**
25+
* This annotation carries the information which repeatable annotations are actually repeated from the
26+
* AST transformation phase to the runtime phase. As repeated annotations are wrapped inside a container
27+
* annotation and even can be wrapped in the container but also be once applied directly,
28+
* you cannot simply use {@link AnnotatedElement#getAnnotations()} but also have to check each annotation
29+
* whether it is a container annotation for a repeatable extension annotation and then use its {@code value}
30+
* attribute. To save doing this effort at runtime using reflection, it is done during AST transformation,
31+
* and the result carried over to runtime phase using this annotation.
32+
*
33+
* <p>This annotation is just for internal usage and should not be applied manually.
34+
*
35+
* @since 2.0
36+
*/
37+
@Beta
38+
@Retention(RetentionPolicy.RUNTIME)
39+
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
40+
public @interface RepeatedExtensionAnnotations {
41+
String VALUE = "value";
42+
43+
Class<? extends Annotation>[] value();
44+
}

spock-core/src/main/java/org/spockframework/runtime/model/NodeInfo.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ public <T extends Annotation> T getAnnotation(Class<T> clazz) {
8282
return getReflection().getAnnotation(clazz);
8383
}
8484

85+
public <T extends Annotation> T[] getAnnotationsByType(Class<T> clazz) {
86+
return getReflection().getAnnotationsByType(clazz);
87+
}
88+
8589
public boolean isAnnotationPresent(Class<? extends Annotation> clazz) {
8690
return getReflection().isAnnotationPresent(clazz);
8791
}

0 commit comments

Comments
 (0)