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 3f18de2a1d GROOVY-11909: Add a @Modifies annotation to groovy-contracts
3f18de2a1d is described below

commit 3f18de2a1d29c5e9c55940a3cd1b2f2d37d4603c
Author: Paul King <[email protected]>
AuthorDate: Tue Apr 7 10:15:54 2026 +1000

    GROOVY-11909: Add a @Modifies annotation to groovy-contracts
---
 .../src/main/java/groovy/contracts/Modifies.java   |  72 +++++++
 .../java/groovy/contracts/ModifiesConditions.java  |  39 ++++
 .../contracts/ast/ModifiesASTTransformation.java   | 155 ++++++++++++++
 .../ModifiesEnsuresValidationTransformation.java   |  71 +++++++
 .../ast/visitor/AnnotationClosureVisitor.java      |  11 +
 .../src/spec/doc/contracts-userguide.adoc          |  95 ++++++++-
 .../contracts/tests/post/ModifiesTests.groovy      | 235 +++++++++++++++++++++
 7 files changed, 676 insertions(+), 2 deletions(-)

diff --git 
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Modifies.java 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Modifies.java
new file mode 100644
index 0000000000..694d0f8a84
--- /dev/null
+++ b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Modifies.java
@@ -0,0 +1,72 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.contracts;
+
+import org.apache.groovy.lang.annotation.Incubating;
+import org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Declares the <b>frame condition</b> for a method: the set of fields and 
parameters
+ * that the method is allowed to modify. All other state is implicitly 
declared unchanged.
+ * <p>
+ * The closure value lists the modifiable targets as field references ({@code 
this.fieldName})
+ * or parameter references ({@code paramName}). Multiple targets can be 
specified using a list:
+ * <pre>
+ *   &#064;Modifies({ this.items })
+ *   void addItem(Item item) { ... }
+ *
+ *   &#064;Modifies({ [this.items, this.count] })
+ *   void addAndCount(Item item) { ... }
+ * </pre>
+ * <p>
+ * Multiple {@code @Modifies} annotations can also be used (via {@link 
Repeatable}):
+ * <pre>
+ *   &#064;Modifies({ this.items })
+ *   &#064;Modifies({ this.count })
+ *   void addAndCount(Item item) { ... }
+ * </pre>
+ * <p>
+ * When both {@code @Modifies} and {@link Ensures} are present on a method,
+ * the {@code old} variable in the postcondition may only reference fields
+ * declared in {@code @Modifies}. A compile error is reported otherwise.
+ * <p>
+ * {@code @Modifies} does not generate runtime assertion code. It serves as
+ * a specification for humans and tools (including AI) to reason about method 
behavior.
+ *
+ * @since 6.0.0
+ * @see Ensures
+ * @see Requires
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
+@Incubating
+@Repeatable(ModifiesConditions.class)
+@GroovyASTTransformationClass({
+        "org.apache.groovy.contracts.ast.ModifiesASTTransformation",
+        
"org.apache.groovy.contracts.ast.ModifiesEnsuresValidationTransformation"
+})
+public @interface Modifies {
+    Class value();
+}
diff --git 
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/ModifiesConditions.java
 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/ModifiesConditions.java
