[
https://issues.apache.org/jira/browse/GROOVY-11909?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18071560#comment-18071560
]
ASF GitHub Bot commented on GROOVY-11909:
-----------------------------------------
paulk-asert opened a new pull request, #2445:
URL: https://github.com/apache/groovy/pull/2445
(no comment)
> 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 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}
--
This message was sent by Atlassian Jira
(v8.20.10#820010)