[ 
https://issues.apache.org/jira/browse/GROOVY-11909?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul King updated GROOVY-11909:
-------------------------------
    Description: 
This is to add a @Modfies annotation to groovy-contracts.

 

Here is the proposed plan (with Claude assisting - and it seems to have a few 
things not right but the general idea is there and I'll try to point it in the 
right direction):
{quote}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
Unknown macro: \{      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
Unknown macro: \{      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(
Unknown macro: \{ [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
{quote}

  was:
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 - and it seems to have a few 
things not right but the general idea is there and I'll try to point it in the 
right direction):
{quote}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
{quote}


> 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
>            Priority: Major
>
> This is to add a @Modfies annotation to groovy-contracts.
>  
> Here is the proposed plan (with Claude assisting - and it seems to have a few 
> things not right but the general idea is there and I'll try to point it in 
> the right direction):
> {quote}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
> Unknown macro: \{      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
> Unknown macro: \{      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(
> Unknown macro: \{ [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
> {quote}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to