You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@groovy.apache.org by "Paul King (Jira)" <ji...@apache.org> on 2026/10/20 01:27:01 UTC

[jira] [Closed] (GROOVY-11909) Add a @Modifies annotation to groovy-contracts

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

Paul King closed GROOVY-11909.
------------------------------

> 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
>             Fix For: 6.0.0-alpha-1
>
>
> 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)