This is an automated email from the ASF dual-hosted git repository.

paulk-asert 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 9a560a6494 GROOVY-12053: groovy-contracts loop @Invariants and 
@Decreases fail under @TypeChecked
9a560a6494 is described below

commit 9a560a649487555ca6d58c7953d2d5290be3fee8
Author: Paul King <[email protected]>
AuthorDate: Sun May 31 15:35:59 2026 +1000

    GROOVY-12053: groovy-contracts loop @Invariants and @Decreases fail under 
@TypeChecked
---
 .../groovy/contracts/ast/LoopContractSupport.java  | 58 ++++++++++++++++++++++
 .../ast/LoopInvariantASTTransformation.java        |  5 ++
 .../ast/LoopVariantASTTransformation.java          |  5 ++
 .../contracts/tests/inv/LoopDecreasesTests.groovy  | 38 ++++++++++++++
 .../contracts/tests/inv/LoopInvariantTests.groovy  | 57 +++++++++++++++++++++
 5 files changed, 163 insertions(+)

diff --git 
a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/LoopContractSupport.java
 
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/LoopContractSupport.java
new file mode 100644
index 0000000000..d246ab1daa
--- /dev/null
+++ 
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/LoopContractSupport.java
@@ -0,0 +1,58 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.contracts.ast;
+
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.classgen.VariableScopeVisitor;
+import org.codehaus.groovy.control.SourceUnit;
+
+/**
+ * Shared helper for the loop-level contract transforms ({@link 
LoopInvariantASTTransformation}
+ * and {@link LoopVariantASTTransformation}).
+ * <p>
+ * Both transforms lift expressions out of an {@code @Invariant}/{@code 
@Decreases} annotation
+ * closure and inline them into the loop body. Because those expressions 
originally lived inside
+ * an annotation member, the compiler's {@link VariableScopeVisitor} never 
descended into them, so
+ * their {@link org.codehaus.groovy.ast.expr.VariableExpression}s reference 
unresolved
+ * {@link org.codehaus.groovy.ast.DynamicVariable}s. Under dynamic Groovy this 
resolves at runtime,
+ * but {@code @TypeChecked}/{@code @CompileStatic} (which run later) then see 
such references as
+ * {@code java.lang.Object} and fail type checking.
+ * <p>
+ * Re-running variable scope analysis once the expressions are real loop-body 
statements links each
+ * reference to its enclosing declaration. This mirrors the compiler's own 
idiom of running a fresh
+ * {@link VariableScopeVisitor} per class (see {@code ResolveVisitor}).
+ */
+final class LoopContractSupport {
+
+    private LoopContractSupport() {
+    }
+
+    /**
+     * Re-resolves variable scopes for the classes of the given source so that 
variable references
+     * inlined out of a loop-contract annotation closure are bound to their 
enclosing declarations.
+     *
+     * @param source the current source unit
+     */
+    static void resolveVariableScopes(final SourceUnit source) {
+        if (source == null || source.getAST() == null) return;
+        for (ClassNode classNode : source.getAST().getClasses()) {
+            new VariableScopeVisitor(source).visitClass(classNode);
+        }
+    }
+}
diff --git 
a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/LoopInvariantASTTransformation.java
 
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/LoopInvariantASTTransformation.java
index d65ec2712e..5ae6b7f51a 100644
--- 
a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/LoopInvariantASTTransformation.java
+++ 
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/LoopInvariantASTTransformation.java
@@ -98,6 +98,11 @@ public class LoopInvariantASTTransformation implements 
ASTTransformation {
         wrapped.setSourcePosition(annotation);
 
         injectAtLoopBodyStart(loopStatement, wrapped);
+
+        // The invariant closure lived inside an annotation, so its variable 
references were never
+        // resolved; re-run scope analysis now that they are real loop-body 
statements so that
+        // @TypeChecked/@CompileStatic can see their declared types.
+        LoopContractSupport.resolveVariableScopes(source);
     }
 
     private static void injectAtLoopBodyStart(LoopingStatement loopStatement, 
Statement check) {
diff --git 
a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/LoopVariantASTTransformation.java
 
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/LoopVariantASTTransformation.java
index 3e899f8a44..802441ae88 100644
--- 
a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/LoopVariantASTTransformation.java
+++ 
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/LoopVariantASTTransformation.java
@@ -129,6 +129,11 @@ public class LoopVariantASTTransformation implements 
ASTTransformation {
 
         // Inject: save at start, check at end
         injectAtLoopBodyStartAndEnd(loopStatement, savePrev, block(saveCurr, 
decreaseCheck));
+
+        // The variant closure lived inside an annotation, so its variable 
references were never
+        // resolved; re-run scope analysis now that they are real loop-body 
statements so that
+        // @TypeChecked/@CompileStatic can see their declared types.
+        LoopContractSupport.resolveVariableScopes(source);
     }
 
     /**
diff --git 
a/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/inv/LoopDecreasesTests.groovy
 
b/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/inv/LoopDecreasesTests.groovy
index 8951a5216c..e68a83da83 100644
--- 
a/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/inv/LoopDecreasesTests.groovy
+++ 
b/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/inv/LoopDecreasesTests.groovy
@@ -168,4 +168,42 @@ class LoopDecreasesTests extends BaseTestClass {
             assert n == 0
         '''
     }
+
+    @Test
+    void decreasesUnderTypeChecked() {
+        assertScript '''
+            import groovy.contracts.Decreases
+            import groovy.transform.TypeChecked
+
+            @TypeChecked
+            def method() {
+                int n = 10
+                @Decreases({ n })
+                while (n > 0) {
+                    n--
+                }
+                assert n == 0
+            }
+            method()
+        '''
+    }
+
+    @Test
+    void decreasesUnderCompileStatic() {
+        assertScript '''
+            import groovy.contracts.Decreases
+            import groovy.transform.CompileStatic
+
+            @CompileStatic
+            def method() {
+                int lo = 0, hi = 10
+                @Decreases({ hi - lo })
+                while (lo < hi) {
+                    lo++
+                }
+                assert lo == 10
+            }
+            method()
+        '''
+    }
 }
diff --git 
a/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/inv/LoopInvariantTests.groovy
 
b/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/inv/LoopInvariantTests.groovy
index 9e13b9d60f..a207b884cc 100644
--- 
a/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/inv/LoopInvariantTests.groovy
+++ 
b/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/inv/LoopInvariantTests.groovy
@@ -272,5 +272,62 @@ class LoopInvariantTests extends BaseTestClass {
             }
         '''
     }
+
+    @Test
+    void invariantUnderTypeChecked() {
+        assertScript '''
+            import groovy.contracts.Invariant
+            import groovy.transform.TypeChecked
+
+            @TypeChecked
+            def method() {
+                int sum = 0
+                @Invariant({ sum >= 0 })
+                @Invariant({ sum <= 100 })
+                for (int i in 1..5) {
+                    sum += i
+                }
+                assert sum == 15
+            }
+            method()
+        '''
+    }
+
+    @Test
+    void invariantUnderCompileStatic() {
+        assertScript '''
+            import groovy.contracts.Invariant
+            import groovy.transform.CompileStatic
+
+            @CompileStatic
+            def method() {
+                int sum = 0
+                @Invariant({ sum >= 0 })
+                for (int i in 1..5) {
+                    sum += i
+                }
+                assert sum == 15
+            }
+            method()
+        '''
+    }
+
+    @Test
+    void invariantViolationUnderCompileStaticThrows() {
+        shouldFail AssertionError, '''
+            import groovy.contracts.Invariant
+            import groovy.transform.CompileStatic
+
+            @CompileStatic
+            def method() {
+                int n = 5
+                @Invariant({ n > 0 })
+                while (n >= 0) {
+                    n--
+                }
+            }
+            method()
+        '''
+    }
 }
 

Reply via email to