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
+    }
 }