Skip to content

Add Reassignable trait - #8175

Open
steve-aom-elliott wants to merge 3 commits into
mainfrom
reassignable-trait
Open

Add Reassignable trait#8175
steve-aom-elliott wants to merge 3 commits into
mainfrom
reassignable-trait

Conversation

@steve-aom-elliott

@steve-aom-elliott steve-aom-elliott commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What

A new incubating Reassignable trait under org.openrewrite.java.trait that answers a syntactic question recipes tend to re-invent locally: could this expression be the target of a plain assignment target = ... without introducing a compile error?

Why

  • Recipes that mechanically rewrite x.setFoo(...) shapes into x = x.rebuild()....build() — or any other transformation that needs to reassign a receiver — currently either duplicate the walk-up-the-cursor-and-check-flags logic each time, or silently skip cases where the receiver is a field access. Concrete example: the Reassignable-shaped guard that landed in rewrite-jackson#153 (MigrateMapperSettersToBuilder#isReassignableReceiver).

Extracting the check as a trait gives future recipes a one-liner and gives us one place to sharpen the semantics.

Scope for v1

Deliberately narrow, matching the jackson recipe's semantics:

  • Matches a J.Identifier referring to a non-final local declared in an enclosing block, and a J.FieldAccess whose target is a bare J.Identifier and whose field is non-final.
  • Rejects method parameters (reassigning a parameter silently drops the caller's expected mutation), final locals and final fields, multi-hop qualified field access (a.b.c), array-element writes, parenthesized targets, and method-chain receivers.
  • Does not model effectively-final semantics, definite assignment, or any control-flow analysis.

Follow-up: migrate the jackson recipe to use the trait once this lands.

Test plan

  • ./gradlew :rewrite-java:test --tests 'org.openrewrite.java.trait.ReassignableTest' — 5 cases (non-final local ✓, final local ✗, method parameter ✗, non-final this.field ✓, final this.field ✗).
  • ./gradlew :rewrite-java:test --tests 'org.openrewrite.java.trait.*' — no regression in other trait tests.

A syntactic check that an expression could be the target of a plain
assignment without introducing a compile error. Handles two shapes:

- a J.Identifier referring to a non-final local declared in an
  enclosing block (method parameters are excluded because reassigning
  a parameter silently drops the caller's expected mutation).
- a J.FieldAccess whose target is a bare J.Identifier and whose field
  is non-final.

Array-element writes, parenthesized targets, method-chain receivers,
and multi-hop qualified field access are intentionally not matched.
Effectively-final semantics are not modelled.
@steve-aom-elliott steve-aom-elliott added enhancement New feature or request test provided Already replicated with a unit test, using JUnit pioneer's ExpectedToFail labels Jul 3, 2026
@steve-aom-elliott steve-aom-elliott moved this from In Progress to Ready to Review in OpenRewrite Jul 3, 2026
@steve-aom-elliott
steve-aom-elliott marked this pull request as ready for review July 3, 2026 21:13

@sambsnyd sambsnyd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you scan the codebase for places this could be used? I know there's the one in your recipe PR, but before we finalize the API it would be good to make sure this trait is applicable/handles variations

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request test provided Already replicated with a unit test, using JUnit pioneer's ExpectedToFail

Projects

Status: Ready to Review

Development

Successfully merging this pull request may close these issues.

2 participants