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 42ce7e0961 GROOVY-12038: Graduate groovy-contracts from incubating to
stable
42ce7e0961 is described below
commit 42ce7e09611dd02d85d6d1e80e3c079a90100460
Author: Paul King <[email protected]>
AuthorDate: Sun May 24 20:28:42 2026 +1000
GROOVY-12038: Graduate groovy-contracts from incubating to stable
---
.../src/main/java/groovy/contracts/Contracted.java | 2 +
.../src/main/java/groovy/contracts/Ensures.java | 4 +-
.../java/groovy/contracts/EnsuresConditions.java | 10 ++-
.../src/main/java/groovy/contracts/Invariant.java | 4 +-
.../src/main/java/groovy/contracts/Invariants.java | 10 ++-
.../src/main/java/groovy/contracts/Requires.java | 4 +-
.../java/groovy/contracts/RequiresConditions.java | 10 ++-
.../main/java/groovy/contracts/package-info.java | 23 ++++-
.../src/spec/doc/contracts-userguide.adoc | 97 +++++++++++++++++++++-
.../src/spec/test/ContractsTest.groovy | 50 +++++++++++
10 files changed, 191 insertions(+), 23 deletions(-)
diff --git
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Contracted.java
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Contracted.java
index 06204f1ffc..5eda698b91 100644
---
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Contracted.java
+++
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Contracted.java
@@ -35,6 +35,8 @@ import java.lang.annotation.Target;
*
* import groovy.contracts.*
* </pre>
+ *
+ * @since 4.0.0
*/
@Documented
@Target({ElementType.PACKAGE, ElementType.TYPE})
diff --git
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Ensures.java
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Ensures.java
index 5ea65e790f..d79144ca73 100644
--- a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Ensures.java
+++ b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Ensures.java
@@ -21,7 +21,6 @@ package groovy.contracts;
import
org.apache.groovy.contracts.annotations.meta.AnnotationProcessorImplementation;
import org.apache.groovy.contracts.annotations.meta.Postcondition;
import org.apache.groovy.contracts.common.impl.EnsuresAnnotationProcessor;
-import org.apache.groovy.lang.annotation.Incubating;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
@@ -77,11 +76,12 @@ import java.lang.annotation.Target;
* }
* </pre>
* </p>
+ *
+ * @since 4.0.0
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
-@Incubating
@Postcondition
@Repeatable(EnsuresConditions.class)
@AnnotationProcessorImplementation(EnsuresAnnotationProcessor.class)
diff --git
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/EnsuresConditions.java
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/EnsuresConditions.java
index b89d8cebe2..a46c94f60b 100644
---
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/EnsuresConditions.java
+++
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/EnsuresConditions.java
@@ -18,8 +18,6 @@
*/
package groovy.contracts;
-import org.apache.groovy.lang.annotation.Incubating;
-
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -27,12 +25,16 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
- * Represents multiple postconditions.
+ * Container annotation for multiple {@link Ensures} postconditions on the
same target.
+ * Synthesized by the compiler from stacked {@code @Ensures} annotations via
+ * {@link java.lang.annotation.Repeatable}; not normally written by hand.
+ *
+ * @since 5.0.0
+ * @see Ensures
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
-@Incubating
public @interface EnsuresConditions {
/**
* Returns the repeated postcondition annotations declared on the target
element.
diff --git
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariant.java
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariant.java
index 96760e920f..c89bc4c813 100644
--- a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariant.java
+++ b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariant.java
@@ -23,7 +23,6 @@ import groovy.lang.annotation.ExtendedTarget;
import
org.apache.groovy.contracts.annotations.meta.AnnotationProcessorImplementation;
import org.apache.groovy.contracts.annotations.meta.ClassInvariant;
import
org.apache.groovy.contracts.common.impl.ClassInvariantAnnotationProcessor;
-import org.apache.groovy.lang.annotation.Incubating;
import org.codehaus.groovy.transform.GroovyASTTransformationClass;
import java.lang.annotation.Documented;
@@ -61,11 +60,12 @@ import java.lang.annotation.Target;
* Whenever a class has a parent which itself specifies a class-invariant,
that class-invariant expression is combined
* with the actual class's invariant (by using a logical AND).
* </p>
+ *
+ * @since 4.0.0
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
-@Incubating
@ClassInvariant
@Repeatable(Invariants.class)
@ExtendedTarget({ExtendedElementType.LOOP, ExtendedElementType.IMPORT})
diff --git
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariants.java
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariants.java
index eaa81f4a3f..0817e08932 100644
---
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariants.java
+++
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Invariants.java
@@ -18,8 +18,6 @@
*/
package groovy.contracts;
-import org.apache.groovy.lang.annotation.Incubating;
-
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -27,12 +25,16 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
- * Represents multiple invariants
+ * Container annotation for multiple {@link Invariant} declarations on the
same type.
+ * Synthesized by the compiler from stacked {@code @Invariant} annotations via
+ * {@link java.lang.annotation.Repeatable}; not normally written by hand.
+ *
+ * @since 5.0.0
+ * @see Invariant
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
-@Incubating
public @interface Invariants {
/**
* Returns the repeated invariant annotations declared on the annotated
type.
diff --git
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Requires.java
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Requires.java
index e1da332744..0cc9139dde 100644
--- a/subprojects/groovy-contracts/src/main/java/groovy/contracts/Requires.java
+++ b/subprojects/groovy-contracts/src/main/java/groovy/contracts/Requires.java
@@ -21,7 +21,6 @@ package groovy.contracts;
import
org.apache.groovy.contracts.annotations.meta.AnnotationProcessorImplementation;
import org.apache.groovy.contracts.annotations.meta.Precondition;
import org.apache.groovy.contracts.common.impl.RequiresAnnotationProcessor;
-import org.apache.groovy.lang.annotation.Incubating;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
@@ -54,11 +53,12 @@ import java.lang.annotation.Target;
* }
* </pre>
* </p>
+ *
+ * @since 4.0.0
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
-@Incubating
@Precondition
@AnnotationProcessorImplementation(RequiresAnnotationProcessor.class)
@Repeatable(RequiresConditions.class)
diff --git
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/RequiresConditions.java
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/RequiresConditions.java
index ac742430e2..fedd0e502c 100644
---
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/RequiresConditions.java
+++
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/RequiresConditions.java
@@ -18,8 +18,6 @@
*/
package groovy.contracts;
-import org.apache.groovy.lang.annotation.Incubating;
-
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -27,12 +25,16 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
- * Represents multiple preconditions.
+ * Container annotation for multiple {@link Requires} preconditions on the
same target.
+ * Synthesized by the compiler from stacked {@code @Requires} annotations via
+ * {@link java.lang.annotation.Repeatable}; not normally written by hand.
+ *
+ * @since 5.0.0
+ * @see Requires
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
-@Incubating
public @interface RequiresConditions {
/**
* Returns the repeated precondition annotations declared on the target
element.
diff --git
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/package-info.java
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/package-info.java
index b976ba4da3..b66fa2f882 100644
---
a/subprojects/groovy-contracts/src/main/java/groovy/contracts/package-info.java
+++
b/subprojects/groovy-contracts/src/main/java/groovy/contracts/package-info.java
@@ -21,10 +21,25 @@
* Design-by-contract (DbC) support with compile-time assertion generation.
*
* <p>
- * {@link groovy.contracts.Requires @Requires} (precondition),
- * {@link groovy.contracts.Ensures @Ensures} (postcondition),
- * {@link groovy.contracts.Invariant @Invariant} (class invariant).
- * Postconditions can access {@code result} and {@code old()} values.
+ * Stable annotations:
* </p>
+ * <ul>
+ * <li>{@link groovy.contracts.Requires @Requires} – method
precondition</li>
+ * <li>{@link groovy.contracts.Ensures @Ensures} – method
postcondition; closures
+ * may reference {@code result} and {@code old}</li>
+ * <li>{@link groovy.contracts.Invariant @Invariant} – class
invariant, also usable
+ * as a loop invariant on {@code for} / {@code while} / {@code do-while}
loops</li>
+ * <li>{@link groovy.contracts.Contracted @Contracted} – package- or
class-level
+ * opt-in marker that enables contract processing</li>
+ * </ul>
+ * <p>
+ * Incubating in 6.0.0 (semantics may evolve in a subsequent 6.x release):
+ * </p>
+ * <ul>
+ * <li>{@link groovy.contracts.Modifies @Modifies} – frame condition
declaring
+ * which fields and parameters a method may modify</li>
+ * <li>{@link groovy.contracts.Decreases @Decreases} – loop
termination measure
+ * (a strictly-decreasing, non-negative variant)</li>
+ * </ul>
*/
package groovy.contracts;
diff --git a/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc
b/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc
index dee4c8934e..1aa1100641 100644
--- a/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc
+++ b/subprojects/groovy-contracts/src/spec/doc/contracts-userguide.adoc
@@ -38,6 +38,37 @@ package.
include::../test/ContractsTest.groovy[tags=basic_example,indent=0]
----
+== Enabling contracts with @Contracted
+
+Contract processing is opt-in. By default, classes that simply mention the
contract annotations
+will have them processed; the `@Contracted` marker is used when you want to
make the opt-in
+explicit at the package level, or when you want to apply contract processing
to a type that
+doesn't itself carry any contract annotations (for example, an empty class
whose contract
+comes entirely from inherited interfaces).
+
+`@Contracted` may be placed on a package declaration (in a
`package-info.groovy` /
+`package-info.java`) to enable contract processing across every type in that
package:
+
+[source,groovy]
+----
+@Contracted
+package com.acme.banking
+
+import groovy.contracts.*
+----
+
+or on an individual type:
+
+[source,groovy]
+----
+@Contracted
+class Account { /* ... */ }
+----
+
+Once enabled, runtime assertion checking still honours the JVM's `-ea` / `-da`
flags;
+`@Contracted` controls whether the AST transforms generate assertion code at
compile time,
+not whether the resulting assertions execute at run time.
+
== More Features
Groovy contracts supports the following feature set:
@@ -45,12 +76,13 @@ 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
+* declaration of loop termination measures via @Decreases
* 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
* assertion injection in Plain Old Groovy Objects (POGOs)
* human-readable assertion messages, based on Groovy power asserts
-* enabling contracts at package- or class-level with @AssertionsEnabled
+* enabling contracts at package- or class-level with @Contracted
* enable or disable contract checking with Java's -ea and -da VM parameters
* annotation contracts: a way to reuse reappearing contract elements in a
project domain model
* detection of circular assertion method calls
@@ -141,6 +173,69 @@
include::../test/ContractsTest.groovy[tags=loop_invariant_multiple_example,inden
* Assignment operators and state-changing postfix/prefix operators are
not supported inside the closure.
+== Loop Termination with @Decreases
+
+Where `@Invariant` on a loop asserts a condition that must hold _during_
iteration,
+`@Decreases` asserts a condition that guarantees the loop will _terminate_: a
+_loop variant_, also known as a termination measure. The closure must yield a
value
+whose successive iteration values form a strictly decreasing, well-founded
sequence.
+Two value shapes are supported:
+
+* A single `Comparable` value — usually numeric — that strictly
decreases
+ on every iteration. If the value is also a `Number`, it must additionally
remain
+ non-negative.
+* A `List` of `Comparable` elements, compared lexicographically: the first
position
+ at which consecutive values differ must show a strict decrease, and earlier
+ positions must be equal. This is the natural way to express termination for
+ nested loops (e.g. `[i, j]`).
+
+If the variant fails to decrease (or a numeric scalar variant becomes
negative),
+a `LoopVariantViolation` (a subclass of `AssertionError`) is thrown.
+
+This is inspired by similar constructs in design-by-contract languages such as
+Dafny and Eiffel.
+
+NOTE: `@Decreases` is incubating in Groovy 6.0.0. The contract — in
particular
+the scalar non-negativity rule and the lexicographic shape for `List`
variants —
+may be relaxed or extended in a future release based on usage feedback.
+
+=== While loop
+
+[source,groovy]
+----
+include::../test/ContractsTest.groovy[tags=decreases_while_example,indent=0]
+----
+
+=== For loop
+
+[source,groovy]
+----
+include::../test/ContractsTest.groovy[tags=decreases_for_example,indent=0]
+----
+
+=== Lexicographic (tuple) variants
+
+For nested loops where no single counter strictly decreases on every iteration,
+return a `List` whose elements form a tuple compared lexicographically. In the
+example below the outer counter stays equal while the inner one decreases, then
+the outer one decreases and the inner one is allowed to reset:
+
+[source,groovy]
+----
+include::../test/ContractsTest.groovy[tags=decreases_lexicographic_example,indent=0]
+----
+
+=== Rules for @Decreases closures
+
+* The closure must return a `Comparable` value or a `List` of `Comparable`
elements.
+* The value is evaluated at the start and end of each iteration; the
end-of-iteration
+ value must be strictly less than the start-of-iteration value
(lexicographically,
+ for `List` variants).
+* For numeric scalar variants, the value must also remain `>= 0`.
+ This non-negativity rule does _not_ apply to non-`Number` `Comparable` values
+ (e.g. `String`) or to `List` variants.
+* The closure may reference any variables visible in the enclosing scope.
+
== Frame Conditions with @Modifies
The `@Modifies` annotation declares which fields or parameters a method is
allowed to modify.
diff --git a/subprojects/groovy-contracts/src/spec/test/ContractsTest.groovy
b/subprojects/groovy-contracts/src/spec/test/ContractsTest.groovy
index 18aa43eeb0..06343cc061 100644
--- a/subprojects/groovy-contracts/src/spec/test/ContractsTest.groovy
+++ b/subprojects/groovy-contracts/src/spec/test/ContractsTest.groovy
@@ -156,6 +156,56 @@ class ContractsTest extends GroovyTestCase {
'''
}
+ void testDecreasesWhile() {
+ assertScript '''
+ // tag::decreases_while_example[]
+ import groovy.contracts.Decreases
+
+ int n = 10
+ @Decreases({ n })
+ while (n > 0) {
+ n--
+ }
+ assert n == 0
+ // end::decreases_while_example[]
+ '''
+ }
+
+ void testDecreasesFor() {
+ assertScript '''
+ // tag::decreases_for_example[]
+ import groovy.contracts.Decreases
+
+ int remaining = 5
+ @Decreases({ remaining })
+ for (int i = 0; i < 5; i++) {
+ remaining--
+ }
+ assert remaining == 0
+ // end::decreases_for_example[]
+ '''
+ }
+
+ void testDecreasesLexicographic() {
+ assertScript '''
+ // tag::decreases_lexicographic_example[]
+ import groovy.contracts.Decreases
+
+ int outer = 2, inner = 3
+ @Decreases({ [outer, inner] })
+ while (outer > 0) {
+ if (inner > 0) {
+ inner--
+ } else {
+ outer--
+ inner = 3
+ }
+ }
+ assert outer == 0
+ // end::decreases_lexicographic_example[]
+ '''
+ }
+
void testJep445Script() {
runScript '''
// tag::jep445_example[]