Paul King created GROOVY-11909:
----------------------------------
Summary: Add a @Modifies annotation to groovy-contracts
Key: GROOVY-11909
URL: https://issues.apache.org/jira/browse/GROOVY-11909
Project: Groovy
Issue Type: New Feature
Components: groovy-contracts
Reporter: Paul King
Assignee: Paul King
This is to explore adding a @Modfies annotation to groovy-contracts. If
successful, I'll keep the PR draft, until we have a discussion in the mailing
list.
Here is the proposed plan (with Claude assisting):
Plan: Add @Modifies annotation to groovy-contracts
Context
We're adding a @Modifies annotation to Groovy's Design by Contract framework
that declares which fields/parameters a method may modify. This is the first
step toward frame conditions — a feature that
helps both humans and AI reason about what a method changes (and crucially,
what it doesn't). Step one is a marker annotation with compile-time validation:
if both @Modifies and @Ensures exist on a
method, old.xxx references in @Ensures must only use fields declared in
@Modifies.
Approach
Follow the @Decreases pattern: a standalone local AST transformation at
SEMANTIC_ANALYSIS, NOT integrated into the existing @ContractElement /
AnnotationProcessor pipeline. This keeps the closure raw
(no boolean expression wrapping, no closure class generation, no runtime
assertions).
At SEMANTIC_ANALYSIS, both @Modifies and @Ensures closures are still raw
ClosureExpression nodes — the AnnotationClosureVisitor that transforms them
runs later at INSTRUCTION_SELECTION.
Files to create
1. subprojects/groovy-contracts/src/main/java/groovy/contracts/Modifies.java
Annotation definition, modeled on @Decreases (Decreases.java:56-63):
@Retention(RetentionPolicy.RUNTIME)
@Target(\{ElementType.CONSTRUCTOR, ElementType.METHOD})
@Incubating
@Repeatable(ModifiesConditions.class)
@GroovyASTTransformationClass("org.apache.groovy.contracts.ast.ModifiesASTTransformation")
public @interface Modifies {
Class value();
}
No @ContractElement, no @AnnotationProcessorImplementation — intentionally
excluded.
2.
subprojects/groovy-contracts/src/main/java/groovy/contracts/ModifiesConditions.java
Container for @Repeatable, following EnsuresConditions.java /
RequiresConditions.java:
@Retention(RetentionPolicy.RUNTIME)
@Target(\{ElementType.CONSTRUCTOR, ElementType.METHOD})
@Incubating
public @interface ModifiesConditions {
Modifies[] value();
}
3.
subprojects/groovy-contracts/src/main/java/org/apache/groovy/contracts/ast/ModifiesASTTransformation.java
Local AST transformation at SEMANTIC_ANALYSIS, modeled on
LoopVariantASTTransformation.java. Logic:
1. Receive AnnotationNode (@Modifies) and MethodNode
2. Extract modification targets from the closure AST:
- PropertyExpression with this receiver → field name (e.g., this.items →
"items")
- VariableExpression → parameter name (e.g., a)
- ListExpression → iterate elements, extract from each
3. Collect all @Modifies annotations on the method (handle @Repeatable), union
their targets
4. Find @Ensures annotations on the same method
5. Walk each @Ensures closure AST looking for PropertyExpression where object
is VariableExpression("old") → extract property name
6. Report compile error if any old.xxx reference has xxx not in the modifies
set
Key helper methods (reuse extractExpression pattern from
LoopVariantASTTransformation.java:189-198):
- extractModifiesNames(ClosureExpression) → Set<String>
- extractOldReferences(ClosureExpression) → Set<String> (walk AST with a
visitor)
- findEnsuresAnnotations(MethodNode) → List<AnnotationNode> (handle both
single and container)
4.
subprojects/groovy-contracts/src/test/groovy/org/apache/groovy/contracts/tests/post/ModifiesTests.groovy
Tests following the existing pattern (extend BaseTestClass, use
create_instance_of):
- @Modifies alone compiles and runs (marker only, no runtime effect)
- @Modifies(\{ this.items }) with @Ensures(\{ old -> old.items != items }) —
passes
- @Modifies(\{ [this.items, this.count] }) with list syntax — passes
- @Modifies(\{ this.items }) with @Ensures(\{ old -> old.count == count }) —
compile error (count not in modifies)
- @Modifies(\{ a }) with parameter reference — passes
- @Ensures without @Modifies — no error (validation only when both present)
- Multiple @Modifies via @Repeatable — union of modification sets
- @Modifies with no @Ensures — compiles fine
Files to reference (not modify)
- Decreases.java — annotation pattern to follow
- LoopVariantASTTransformation.java — transformation pattern to follow
(extractExpression, visit structure)
- Ensures.java / EnsuresConditions.java — annotation naming conventions
- OldVariablePostconditionTests.groovy — test patterns with old variable
- BaseTestClass.groovy — test infrastructure
Verification
1. ./gradlew :groovy-contracts:test — all existing tests pass (no regressions)
2. New ModifiesTests pass — both positive (compiles) and negative (compile
error for invalid old references)
3. Manual test: a class with @Modifies and valid @Ensures compiles and runs;
changing @Ensures to reference an unlisted field produces a clear compile error
--
This message was sent by Atlassian Jira
(v8.20.10#820010)