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 {

Reply via email to