new file mode 100644
index 0000000000..8cc4d86595
--- /dev/null
+++ 
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/ModifiesConditions.java
@@ -0,0 +1,39 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.contracts;
+
+import org.apache.groovy.lang.annotation.Incubating;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Container for multiple {@link Modifies} annotations on the same method.
+ *
+ * @since 6.0.0
+ * @see Modifies
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
+@Incubating
+public @interface ModifiesConditions {
+    Modifies[] value();
+}
diff --git 
a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/ModifiesASTTransformation.java
 
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/ModifiesASTTransformation.java
new file mode 100644
index 0000000000..45b1f1984c
--- /dev/null
+++ 
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/ModifiesASTTransformation.java
@@ -0,0 +1,155 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.contracts.ast;
+
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.ListExpression;
+import org.codehaus.groovy.ast.expr.PropertyExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.ExpressionStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.syntax.SyntaxException;
+import org.codehaus.groovy.transform.ASTTransformation;
+import org.codehaus.groovy.transform.GroovyASTTransformation;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Handles {@link groovy.contracts.Modifies} annotations placed on methods.
+ * Extracts the declared modification targets (fields and parameters) from
+ * the annotation closure and stores them as node metadata on the {@link 
MethodNode}.
+ * <p>
+ * The metadata key is {@value #MODIFIES_FIELDS_KEY} and the value is a
+ * {@code Set<String>} of field/parameter names. Downstream processors
+ * (such as {@code @Ensures} validation) can read this metadata.
+ *
+ * @since 6.0.0
+ * @see groovy.contracts.Modifies
+ */
+@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
+public class ModifiesASTTransformation implements ASTTransformation {
+
+    /** Node metadata key for the set of modifiable field/parameter names. */
+    public static final String MODIFIES_FIELDS_KEY = 
"groovy.contracts.modifiesFields";
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void visit(final ASTNode[] nodes, final SourceUnit source) {
+        if (nodes.length != 2) return;
+        if (!(nodes[0] instanceof AnnotationNode annotation)) return;
+        if (!(nodes[1] instanceof MethodNode methodNode)) return;
+
+        // For @Repeatable, each annotation triggers this transform separately,
+        // so merge with any existing metadata from prior invocations.
+        Set<String> modifiesSet = (Set<String>) 
methodNode.getNodeMetaData(MODIFIES_FIELDS_KEY);
+        if (modifiesSet == null) {
+            modifiesSet = new LinkedHashSet<>();
+        }
+
+        extractFromAnnotation(annotation, methodNode, modifiesSet, source);
+
+        if (!modifiesSet.isEmpty()) {
+            methodNode.putNodeMetaData(MODIFIES_FIELDS_KEY, modifiesSet);
+        }
+    }
+
+    private static void extractFromAnnotation(AnnotationNode annotation, 
MethodNode methodNode, Set<String> modifiesSet, SourceUnit source) {
+        Expression value = annotation.getMember("value");
+        if (!(value instanceof ClosureExpression closureExpr)) return;
+
+        Expression expr = extractExpression(closureExpr);
+        if (expr == null) {
+            source.addError(new SyntaxException(
+                    "@Modifies closure must contain a field reference, 
parameter reference, or list of references",
+                    annotation.getLineNumber(), annotation.getColumnNumber()));
+            return;
+        }
+
+        if (expr instanceof ListExpression listExpr) {
+            for (Expression element : listExpr.getExpressions()) {
+                addValidatedName(element, methodNode, modifiesSet, source);
+            }
+        } else {
+            addValidatedName(expr, methodNode, modifiesSet, source);
+        }
+    }
+
+    private static void addValidatedName(Expression expr, MethodNode 
methodNode, Set<String> modifiesSet, SourceUnit source) {
+        if (expr instanceof PropertyExpression propExpr) {
+            Expression objExpr = propExpr.getObjectExpression();
+            if (objExpr instanceof VariableExpression varExpr && 
"this".equals(varExpr.getName())) {
+                String fieldName = propExpr.getPropertyAsString();
+                ClassNode declaringClass = methodNode.getDeclaringClass();
+                if (declaringClass != null && 
declaringClass.getField(fieldName) == null) {
+                    source.addError(new SyntaxException(
+                            "@Modifies references field '" + fieldName + "' 
which does not exist in " + declaringClass.getName(),
+                            expr.getLineNumber(), expr.getColumnNumber()));
+                } else {
+                    modifiesSet.add(fieldName);
+                }
+                return;
+            }
+        }
+        if (expr instanceof VariableExpression varExpr) {
+            String name = varExpr.getName();
+            if (!"this".equals(name)) {
+                boolean isParam = false;
+                for (Parameter param : methodNode.getParameters()) {
+                    if (param.getName().equals(name)) {
+                        isParam = true;
+                        break;
+                    }
+                }
+                if (!isParam) {
+                    source.addError(new SyntaxException(
+                            "@Modifies references '" + name + "' which is not 
a parameter of " + methodNode.getName() + "()",
+                            expr.getLineNumber(), expr.getColumnNumber()));
+                } else {
+                    modifiesSet.add(name);
+                }
+                return;
+            }
+        }
+        source.addError(new SyntaxException(
+                "@Modifies elements must be field references (this.field) or 
parameter references",
+                expr.getLineNumber(), expr.getColumnNumber()));
+    }
+
+    private static Expression extractExpression(ClosureExpression 
closureExpression) {
+        BlockStatement block = (BlockStatement) closureExpression.getCode();
+        List<Statement> statements = block.getStatements();
+        if (statements.size() != 1) return null;
+        Statement stmt = statements.get(0);
+        if (stmt instanceof ExpressionStatement exprStmt) {
+            return exprStmt.getExpression();
+        }
+        return null;
+    }
+}
diff --git 
a/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/ModifiesEnsuresValidationTransformation.java
 
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/ModifiesEnsuresValidationTransformation.java
new file mode 100644
index 0000000000..1a457bb6e3
--- /dev/null
+++ 
b/subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/ModifiesEnsuresValidationTransformation.java
@@ -0,0 +1,71 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.contracts.ast;
+
+import org.apache.groovy.contracts.ast.visitor.AnnotationClosureVisitor;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.syntax.SyntaxException;
+import org.codehaus.groovy.transform.ASTTransformation;
+import org.codehaus.groovy.transform.GroovyASTTransformation;
+
+import java.util.Set;
+
+/**
+ * Validates that {@code @Ensures} postconditions on a method only reference
+ * fields via {@code old.xxx} that are declared as modifiable by {@code 
@Modifies}.
+ * <p>
+ * Runs at {@link CompilePhase#INSTRUCTION_SELECTION} after both:
+ * <ul>
+ *   <li>{@link ModifiesASTTransformation} has stored the modification set, 
and</li>
+ *   <li>{@link AnnotationClosureVisitor} has recorded the {@code old} 
references</li>
+ * </ul>
+ * Then simply compares the two metadata sets.
+ *
+ * @since 6.0.0
+ * @see groovy.contracts.Modifies
+ */
+@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
+public class ModifiesEnsuresValidationTransformation implements 
ASTTransformation {
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void visit(final ASTNode[] nodes, final SourceUnit source) {
+        if (nodes.length != 2) return;
+        if (!(nodes[0] instanceof AnnotationNode annotation)) return;
+        if (!(nodes[1] instanceof MethodNode methodNode)) return;
+
+        Set<String> modifiesSet = (Set<String>) 
methodNode.getNodeMetaData(ModifiesASTTransformation.MODIFIES_FIELDS_KEY);
+        if (modifiesSet == null || modifiesSet.isEmpty()) return;
+
+        Set<String> oldRefs = (Set<String>) 
methodNode.getNodeMetaData(AnnotationClosureVisitor.OLD_REFERENCES_KEY);
+        if (oldRefs == null || oldRefs.isEmpty()) return;
+
+        for (String ref : oldRefs) {
+            if (!modifiesSet.contains(ref)) {
+                source.addError(new SyntaxException(
+                        "@Ensures references old." + ref + " but @Modifies 
does not declare '" + ref + "' as modifiable",
+                        annotation.getLineNumber(), 
annotation.getColumnNumber()));
+            }
+        }
+    }
+}
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 024d4168d1..5f418364c2 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
@@ -459,6 +459,9 @@ public class AnnotationClosureVisitor extends BaseVisitor 
implements ASTNodeMeta
         }
     }
 
