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)

Reply via email to