[
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)