+    /** Node metadata key for the set of field names referenced via {@code 
old.xxx} in postconditions. */
+    public static final String OLD_REFERENCES_KEY = 
"groovy.contracts.oldReferences";
+
     private static class OldPropertyExpressionTransformer extends 
ClassCodeExpressionTransformer {
         private final MethodNode methodNode;
         private CastExpression currentCast;
@@ -472,6 +475,7 @@ public class AnnotationClosureVisitor extends BaseVisitor 
implements ASTNodeMeta
             return null;
         }
 
+        @SuppressWarnings("unchecked")
         @Override
         public Expression transform(Expression expr) {
             if (expr instanceof CastExpression) {
@@ -488,6 +492,13 @@ public class AnnotationClosureVisitor extends BaseVisitor 
implements ASTNodeMeta
                 if (objExpr instanceof VariableExpression varExpr) {
                     if ("old".equals(varExpr.getName())) {
                         String propName = propExpr.getPropertyAsString();
+                        // Record the old reference for @Modifies validation
+                        java.util.Set<String> oldRefs = 
(java.util.Set<String>) methodNode.getNodeMetaData(OLD_REFERENCES_KEY);
+                        if (oldRefs == null) {
+                            oldRefs = new java.util.LinkedHashSet<>();
+                            methodNode.putNodeMetaData(OLD_REFERENCES_KEY, 
oldRefs);
+                        }
+                        oldRefs.add(propName);
                         ClassNode declaringClass = 
methodNode.getDeclaringClass();
                         if (declaringClass != null && 
declaringClass.getField(propName) != null) {
                             CastExpression adjusted = new 
CastExpression(declaringClass.getField(propName).getType(), expr);
diff --git a/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc 
b/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc
index 2fe98b130c..e69431171e 100644
--- a/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc
+++ b/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc
@@ -22,8 +22,8 @@
 = Groovy Contracts – design by contract support for Groovy
 
 This module provides contract annotations that support the specification of 
class-invariants,
-pre- and post-conditions on Groovy classes and interfaces, and loop invariants 
on
-`for`, `while`, and `do-while` loops.
+pre- and post-conditions on Groovy classes and interfaces, loop invariants on
+`for`, `while`, and `do-while` loops, and frame conditions declaring which 
fields a method may modify.
 Special support is provided so that post-conditions may refer to the old value 
of variables
 or to the result value associated with calling a method.
 
@@ -44,6 +44,7 @@ Groovy contracts supports the following feature set:
 
 * definition of class invariants, pre- and post-conditions via @Invariant, 
@Requires and @Ensures
 * definition of loop invariants on `for`, `while`, and `do-while` loops via 
@Invariant
+* declaration of frame conditions (which fields a method may modify) via 
@Modifies
 * inheritance of class invariants, pre- and post-conditions of concrete 
predecessor classes
 * inheritance of class invariants, pre- and post-conditions in implemented 
interfaces
 * usage of old and result variable in post-condition assertions
@@ -140,3 +141,93 @@ 
include::../test/ContractsTest.groovy[tags=loop_invariant_multiple_example,inden
 * Assignment operators and state-changing postfix/prefix operators are
   not supported inside the closure.
 
+== Frame Conditions with @Modifies
+
+The `@Modifies` annotation declares which fields or parameters a method is 
allowed to modify.
+All other state is implicitly declared unchanged. This is known as a _frame 
condition_ in
+design-by-contract terminology, inspired by similar constructs in Dafny and 
JML.
+
+`@Modifies` does not generate runtime assertion code — it serves as a 
specification
+for humans and tools (including AI) to reason about what a method changes.
+
+=== Basic usage
+
+Use `this.fieldName` to declare that a field may be modified:
+
+[source,groovy]
+----
+@Modifies({ this.items })
+void addItem(String item) {
+    items.add(item)
+}
+----
+
+For multiple fields, use a list expression:
+
+[source,groovy]
+----
+@Modifies({ [this.items, this.count] })
+void addAndCount(String item) {
+    items.add(item)
+    count++
+}
+----
+
+Or use multiple `@Modifies` annotations (via `@Repeatable`):
+
+[source,groovy]
+----
+@Modifies({ this.items })
+@Modifies({ this.count })
+void addAndCount(String item) { ... }
+----
+
+=== Parameter references
+
+Method parameters can also be declared as modifiable:
+
+[source,groovy]
+----
+@Modifies({ arr })
+void fillArray(int[] arr) {
+    for (int i = 0; i < arr.length; i++) {
+        arr[i] = i
+    }
+}
+----
+
+=== Interaction with @Ensures
+
+When both `@Modifies` and `@Ensures` are present on a method, a compile-time 
check
+ensures that the `old` variable in the postcondition only references fields 
declared
+in `@Modifies`. Referencing a field via `old` that is not in the modification 
set
+produces a compile error:
+
+[source,groovy]
+----
+@Modifies({ this.items })
+@Ensures({ old -> old.items.size() < items.size() })  // OK: items is declared
+void addItem(String item) { ... }
+
+@Modifies({ this.items })
+@Ensures({ old -> old.count == count })  // ERROR: count not declared in 
@Modifies
+void addItem(String item) { ... }
+----
+
+=== Compile-time validation
+
+The `@Modifies` annotation validates at compile time that:
+
+* Field references (`this.fieldName`) refer to fields that exist on the class.
+* Parameter references refer to actual parameters of the method.
+* If `@Ensures` is also present, `old.fieldName` references only access fields
+  declared in `@Modifies`.
+
+=== Rules for @Modifies closures
+
+* The closure must contain a single field reference (`this.field`), parameter
+  reference (`param`), or a list of such references.
+* Field references use the `this.fieldName` syntax.
+* Parameter references use the bare parameter name.
+* The closure is not executed at runtime — it is only analyzed at compile time.
+
diff --git 
a/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/post/ModifiesTests.groovy
 
b/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/post/ModifiesTests.groovy
new file mode 100644
index 0000000000..5eed2df0db
--- /dev/null
+++ 
b/subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/post/ModifiesTests.groovy
@@ -0,0 +1,235 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.contracts.tests.post
+
+import org.apache.groovy.contracts.tests.basic.BaseTestClass
+import org.codehaus.groovy.control.MultipleCompilationErrorsException
+import org.junit.jupiter.api.Test
+
+class ModifiesTests extends BaseTestClass {
+
+    @Test
+    void modifies_alone_compiles_and_runs() {
+        def instance = create_instance_of('''
+            import groovy.contracts.*
+
+            class A {
+                List items = []
+
+                @Modifies({ this.items })
+                void addItem(String item) {
+                    items.add(item)
+                }
+            }
+        ''')
+        instance.addItem('hello')
+        assert instance.items == ['hello']
+    }
+
+    @Test
+    void modifies_with_list_syntax() {
+        def instance = create_instance_of('''
+            import groovy.contracts.*
+
+            class A {
+                List items = []
+                int count = 0
+
+                @Modifies({ [this.items, this.count] })
+                void addItem(String item) {
+                    items.add(item)
+                    count++
+                }
+            }
+        ''')
+        instance.addItem('hello')
+        assert instance.items == ['hello']
+        assert instance.count == 1
+    }
+
+    @Test
+    void modifies_with_parameter_reference() {
+        def instance = create_instance_of('''
+            import groovy.contracts.*
+
+            class A {
+                @Modifies({ arr })
+                void fillArray(int[] arr) {
+                    for (int i = 0; i < arr.length; i++) {
+                        arr[i] = i
+                    }
+                }
+            }
+        ''')
+        def arr = new int[3]
+        instance.fillArray(arr)
+        assert arr == [0, 1, 2] as int[]
+    }
+
+    @Test
+    void modifies_with_ensures_valid_old_reference() {
+        def instance = create_instance_of('''
+            import groovy.contracts.*
+
+            class A {
+                int count = 0
+
+                @Modifies({ this.count })
+                @Ensures({ old -> old.count < count })
+                void increment() {
+                    count++
+                }
+            }
+        ''')
+        instance.increment()
+        assert instance.count == 1
+    }
+
+    @Test
+    void modifies_with_ensures_valid_old_reference_list_syntax() {
+        def instance = create_instance_of('''
+            import groovy.contracts.*
+
+            class A {
+                List items = []
+                int count = 0
+
+                @Modifies({ [this.items, this.count] })
+                @Ensures({ old -> old.count < count })
+                void addItem(String item) {
+                    items.add(item)
+                    count++
+                }
+            }
+        ''')
+        instance.addItem('hello')
+        assert instance.count == 1
+    }
+
+    @Test
+    void modifies_with_ensures_invalid_old_reference_causes_compile_error() {
+        shouldFail MultipleCompilationErrorsException, {
+            add_class_to_classpath('''
+                import groovy.contracts.*
+
+                class A {
+                    List items = []
+                    int count = 0
+
+                    @Modifies({ this.items })
+                    @Ensures({ old -> old.count == count })
+                    void addItem(String item) {
+                        items.add(item)
+                    }
+                }
+            ''')
+        }
+    }
+
+    @Test
+    void ensures_without_modifies_no_error() {
+        def instance = create_instance_of('''
+            import groovy.contracts.*
+
+            class A {
+                int count = 0
+
+                @Ensures({ old -> old.count < count })
+                void increment() {
+                    count++
+                }
+            }
+        ''')
+        instance.increment()
+        assert instance.count == 1
+    }
+
+    @Test
+    void modifies_without_ensures_no_error() {
+        def instance = create_instance_of('''
+            import groovy.contracts.*
+
+            class A {
+                int count = 0
+
+                @Modifies({ this.count })
+                void increment() {
+                    count++
+                }
+            }
+        ''')
+        instance.increment()
+        assert instance.count == 1
+    }
+
+    @Test
+    void multiple_modifies_via_repeatable() {
+        def instance = create_instance_of('''
+            import groovy.contracts.*
+
+            class A {
+                List items = []
+                int count = 0
+
+                @Modifies({ this.items })
+                @Modifies({ this.count })
+                @Ensures({ old -> old.count < count })
+                void addItem(String item) {
+                    items.add(item)
+                    count++
+                }
+            }
+        ''')
+        instance.addItem('hello')
+        assert instance.count == 1
+    }
+
+    @Test
+    void modifies_with_nonexistent_field_causes_compile_error() {
+        shouldFail MultipleCompilationErrorsException, {
+            add_class_to_classpath('''
+                import groovy.contracts.*
+
+                class A {
+                    List items = []
+
+                    @Modifies({ this.nonExistent })
+                    void addItem(String item) {
+                        items.add(item)
+                    }
+                }
+            ''')
+        }
+    }
+
+    @Test
+    void modifies_with_nonexistent_param_causes_compile_error() {
+        shouldFail MultipleCompilationErrorsException, {
+            add_class_to_classpath('''
+                import groovy.contracts.*
+
+                class A {
+                    @Modifies({ noSuchParam })
+                    void addItem(String item) {
+                    }
+                }
+            ''')
+        }
+    }
+}

Reply via email to