You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/06/12 03:38:53 UTC
(groovy) branch master updated: GROOVY-12084: groovy-contracts: @Synchronized method with @Ensures/@Invariant but no @Requires throws ClassCastException (SynchronizedStatement → BlockStatement)
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 678e80e701 GROOVY-12084: groovy-contracts: @Synchronized method with @Ensures/@Invariant but no @Requires throws ClassCastException (SynchronizedStatement → BlockStatement)
678e80e701 is described below
commit 678e80e7014cb8f52e6e47272aa2fd098f6798d1
Author: Paul King <pa...@asert.com.au>
AuthorDate: Fri Jun 12 12:22:01 2026 +1000
GROOVY-12084: groovy-contracts: @Synchronized method with @Ensures/@Invariant but no @Requires throws ClassCastException (SynchronizedStatement → BlockStatement)
---
.../groovy/contracts/generation/BaseGenerator.java | 17 +++++
.../generation/PostconditionGenerator.java | 4 +
.../contracts/compability/SynchronizedTests.groovy | 86 ++++++++++++++++++++++
3 files changed, 107 insertions(+)
diff --git a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/generation/BaseGenerator.java b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/generation/BaseGenerator.java
index 2e4e471d80..4fa3c5567e 100644
--- a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/generation/BaseGenerator.java
+++ b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/generation/BaseGenerator.java
@@ -86,6 +86,23 @@ public abstract class BaseGenerator {
this.source = source;
}
+ /**
+ * Ensures the given method's body is a {@link BlockStatement} so that contract code generators can
+ * weave assertions into it. Earlier transforms (e.g. {@code @Synchronized}, which runs at
+ * CANONICALIZATION) may have replaced the body with a non-block statement such as a
+ * {@code SynchronizedStatement}; in that case the original statement is wrapped in a fresh block.
+ * A {@code null} body (e.g. an abstract method or a not-yet-populated synthetic body) is left
+ * untouched. Mirrors the body normalization the precondition path already performs (GROOVY-12066)
+ * so the post-condition and invariant generators see a consistent body shape (GROOVY-12084).
+ *
+ * @param method the method whose body should be normalized to a block
+ */
+ protected static void ensureBlockBody(final MethodNode method) {
+ if (method.getCode() != null && !(method.getCode() instanceof BlockStatement)) {
+ method.setCode(block(method.getCode()));
+ }
+ }
+
/**
* @param classNode the {@link org.codehaus.groovy.ast.ClassNode} used to look up the invariant closure field
* @return the field name of the invariant closure field of the given <tt>classNode</tt>
diff --git a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/generation/PostconditionGenerator.java b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/generation/PostconditionGenerator.java
index c4ed966c7d..d3e30d27cc 100644
--- a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/generation/PostconditionGenerator.java
+++ b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/generation/PostconditionGenerator.java
@@ -132,6 +132,10 @@ public class PostconditionGenerator extends BaseGenerator {
private void addPostcondition(MethodNode method, BlockStatement postconditionBlockStatement) {
if (Boolean.TRUE.equals(method.getNodeMetaData(METHOD_PROCESSED))) return;
+ // GROOVY-12084: normalize a non-block body (e.g. a @Synchronized method whose body was wrapped
+ // in a SynchronizedStatement at CANONICALIZATION) before weaving, so the cast below is safe;
+ // covers both the explicit and the inherited (default) postcondition paths that reach here
+ ensureBlockBody(method);
final BlockStatement block = (BlockStatement) method.getCode();
Expression contractsEnabled = localVarX(BaseVisitor.GCONTRACTS_ENABLED_VAR, ClassHelper.boolean_TYPE);
diff --git a/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/compability/SynchronizedTests.groovy b/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/compability/SynchronizedTests.groovy
index 6982cefd24..c3169f0895 100644
--- a/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/compability/SynchronizedTests.groovy
+++ b/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/compability/SynchronizedTests.groovy
@@ -41,4 +41,90 @@ class SynchronizedTests extends GroovyShellTestCase {
evaluate source
}
+
+ // GROOVY-12084: @Synchronized wraps the method body in a SynchronizedStatement
+ // (at CANONICALIZATION) before contracts run; an @Ensures with no @Requires used
+ // to throw ClassCastException because the postcondition generator assumed the body
+ // was still a BlockStatement. Also assert the postcondition is actually evaluated
+ // (not merely that compilation succeeds) by tripping a violation.
+ void test_Synchronized_with_Ensures_but_no_Requires() {
+
+ def source = """
+ import groovy.contracts.*
+ import org.apache.groovy.contracts.PostconditionViolation
+ import static groovy.test.GroovyAssert.shouldFail
+
+ class A {
+
+ @groovy.transform.Synchronized
+ @Ensures({ result >= 0 })
+ int m(int a) { return a }
+
+ }
+
+ def a = new A()
+ assert a.m(12) == 12
+ shouldFail(PostconditionViolation) { a.m(-1) }
+ """
+
+ evaluate source
+ }
+
+ // GROOVY-12084: same wrapping scenario combined with a class invariant; the call
+ // satisfies the postcondition but violates the invariant, proving the invariant is
+ // genuinely woven into the synchronized method
+ void test_Synchronized_with_Ensures_and_Invariant_but_no_Requires() {
+
+ def source = """
+ import groovy.contracts.*
+ import org.apache.groovy.contracts.ClassInvariantViolation
+ import static groovy.test.GroovyAssert.shouldFail
+
+ @Invariant({ count >= 0 })
+ class A {
+
+ int count = 0
+
+ @groovy.transform.Synchronized
+ @Ensures({ result == a })
+ int m(int a) { count = a; return a }
+
+ }
+
+ def a = new A()
+ assert a.m(12) == 12
+ shouldFail(ClassInvariantViolation) { a.m(-1) }
+ """
+
+ evaluate source
+ }
+
+ // GROOVY-12084: the inherited (default) postcondition path also reaches the cast;
+ // a @Synchronized override of a method whose superclass declares @Ensures used to
+ // throw the same ClassCastException
+ void test_Synchronized_override_inheriting_Ensures() {
+
+ def source = """
+ import groovy.contracts.*
+ import org.apache.groovy.contracts.PostconditionViolation
+ import static groovy.test.GroovyAssert.shouldFail
+
+ class A {
+ @Ensures({ result >= 0 })
+ int m(int a) { return a }
+ }
+
+ class B extends A {
+ @groovy.transform.Synchronized
+ @Override
+ int m(int a) { return a }
+ }
+
+ def b = new B()
+ assert b.m(12) == 12
+ shouldFail(PostconditionViolation) { b.m(-1) }
+ """
+
+ evaluate source
+ }
}