This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new c67a68d7a8 GROOVY-11884: Close gaps in Groovy annotation validation
c67a68d7a8 is described below
commit c67a68d7a899279ec7c4a92a56e4a85c31bcfeaa
Author: Paul King <[email protected]>
AuthorDate: Sat Mar 28 09:35:57 2026 +1000
GROOVY-11884: Close gaps in Groovy annotation validation
---
src/main/java/groovy/lang/Grab.java | 4 ++
src/main/java/groovy/lang/GrabConfig.java | 4 ++
src/main/java/groovy/lang/GrabExclude.java | 4 ++
src/main/java/groovy/lang/GrabResolver.java | 4 ++
src/main/java/groovy/lang/Grapes.java | 4 ++
src/main/java/groovy/lang/Newify.java | 3 +
.../lang/annotation/ExtendedElementType.java} | 22 +++---
.../annotation/ExtendedTarget.java} | 27 +++----
src/main/java/groovy/transform/ASTTest.java | 3 +
src/main/java/groovy/transform/BaseScript.java | 5 +-
src/main/java/groovy/transform/Parallel.java | 3 +
.../org/codehaus/groovy/ast/AnnotationNode.java | 44 ++++++++++++
.../codehaus/groovy/classgen/ExtendedVerifier.java | 36 ++++++++++
src/test-resources/core/AnnotatedLoop_02x.groovy | 12 +---
.../groovy/gls/annotations/AnnotationTest.groovy | 83 ++++++++++++++++++++++
.../groovy/groovy/annotations/MyIntegerAnno.groovy | 4 ++
.../src/main/java/groovy/contracts/Invariant.java | 3 +
17 files changed, 228 insertions(+), 37 deletions(-)
diff --git a/src/main/java/groovy/lang/Grab.java
b/src/main/java/groovy/lang/Grab.java
index 4e4b330e24..d7800b8f7d 100644
--- a/src/main/java/groovy/lang/Grab.java
+++ b/src/main/java/groovy/lang/Grab.java
@@ -18,6 +18,9 @@
*/
package groovy.lang;
+import groovy.lang.annotation.ExtendedElementType;
+import groovy.lang.annotation.ExtendedTarget;
+
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -56,6 +59,7 @@ import java.lang.annotation.Target;
ElementType.METHOD,
ElementType.PARAMETER,
ElementType.TYPE})
+@ExtendedTarget(ExtendedElementType.IMPORT)
public @interface Grab {
/**
* The organisation or group, e.g.: "org.apache.ant". A non-empty value is
required unless value() is used.
diff --git a/src/main/java/groovy/lang/GrabConfig.java
b/src/main/java/groovy/lang/GrabConfig.java
index 015dd964a0..ec3ab58204 100644
--- a/src/main/java/groovy/lang/GrabConfig.java
+++ b/src/main/java/groovy/lang/GrabConfig.java
@@ -18,6 +18,9 @@
*/
package groovy.lang;
+import groovy.lang.annotation.ExtendedElementType;
+import groovy.lang.annotation.ExtendedTarget;
+
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -78,6 +81,7 @@ import java.lang.annotation.Target;
ElementType.METHOD,
ElementType.PARAMETER,
ElementType.TYPE})
+@ExtendedTarget(ExtendedElementType.IMPORT)
public @interface GrabConfig {
/**
* Set to true if you want to use the system classloader when loading the
grape.
diff --git a/src/main/java/groovy/lang/GrabExclude.java
b/src/main/java/groovy/lang/GrabExclude.java
index eb5a50d637..91d4364be1 100644
--- a/src/main/java/groovy/lang/GrabExclude.java
+++ b/src/main/java/groovy/lang/GrabExclude.java
@@ -18,6 +18,9 @@
*/
package groovy.lang;
+import groovy.lang.annotation.ExtendedElementType;
+import groovy.lang.annotation.ExtendedTarget;
+
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -41,6 +44,7 @@ import java.lang.annotation.Target;
ElementType.METHOD,
ElementType.PARAMETER,
ElementType.TYPE})
+@ExtendedTarget(ExtendedElementType.IMPORT)
public @interface GrabExclude {
/**
diff --git a/src/main/java/groovy/lang/GrabResolver.java
b/src/main/java/groovy/lang/GrabResolver.java
index de963074ea..1e979c0ba2 100644
--- a/src/main/java/groovy/lang/GrabResolver.java
+++ b/src/main/java/groovy/lang/GrabResolver.java
@@ -18,6 +18,9 @@
*/
package groovy.lang;
+import groovy.lang.annotation.ExtendedElementType;
+import groovy.lang.annotation.ExtendedTarget;
+
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -51,6 +54,7 @@ import java.lang.annotation.Target;
ElementType.METHOD,
ElementType.PARAMETER,
ElementType.TYPE})
+@ExtendedTarget(ExtendedElementType.IMPORT)
public @interface GrabResolver {
/**
* Allows a shorthand form which sets the name and root to this value.
diff --git a/src/main/java/groovy/lang/Grapes.java
b/src/main/java/groovy/lang/Grapes.java
index 0882e0bb9d..d13151c2eb 100644
--- a/src/main/java/groovy/lang/Grapes.java
+++ b/src/main/java/groovy/lang/Grapes.java
@@ -18,6 +18,9 @@
*/
package groovy.lang;
+import groovy.lang.annotation.ExtendedElementType;
+import groovy.lang.annotation.ExtendedTarget;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -61,6 +64,7 @@ import java.lang.annotation.RetentionPolicy;
* are grabbed. See {@code @GrabConfig} for further information.
*/
@Retention(RetentionPolicy.SOURCE)
+@ExtendedTarget(ExtendedElementType.IMPORT)
public @interface Grapes {
Grab[] value();
diff --git a/src/main/java/groovy/lang/Newify.java
b/src/main/java/groovy/lang/Newify.java
index 598d8a700b..7c482593a0 100644
--- a/src/main/java/groovy/lang/Newify.java
+++ b/src/main/java/groovy/lang/Newify.java
@@ -18,6 +18,8 @@
*/
package groovy.lang;
+import groovy.lang.annotation.ExtendedElementType;
+import groovy.lang.annotation.ExtendedTarget;
import org.codehaus.groovy.transform.GroovyASTTransformationClass;
import java.lang.annotation.Documented;
@@ -115,6 +117,7 @@ import java.lang.annotation.Target;
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE,
ElementType.FIELD, ElementType.LOCAL_VARIABLE})
+@ExtendedTarget(ExtendedElementType.IMPORT)
@GroovyASTTransformationClass("org.codehaus.groovy.transform.NewifyASTTransformation")
public @interface Newify {
/**
diff --git a/src/test/groovy/groovy/annotations/MyIntegerAnno.groovy
b/src/main/java/groovy/lang/annotation/ExtendedElementType.java
similarity index 68%
copy from src/test/groovy/groovy/annotations/MyIntegerAnno.groovy
copy to src/main/java/groovy/lang/annotation/ExtendedElementType.java
index db1a56a131..16019ae27a 100644
--- a/src/test/groovy/groovy/annotations/MyIntegerAnno.groovy
+++ b/src/main/java/groovy/lang/annotation/ExtendedElementType.java
@@ -16,18 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
-package groovy.annotations
-
-import java.lang.annotation.*
-
-import static java.lang.annotation.RetentionPolicy.*
-import org.codehaus.groovy.transform.GroovyASTTransformationClass
+package groovy.lang.annotation;
/**
- * Test annotation
+ * Groovy-specific element types that extend {@link
java.lang.annotation.ElementType}
+ * to cover Groovy language constructs not present in the JDK.
+ *
+ * @see ExtendedTarget
+ * @since 6.0.0
*/
-@Retention(RUNTIME)
-@GroovyASTTransformationClass("groovy.annotations.MyIntegerAnnoTraceASTTransformation")
-@interface MyIntegerAnno {
- int value()
+public enum ExtendedElementType {
+ /** Import statement. */
+ IMPORT,
+ /** Loop statement ({@code for}, {@code while}, {@code do-while}). */
+ LOOP
}
diff --git a/src/main/java/groovy/transform/Parallel.java
b/src/main/java/groovy/lang/annotation/ExtendedTarget.java
similarity index 59%
copy from src/main/java/groovy/transform/Parallel.java
copy to src/main/java/groovy/lang/annotation/ExtendedTarget.java
index 1fd7e61167..278f40c1bc 100644
--- a/src/main/java/groovy/transform/Parallel.java
+++ b/src/main/java/groovy/lang/annotation/ExtendedTarget.java
@@ -16,28 +16,23 @@
* specific language governing permissions and limitations
* under the License.
*/
-package groovy.transform;
+package groovy.lang.annotation;
-import org.apache.groovy.lang.annotation.Incubating;
-import org.codehaus.groovy.transform.GroovyASTTransformationClass;
-
-import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
/**
- * Runs each iteration of an annotated {@code for} loop on a separate thread.
- * <p>
- * This annotation is a lightweight transform that intentionally favors
simplicity
- * over production-grade parallel orchestration.
+ * Indicates Groovy-specific element types on which an annotation is allowed,
+ * complementing {@link java.lang.annotation.Target} for constructs that have
+ * no corresponding {@link java.lang.annotation.ElementType}.
*
+ * @see ExtendedElementType
* @since 6.0.0
- * @see org.codehaus.groovy.transform.ParallelASTTransformation
*/
-@Incubating
-@Documented
-@Retention(RetentionPolicy.SOURCE)
-@GroovyASTTransformationClass("org.codehaus.groovy.transform.ParallelASTTransformation")
-public @interface Parallel {
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.ANNOTATION_TYPE)
+public @interface ExtendedTarget {
+ ExtendedElementType[] value();
}
-
diff --git a/src/main/java/groovy/transform/ASTTest.java
b/src/main/java/groovy/transform/ASTTest.java
index ec384a3d59..2fbe616b58 100644
--- a/src/main/java/groovy/transform/ASTTest.java
+++ b/src/main/java/groovy/transform/ASTTest.java
@@ -19,6 +19,8 @@
package groovy.transform;
import groovy.lang.Closure;
+import groovy.lang.annotation.ExtendedElementType;
+import groovy.lang.annotation.ExtendedTarget;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.transform.GroovyASTTransformationClass;
@@ -57,6 +59,7 @@ import java.lang.annotation.Target;
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD,
ElementType.PARAMETER, ElementType.LOCAL_VARIABLE})
+@ExtendedTarget(ExtendedElementType.LOOP)
@GroovyASTTransformationClass("org.codehaus.groovy.transform.ASTTestTransformation")
public @interface ASTTest {
/**
diff --git a/src/main/java/groovy/transform/BaseScript.java
b/src/main/java/groovy/transform/BaseScript.java
index 33e3b3714a..8d2f7783ff 100644
--- a/src/main/java/groovy/transform/BaseScript.java
+++ b/src/main/java/groovy/transform/BaseScript.java
@@ -19,6 +19,8 @@
package groovy.transform;
import groovy.lang.Script;
+import groovy.lang.annotation.ExtendedElementType;
+import groovy.lang.annotation.ExtendedTarget;
import org.codehaus.groovy.transform.GroovyASTTransformationClass;
import java.lang.annotation.Documented;
@@ -144,7 +146,8 @@ import java.lang.annotation.Target;
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
-@Target({ElementType.LOCAL_VARIABLE, ElementType.PACKAGE, ElementType.TYPE /*,
ElementType.IMPORT*/})
+@Target({ElementType.LOCAL_VARIABLE, ElementType.PACKAGE, ElementType.TYPE})
+@ExtendedTarget(ExtendedElementType.IMPORT)
@GroovyASTTransformationClass("org.codehaus.groovy.transform.BaseScriptASTTransformation")
public @interface BaseScript {
Class value() default Script.class;
diff --git a/src/main/java/groovy/transform/Parallel.java
b/src/main/java/groovy/transform/Parallel.java
index 1fd7e61167..e27d0de8b4 100644
--- a/src/main/java/groovy/transform/Parallel.java
+++ b/src/main/java/groovy/transform/Parallel.java
@@ -18,6 +18,8 @@
*/
package groovy.transform;
+import groovy.lang.annotation.ExtendedElementType;
+import groovy.lang.annotation.ExtendedTarget;
import org.apache.groovy.lang.annotation.Incubating;
import org.codehaus.groovy.transform.GroovyASTTransformationClass;
@@ -37,6 +39,7 @@ import java.lang.annotation.RetentionPolicy;
@Incubating
@Documented
@Retention(RetentionPolicy.SOURCE)
+@ExtendedTarget(ExtendedElementType.LOOP)
@GroovyASTTransformationClass("org.codehaus.groovy.transform.ParallelASTTransformation")
public @interface Parallel {
}
diff --git a/src/main/java/org/codehaus/groovy/ast/AnnotationNode.java
b/src/main/java/org/codehaus/groovy/ast/AnnotationNode.java
index 2ca4b0aab0..ddcf41eb55 100644
--- a/src/main/java/org/codehaus/groovy/ast/AnnotationNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/AnnotationNode.java
@@ -24,6 +24,9 @@ import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
+import groovy.lang.annotation.ExtendedElementType;
+import groovy.lang.annotation.ExtendedTarget;
+
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -52,6 +55,8 @@ public class AnnotationNode extends ASTNode {
public static final int RECORD_COMPONENT_TARGET = 1 << 10;
/** Groovy-only target for statement-level annotations (e.g. on {@code
for}/{@code while} loops). */
public static final int STATEMENT_TARGET = 1 << 11;
+ /** Groovy-only target for annotations on import statements. */
+ public static final int IMPORT_TARGET = 1 << 12;
public static final int TYPE_TARGET = ANNOTATION_TARGET | 1;
// GROOVY-7151
private final ClassNode classNode;
@@ -159,9 +164,47 @@ public class AnnotationNode extends ASTNode {
return 0x4FF; // GROOVY-11838: JLS 9.6.4.1
});
+ // check @ExtendedTarget for Groovy-specific element types
+ allowedTargets |=
classNode.redirect().getNodeMetaData(ExtendedTarget.class, (k) -> {
+ for (AnnotationNode an : classNode.getAnnotations()) {
+ if
("groovy.lang.annotation.ExtendedTarget".equals(an.getClassNode().getName())) {
+ Expression member = an.getMember("value");
+ int bits = 0;
+ if (member instanceof ListExpression list) {
+ for (Expression e : list.getExpressions()) {
+ bits |= extendedElementTypeBits(e);
+ }
+ } else {
+ bits |= extendedElementTypeBits(member);
+ }
+ return bits;
+ }
+ }
+ return 0;
+ });
+
return (target & allowedTargets) == target;
}
+ private static int extendedElementTypeBits(final Expression e) {
+ if (e instanceof PropertyExpression item) {
+ return valueOfBits(item.getPropertyAsString());
+ }
+ return 0;
+ }
+
+ private static int valueOfBits(String name) {
+ if (name == null) return 0;
+ try {
+ return switch (ExtendedElementType.valueOf(name)) {
+ case IMPORT -> IMPORT_TARGET;
+ case LOOP -> STATEMENT_TARGET;
+ };
+ } catch (IllegalArgumentException ignore) {
+ return 0;
+ }
+ }
+
private RetentionPolicy getRetentionPolicy() {
if (!(classNode.isPrimaryClassNode() || classNode.isResolved()))
throw new IllegalStateException("cannot check retention at this
time");
@@ -260,6 +303,7 @@ public class AnnotationNode extends ASTNode {
case TYPE_USE_TARGET -> "TYPE_USE";
case RECORD_COMPONENT_TARGET -> "RECORD_COMPONENT";
case STATEMENT_TARGET -> "STATEMENT";
+ case IMPORT_TARGET -> "IMPORT";
default -> "unknown target";
};
}
diff --git a/src/main/java/org/codehaus/groovy/classgen/ExtendedVerifier.java
b/src/main/java/org/codehaus/groovy/classgen/ExtendedVerifier.java
index 7093788260..56485ab555 100644
--- a/src/main/java/org/codehaus/groovy/classgen/ExtendedVerifier.java
+++ b/src/main/java/org/codehaus/groovy/classgen/ExtendedVerifier.java
@@ -25,7 +25,9 @@ import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ConstructorNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.GenericsType;
+import org.codehaus.groovy.ast.ImportNode;
import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.PackageNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.PropertyNode;
@@ -50,16 +52,19 @@ import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import static java.util.stream.Collectors.toList;
import static org.codehaus.groovy.ast.AnnotationNode.ANNOTATION_TARGET;
import static org.codehaus.groovy.ast.AnnotationNode.CONSTRUCTOR_TARGET;
import static org.codehaus.groovy.ast.AnnotationNode.FIELD_TARGET;
+import static org.codehaus.groovy.ast.AnnotationNode.IMPORT_TARGET;
import static org.codehaus.groovy.ast.AnnotationNode.LOCAL_VARIABLE_TARGET;
import static org.codehaus.groovy.ast.AnnotationNode.METHOD_TARGET;
import static org.codehaus.groovy.ast.AnnotationNode.PACKAGE_TARGET;
import static org.codehaus.groovy.ast.AnnotationNode.PARAMETER_TARGET;
import static org.codehaus.groovy.ast.AnnotationNode.RECORD_COMPONENT_TARGET;
+import static org.codehaus.groovy.ast.AnnotationNode.STATEMENT_TARGET;
import static org.codehaus.groovy.ast.AnnotationNode.TYPE_PARAMETER_TARGET;
import static org.codehaus.groovy.ast.AnnotationNode.TYPE_TARGET;
import static org.codehaus.groovy.ast.AnnotationNode.TYPE_USE_TARGET;
@@ -86,6 +91,7 @@ public class ExtendedVerifier extends ClassCodeVisitorSupport
{
private ClassNode currentClass;
private final SourceUnit source;
private final Map<String, Boolean> repeatableCache = new HashMap<>();
+ private final Set<ModuleNode> visitedModules = new HashSet<>();
public ExtendedVerifier(final SourceUnit sourceUnit) {
this.source = sourceUnit;
@@ -105,6 +111,10 @@ public class ExtendedVerifier extends
ClassCodeVisitorSupport {
if (packageNode != null) {
visitAnnotations(packageNode, PACKAGE_TARGET);
}
+ ModuleNode module = node.getModule();
+ if (module != null && visitedModules.add(module)) {
+ visitImportAnnotations(module);
+ }
if (node.isAnnotationDefinition()) {
visitAnnotations(node, ANNOTATION_TARGET);
} else {
@@ -221,6 +231,32 @@ public class ExtendedVerifier extends
ClassCodeVisitorSupport {
}
}
+ @Override
+ protected void visitStatementAnnotations(final Statement statement) {
+ for (AnnotationNode annotation : statement.getStatementAnnotations()) {
+ ErrorCollector errorCollector = new
ErrorCollector(source.getConfiguration());
+ AnnotationVisitor visitor = new AnnotationVisitor(source,
errorCollector);
+ AnnotationNode visited = visitor.visit(annotation);
+ source.getErrorCollector().addCollectorContents(errorCollector);
+
+ if (!visited.isTargetAllowed(STATEMENT_TARGET)) {
+ addError("Annotation @" + visited.getClassNode().getName() + "
is not allowed on element "
+ + AnnotationNode.targetToName(STATEMENT_TARGET),
visited);
+ }
+ }
+ }
+
+ private void visitImportAnnotations(final ModuleNode module) {
+ List<ImportNode> allImports = new ArrayList<>();
+ allImports.addAll(module.getImports());
+ allImports.addAll(module.getStarImports());
+ allImports.addAll(module.getStaticImports().values());
+ allImports.addAll(module.getStaticStarImports().values());
+ for (ImportNode importNode : allImports) {
+ visitAnnotations(importNode, IMPORT_TARGET);
+ }
+ }
+
//--------------------------------------------------------------------------
private void visitTypeAnnotations(final ClassNode node) {
diff --git a/src/test-resources/core/AnnotatedLoop_02x.groovy
b/src/test-resources/core/AnnotatedLoop_02x.groovy
index 2f5c031b31..c5e3a62b81 100644
--- a/src/test-resources/core/AnnotatedLoop_02x.groovy
+++ b/src/test-resources/core/AnnotatedLoop_02x.groovy
@@ -77,11 +77,9 @@ assert x == 4
@ASTTest(phase=SEMANTIC_ANALYSIS, value={
assert node instanceof ForStatement
def annos = node.statementAnnotations
- assert annos.size() == 2
+ assert annos.size() == 1
assert annos[0].classNode.name == 'groovy.transform.ASTTest'
- assert annos[1].classNode.name == 'java.lang.SuppressWarnings'
})
-@SuppressWarnings('unused')
for (String s in ['a', 'b']) {
assert s.length() == 1
}
@@ -91,11 +89,9 @@ int y = 0
@ASTTest(phase=SEMANTIC_ANALYSIS, value={
assert node instanceof WhileStatement
def annos = node.statementAnnotations
- assert annos.size() == 2
+ assert annos.size() == 1
assert annos[0].classNode.name == 'groovy.transform.ASTTest'
- assert annos[1].classNode.name == 'java.lang.SuppressWarnings'
})
-@SuppressWarnings('unused')
while (y < 2) {
y++
}
@@ -106,11 +102,9 @@ int z = 0
@ASTTest(phase=SEMANTIC_ANALYSIS, value={
assert node instanceof DoWhileStatement
def annos = node.statementAnnotations
- assert annos.size() == 2
+ assert annos.size() == 1
assert annos[0].classNode.name == 'groovy.transform.ASTTest'
- assert annos[1].classNode.name == 'java.lang.SuppressWarnings'
})
-@SuppressWarnings('unused')
do {
z++
} while (z < 3)
diff --git a/src/test/groovy/gls/annotations/AnnotationTest.groovy
b/src/test/groovy/gls/annotations/AnnotationTest.groovy
index dd443012cc..242cff5923 100644
--- a/src/test/groovy/gls/annotations/AnnotationTest.groovy
+++ b/src/test/groovy/gls/annotations/AnnotationTest.groovy
@@ -1328,4 +1328,87 @@ final class AnnotationTest {
assert obj.m() == 0
'''
}
+
+ @Test
+ void testAnnotationNotAllowedOnImport() {
+ def err = shouldFail shell, '''
+ @Deprecated
+ import java.lang.String
+
+ assert true
+ '''
+ assert err.message.contains('Annotation @java.lang.Deprecated is not
allowed on element IMPORT')
+ }
+
+ @Test
+ void testAnnotationWithExtendedTargetAllowedOnImport() {
+ assertScript shell, '''
+ import groovy.lang.annotation.ExtendedElementType
+ import groovy.lang.annotation.ExtendedTarget
+
+ @Retention(SOURCE)
+ @Target([FIELD, METHOD])
+ @ExtendedTarget(ExtendedElementType.IMPORT)
+ @interface MyImportAnno {}
+
+ @MyImportAnno
+ import java.lang.String
+
+ assert true
+ '''
+ }
+
+ @Test
+ void testAnnotationWithExtendedTargetNotAllowedOnImport() {
+ def err = shouldFail shell, '''
+ import groovy.lang.annotation.ExtendedElementType
+ import groovy.lang.annotation.ExtendedTarget
+
+ @Retention(SOURCE)
+ @ExtendedTarget(ExtendedElementType.LOOP)
+ @interface LoopOnly {}
+
+ @LoopOnly
+ import java.lang.String
+
+ assert true
+ '''
+ assert err.message.contains('Annotation @LoopOnly is not allowed on
element IMPORT')
+ }
+
+ @Test
+ void testAnnotationWithExtendedTargetAllowedOnLoop() {
+ assertScript shell, '''
+ import groovy.lang.annotation.ExtendedElementType
+ import groovy.lang.annotation.ExtendedTarget
+
+ @Retention(SOURCE)
+ @ExtendedTarget(ExtendedElementType.LOOP)
+ @interface MyLoopAnno {}
+
+ @MyLoopAnno
+ for (int i = 0; i < 3; i++) {}
+
+ @MyLoopAnno
+ while (false) {}
+
+ assert true
+ '''
+ }
+
+ @Test
+ void testAnnotationWithExtendedTargetNotAllowedOnLoop() {
+ def err = shouldFail shell, '''
+ import groovy.lang.annotation.ExtendedElementType
+ import groovy.lang.annotation.ExtendedTarget
+
+ @Retention(SOURCE)
+ @ExtendedTarget(ExtendedElementType.IMPORT)
+ @interface ImportOnly {}
+
+ @ImportOnly
+ for (int i = 0; i < 3; i++) {}
+ '''
+ assert err.message.contains('Annotation @ImportOnly is not allowed on
element STATEMENT')
+ }
}
diff --git a/src/test/groovy/groovy/annotations/MyIntegerAnno.groovy
b/src/test/groovy/groovy/annotations/MyIntegerAnno.groovy
index db1a56a131..9bf9f00d48 100644
--- a/src/test/groovy/groovy/annotations/MyIntegerAnno.groovy
+++ b/src/test/groovy/groovy/annotations/MyIntegerAnno.groovy
@@ -18,6 +18,9 @@
*/
package groovy.annotations
+import groovy.lang.annotation.ExtendedElementType
+import groovy.lang.annotation.ExtendedTarget
+
import java.lang.annotation.*
import static java.lang.annotation.RetentionPolicy.*
@@ -27,6 +30,7 @@ import
org.codehaus.groovy.transform.GroovyASTTransformationClass
* Test annotation
*/
@Retention(RUNTIME)
+@ExtendedTarget(ExtendedElementType.IMPORT)
@GroovyASTTransformationClass("groovy.annotations.MyIntegerAnnoTraceASTTransformation")
@interface MyIntegerAnno {
int value()
diff --git
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariant.java
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariant.java
index 053b89a77d..55e8d905aa 100644
--- a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariant.java
+++ b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariant.java
@@ -18,6 +18,8 @@
*/
package groovy.contracts;
+import groovy.lang.annotation.ExtendedElementType;
+import groovy.lang.annotation.ExtendedTarget;
import
org.apache.groovy.contracts.annotations.meta.AnnotationProcessorImplementation;
import org.apache.groovy.contracts.annotations.meta.ClassInvariant;
import
org.apache.groovy.contracts.common.impl.ClassInvariantAnnotationProcessor;
@@ -64,6 +66,7 @@ import java.lang.annotation.Target;
@Incubating
@ClassInvariant
@Repeatable(Invariants.class)
+@ExtendedTarget(ExtendedElementType.LOOP)
@AnnotationProcessorImplementation(ClassInvariantAnnotationProcessor.class)
@GroovyASTTransformationClass("org.apache.groovy.contracts.ast.LoopInvariantASTTransformation")
public @interface Invariant {