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 2d9c0b7292 GROOVY-11910: Add ModifiesChecker type checking extension 
to verify @Modifies frame conditions (treat @Pure the same as @Modifies({}))
2d9c0b7292 is described below

commit 2d9c0b7292e8a3fa9b0ff44c4c6c5652a71ad53f
Author: Paul King <[email protected]>
AuthorDate: Thu Apr 9 17:04:39 2026 +1000

    GROOVY-11910: Add ModifiesChecker type checking extension to verify 
@Modifies frame conditions (treat @Pure the same as @Modifies({}))
---
 .../groovy/typecheckers/ModifiesChecker.groovy     | 12 ++++---
 .../groovy/typecheckers/ModifiesCheckerTest.groovy | 39 +++++++++++++++++++++-
 2 files changed, 46 insertions(+), 5 deletions(-)

diff --git 
a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/ModifiesChecker.groovy
 
b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/ModifiesChecker.groovy
index 9e62b5f7c9..1189607c6f 100644
--- 
a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/ModifiesChecker.groovy
+++ 
b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/ModifiesChecker.groovy
@@ -97,12 +97,19 @@ class ModifiesChecker extends 
GroovyTypeCheckingExtensionSupport.TypeCheckingDSL
         afterVisitMethod { MethodNode mn ->
             // Key matches ModifiesASTTransformation.MODIFIES_FIELDS_KEY — no 
hard dependency on groovy-contracts
             Set<String> modifiesSet = 
mn.getNodeMetaData('groovy.contracts.modifiesFields') as Set<String>
-            if (modifiesSet == null) return // no @Modifies on this method — 
nothing to check
+            if (modifiesSet == null && hasPureAnno(mn)) {
+                modifiesSet = Collections.emptySet() // @Pure implies 
@Modifies({})
+            }
+            if (modifiesSet == null) return // no @Modifies or @Pure on this 
method — nothing to check
 
             mn.code?.visit(makeVisitor(modifiesSet, mn))
         }
     }
 
+    private static boolean hasPureAnno(MethodNode method) {
+        method.annotations?.any { it.classNode?.nameWithoutPackage in 
PURE_ANNOS } ?: false
+    }
+
     private CheckingVisitor makeVisitor(Set<String> modifiesSet, MethodNode 
methodNode) {
         Set<String> paramNames = methodNode.parameters*.name as Set<String>
 
@@ -234,9 +241,6 @@ class ModifiesChecker extends 
GroovyTypeCheckingExtensionSupport.TypeCheckingDSL
                 null
             }
 
-            private static boolean hasPureAnno(MethodNode method) {
-                method.annotations?.any { it.classNode?.nameWithoutPackage in 
PURE_ANNOS } ?: false
-            }
         }
     }
 }
diff --git 
a/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/ModifiesCheckerTest.groovy
 
b/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/ModifiesCheckerTest.groovy
index 749bd84ba8..b0d15ec837 100644
--- 
a/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/ModifiesCheckerTest.groovy
+++ 
b/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/ModifiesCheckerTest.groovy
@@ -248,7 +248,44 @@ final class ModifiesCheckerTest {
         '''
     }
 
-    // === No @Modifies — checker is silent ===
+    // === @Pure implies @Modifies({}) ===
+
+    @Test
+    void pure_method_with_no_field_writes_passes() {
+        assertScript shell, '''
+            import groovy.transform.Pure
+
+            class A {
+                int count = 0
+
+                @Pure
+                int currentCount() {
+                    return count
+                }
+            }
+            assert new A().currentCount() == 0
+        '''
+    }
+
+    @Test
+    void pure_method_with_field_write_fails() {
+        def err = shouldFail shell, '''
+            import groovy.transform.Pure
+
+            class A {
+                int count = 0
+
+                @Pure
+                int increment() {
+                    count++
+                    return count
+                }
+            }
+        '''
+        assert err.message.contains('@Modifies violation')
+    }
+
+    // === No @Modifies or @Pure — checker is silent ===
 
     @Test
     void no_modifies_annotation_no_checking() {

Reply via email to