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() {