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()
+ '''
+ }
}