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 4a4986e385 GROOVY-12052: groovy-contracts @Ensures could support
static methods
4a4986e385 is described below
commit 4a4986e3854e9ec8c7be001a3b4066197cf6e832
Author: Paul King <[email protected]>
AuthorDate: Sun May 31 10:41:08 2026 +1000
GROOVY-12052: groovy-contracts @Ensures could support static methods
---
.../ast/visitor/AnnotationClosureVisitor.java | 7 +++++
.../contracts/generation/CandidateChecks.java | 1 -
.../generation/PostconditionGenerator.java | 15 +++++++---
.../other/ClosureExpressionValidationTests.groovy | 20 +++++++++++++
.../tests/post/SimplePostconditionTests.groovy | 33 ++++++++++++++++++++--
5 files changed, 68 insertions(+), 8 deletions(-)
diff --git
a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/visitor/AnnotationClosureVisitor.java
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/visitor/AnnotationClosureVisitor.java
index f1061ff271..6bff20d7b2 100644
---
a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/visitor/AnnotationClosureVisitor.java
+++
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/visitor/AnnotationClosureVisitor.java
@@ -484,6 +484,13 @@ public class AnnotationClosureVisitor extends BaseVisitor
implements ASTNodeMeta
@Override
public void visitVariableExpression(VariableExpression expression) {
+ // GROOVY-12052: 'old' snapshots instance state, which a static
method has none of
+ if (!secondPass && methodNode != null && methodNode.isStatic()
+ && "old".equals(expression.getName())
+ &&
AnnotationUtils.hasAnnotationOfType(annotationNode.getClassNode(),
POSTCONDITION_TYPE_NAME)) {
+ addError("[groovy-contracts] 'old' is not supported in
postconditions of static methods.", expression);
+ }
+
// in case of a FieldNode, checks whether the FieldNode can be
replaced with a Parameter
Variable accessedVariable =
getParameterCandidate(expression.getAccessedVariable());
if (accessedVariable instanceof FieldNode fieldNode) {
diff --git
a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/generation/CandidateChecks.java
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/generation/CandidateChecks.java
index 50ae38fca6..bba5ed0792 100644
---
a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/generation/CandidateChecks.java
+++
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/generation/CandidateChecks.java
@@ -131,7 +131,6 @@ public class CandidateChecks {
*/
public static boolean isPostconditionCandidate(final ClassNode type, final
MethodNode method) {
if (!isPreconditionCandidate(type, method)) return false;
- if (method.isStatic()) return false;
return true;
}
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 e520311bb2..1863f85fca 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
@@ -45,6 +45,7 @@ import static
org.codehaus.groovy.ast.tools.GeneralUtils.callThisX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.declS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.mapX;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveVoid;
/**
@@ -140,21 +141,27 @@ public class PostconditionGenerator extends BaseGenerator
{
localPostconditionBlockStatement.getStatements().add(0,
declS(result, returnStatement.getExpression()));
AssertStatementCreationUtility.injectResultVariableReturnStatementAndAssertionCallStatement(block,
method.getReturnType().redirect(), returnStatement,
localPostconditionBlockStatement);
}
- setOldVariablesIfEnabled(block, contractsEnabled);
+ setOldVariablesIfEnabled(block, contractsEnabled,
method.isStatic());
} else if (method instanceof ConstructorNode) {
block.addStatements(postconditionBlockStatement.getStatements());
} else {
- setOldVariablesIfEnabled(block, contractsEnabled);
+ setOldVariablesIfEnabled(block, contractsEnabled,
method.isStatic());
block.addStatements(postconditionBlockStatement.getStatements());
}
method.putNodeMetaData(METHOD_PROCESSED, true);
}
}
- private void setOldVariablesIfEnabled(BlockStatement block, Expression
contractsEnabled) {
- // Assign the return statement expression to a local variable: Map old
+ private void setOldVariablesIfEnabled(BlockStatement block, Expression
contractsEnabled, boolean isStatic) {
final Expression oldVariableExpression = localVarX("old", new
ClassNode(Map.class));
+ if (isStatic) {
+ // static methods have no instance state to snapshot; the
synthetic old-variables method is an
+ // instance method, so 'old' is simply an empty map (references to
it are rejected at compile time)
+ block.getStatements().add(0, declS(oldVariableExpression, mapX()));
+ return;
+ }
+ // Assign the return statement expression to a local variable: Map old
Statement oldVariableStatement = assignS(oldVariableExpression,
callThisX(OldVariableGenerationUtility.OLD_VARIABLES_METHOD));
block.getStatements().add(0, declS(oldVariableExpression,
ConstantExpression.NULL));
block.getStatements().add(1, ifS(boolX(contractsEnabled),
block(oldVariableStatement)));
diff --git
a/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/other/ClosureExpressionValidationTests.groovy
b/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/other/ClosureExpressionValidationTests.groovy
index 5a42686e86..163d182513 100644
---
a/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/other/ClosureExpressionValidationTests.groovy
+++
b/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/other/ClosureExpressionValidationTests.groovy
@@ -199,4 +199,24 @@ class ClosureExpressionValidationTests extends
GroovyShellTestCase {
assertTrue msg.contains("State changing postfix & prefix operators are
not supported.")
}
+
+ // GROOVY-12052
+ void testOldNotSupportedInStaticPostcondition() {
+
+ def msg = shouldFail CompilationFailedException, {
+ evaluate """
+ import groovy.contracts.*
+
+ class A {
+
+ @Ensures({ old.value == result })
+ static int op(int value) { value }
+ }
+
+ A.op(1)
+ """
+ }
+
+ assertTrue msg.contains("'old' is not supported in postconditions of
static methods")
+ }
}
diff --git
a/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/post/SimplePostconditionTests.groovy
b/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/post/SimplePostconditionTests.groovy
index b88c4638bb..8bac8f5539 100644
---
a/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/post/SimplePostconditionTests.groovy
+++
b/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/post/SimplePostconditionTests.groovy
@@ -166,8 +166,9 @@ class Account {
}
}
+ // GROOVY-12052
@Test
- void no_postcondition_on_static_methods() {
+ void postcondition_on_static_void_method() {
def source = """
import groovy.contracts.*
@@ -175,14 +176,40 @@ class Account {
class Account {
@Ensures({ amount != null })
- static def withdraw(def amount) {
+ static void withdraw(def amount) {
+ println amount
+ }
+}
+ """
+
+ def clazz = add_class_to_classpath(source)
+ clazz.withdraw(10)
+ shouldFail(PostconditionViolation) {
+ clazz.withdraw(null)
+ }
+ }
+
+ // GROOVY-12052
+ @Test
+ void postcondition_with_result_on_static_method() {
+
+ def source = """
+ import groovy.contracts.*
+
+class Account {
+
+ @Ensures({ result >= 0 })
+ static int balanceAfter(int amount) {
return amount
}
}
"""
def clazz = add_class_to_classpath(source)
- assert clazz.withdraw(null) == null
+ assert clazz.balanceAfter(10) == 10
+ shouldFail(PostconditionViolation) {
+ clazz.balanceAfter(-5)
+ }
}
@Test