Rewrite trailing setters and field setters as mapper.rebuild() chains - #153
Merged
steve-aom-elliott merged 2 commits intoJul 3, 2026
Merged
Conversation
`MigrateMapperSettersToBuilder` previously left `mapper.rebuild()...build()` TODO comments on any setter it couldn't fold into a `Mapper.builder()...build()` chain, and silently skipped setters whose receiver was a field access (`this.mapper.setX(...)`). This change rewrites those setters as `receiver = receiver.rebuild().<builderName>(args).build();` when the receiver is safely reassignable: - a non-final local declared in an enclosing block (method parameters are excluded because reassignment silently drops the caller's expected mutation), or - a `J.FieldAccess` with an identifier target and a non-final field. A `doAfterVisit` post-pass coalesces consecutive per-receiver rebuild assignments into a single chained `.rebuild()....build()`, so a run of N setters becomes one assignment rather than N. Setters that remain not-rewritable (final field, parameter, receiver behind a method chain) still get the existing TODO comment.
sambsnyd
approved these changes
Jul 3, 2026
Comment on lines
+763
to
+781
| private static boolean isReassignableReceiver(Expression select, Cursor cursor) { | ||
| if (select instanceof J.Identifier) { | ||
| return isReassignableLocal((J.Identifier) select, cursor); | ||
| } | ||
| if (select instanceof J.FieldAccess) { | ||
| J.FieldAccess fa = (J.FieldAccess) select; | ||
| if (!(fa.getTarget() instanceof J.Identifier)) { | ||
| return false; | ||
| } | ||
| JavaType.Variable v = fa.getName().getFieldType(); | ||
| return v != null && !v.hasFlags(Flag.Final); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private static boolean isReassignableLocal(J.Identifier ident, Cursor cursor) { | ||
| JavaType.Variable fieldType = ident.getFieldType(); | ||
| if (fieldType != null && fieldType.hasFlags(Flag.Final)) { | ||
| return false; |
Member
There was a problem hiding this comment.
I wonder if we should have a Reassignable trait or something like it. There must be quite a few recipes which must ask a variation on this question.
Contributor
Author
There was a problem hiding this comment.
I can kick something off to try making one that would live in openrewrite/rewrite, as I feel that's likely a more appropriate place for it to live. Later on we could refactor this to use that trait instead?
Contributor
Author
There was a problem hiding this comment.
@sambsnyd You can see the initial pass of this here:
steve-aom-elliott
deleted the
migrate-mapper-setters-rebuild-and-field-access
branch
July 3, 2026 20:52
This was referenced Jul 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
MigrateMapperSettersToBuilderpreviously leftmapper.rebuild()...build()TODO comments on any setter it couldn't fold into aMapper.builder()...build()chain, and silently skipped setters whose receiver was a field access (this.mapper.setX(...)).This PR rewrites those setters as
receiver = receiver.rebuild().<builderName>(args).build();when the receiver is safely reassignable:J.FieldAccesswith an identifier target and a non-final fieldA
doAfterVisitpost-pass coalesces consecutive per-receiver rebuild assignments into a single chained.rebuild()....build(), so a run of N setters becomes one assignment rather than N.Before
The
enable(...)call was left with a TODO because it fell outside the contiguous run followingnew JsonMapper(). A field-scoped variant likethis.mapper.setX(...)produced no rewrite and no TODO at all.After
And, newly covered, the
this.mapper.setX(...)shape:How
visitMethodInvocationto acceptJ.FieldAccessselects with aJ.Identifiertarget, so field-shape setters are considered rather than silently dropped.rewriteAsRebuildAssignmentto build aJavaTemplateproducing one<lhs> = <lhs>.rebuild().<builderName>(args).build()assignment per setter, applied at the setter's own coordinate — invoked before the TODO fallback when the receiver is reassignable.coalesceRebuildAssignmentsas adoAfterVisitpost-pass. Purely tree surgery (no template): it walks each block, parses consecutive<lhs> = <lhs>.rebuild()....build();statements viatryParseRebuildAssignment, and if their LHSs areSemanticallyEqual, rewires the chain calls into one shared.rebuild()+.build().rebuild()tomapperStubso theJavaTemplateparser resolves it against the Jackson 2 classpath used for template stubs.Method parameters, final fields, and receivers behind a method chain still get the existing TODO comment.
Test plan
./gradlew test— all tests pass, including five new cases under@Nested class RewriteAsRebuild:trailingSetterAfterGapOnLocal— one straggler after a gap becomes arebuild()assignment.trailingSettersCoalesceIntoOneRebuildChain— multiple stragglers coalesce into one chain.thisFieldSetterCoalescesIntoRebuild—this.mapper.setX(...)rewrites and coalesces.qualifiedFieldSetterRewritten—holder.mapper.setX(...)rewrites.finalFieldStillGetsTodoComment— final field left with TODO (no rewrite).CommentFallback.mapperFromParameterstill leaves the TODO comment (parameters intentionally excluded).