From 5895b62285fe2346f01c7a9ca118363b3255b43d Mon Sep 17 00:00:00 2001 From: Sam Barker Date: Mon, 31 Aug 2026 14:15:58 +1200 Subject: [PATCH 1/6] Failing test for #8723 Signed-off-by: Sam Barker --- .../org/openrewrite/java/tree/RecordTest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/RecordTest.java b/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/RecordTest.java index 7f5764f6d3f..5d2b0ee69f5 100644 --- a/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/RecordTest.java +++ b/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/RecordTest.java @@ -124,6 +124,34 @@ public record JavaRecord(String name, @Deprecated int age) { ); } + @Issue("https://github.com/openrewrite/rewrite/issues/8723") + @Test + void namedAttributeAnnotationOnComponentWithCompactConstructor() { + rewriteRun( + java( + """ + import java.lang.annotation.ElementType; + import java.lang.annotation.Target; + + @Target({ElementType.PARAMETER, ElementType.RECORD_COMPONENT}) + @interface PluginImplConfig { + String implNameProperty(); + } + """ + ), + java( + """ + public record MyRecord( + String type, + @PluginImplConfig(implNameProperty = "type") Object config) { + public MyRecord { + } + } + """ + ) + ); + } + @Issue("https://github.com/openrewrite/rewrite/issues/6401") @Test void annotationsAndRecords() { From 5901e22235e6e5e7449462d24de6d730815e0877 Mon Sep 17 00:00:00 2001 From: Sam Barker Date: Tue, 1 Sep 2026 10:09:21 +1200 Subject: [PATCH 2/6] Fix named annotation attribute on record component (Java 21) Record-component annotations get attributed to multiple copies of the same annotation tree (field, accessor, constructor parameter). Only the original copy is registered in javac's position table; the others report no end position at all, even when the attribute name was genuinely written in source. The parser used that end-position lookup to decide whether an attribute was explicitly named (@A(value = "x")) or compiler shorthand (@A("x")). Since the lookup fails for these copies regardless of which case it actually is, it treated a real explicit assignment as shorthand, converted only the value, and left its cursor stuck mid-attribute-name. Check the source text directly for the attribute name instead of relying on the position lookup. Fixes #8723 Assisted-by: Claude Sonnet 5 Signed-off-by: Sam Barker --- .../ReloadableJava21ParserVisitor.java | 12 ++++-- .../org/openrewrite/java/tree/RecordTest.java | 38 ++++++++++++++----- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java b/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java index b9aa7704064..0fd633a1b21 100644 --- a/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java +++ b/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java @@ -135,11 +135,15 @@ public J visitAnnotation(AnnotationTree node, Space fmt) { List> expressions; if (node.getArguments().size() == 1) { ExpressionTree arg = node.getArguments().get(0); - if (arg instanceof JCAssign) { - if (endPos(arg) < 0) { - expressions = singletonList(convert(((JCAssign) arg).rhs, t -> sourceBefore(")"))); - } else { + if (arg instanceof JCAssign assign && assign.lhs instanceof JCIdent attributeName) { + int saveCursor = cursor; + whitespace(); + boolean namedInSource = source.startsWith(attributeName.name.toString(), cursor); + cursor = saveCursor; + if (namedInSource) { expressions = singletonList(convert(arg, t -> sourceBefore(")"))); + } else { + expressions = singletonList(convert(assign.rhs, t -> sourceBefore(")"))); } } else { expressions = singletonList(convert(arg, t -> sourceBefore(")"))); diff --git a/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/RecordTest.java b/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/RecordTest.java index 5d2b0ee69f5..fa4e728ba82 100644 --- a/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/RecordTest.java +++ b/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/RecordTest.java @@ -126,26 +126,46 @@ public record JavaRecord(String name, @Deprecated int age) { @Issue("https://github.com/openrewrite/rewrite/issues/8723") @Test - void namedAttributeAnnotationOnComponentWithCompactConstructor() { + void namedAttributeAnnotationOnComponent() { rewriteRun( java( """ import java.lang.annotation.ElementType; import java.lang.annotation.Target; - + @Target({ElementType.PARAMETER, ElementType.RECORD_COMPONENT}) - @interface PluginImplConfig { - String implNameProperty(); + @interface A { + String value(); } """ ), java( """ - public record MyRecord( - String type, - @PluginImplConfig(implNameProperty = "type") Object config) { - public MyRecord { - } + public record MyRecord(@A(value = "a") String name) { + } + """ + ) + ); + } + + @Issue("https://github.com/openrewrite/rewrite/issues/8723") + @Test + void namedAttributeAnnotationOnComponentFollowedByAdditionalSource() { + rewriteRun( + java( + """ + import java.lang.annotation.ElementType; + import java.lang.annotation.Target; + + @Target({ElementType.PARAMETER, ElementType.RECORD_COMPONENT}) + @interface A { + String value(); + } + """ + ), + java( + """ + public record MyRecord(@A(value = "a") String name, int age) { } """ ) From 777d7f30ac03ea0d0b0294719b5ea5e3432df7d6 Mon Sep 17 00:00:00 2001 From: Sam Barker Date: Tue, 1 Sep 2026 10:10:43 +1200 Subject: [PATCH 3/6] Fix named annotation attribute on record component (Java 17) Same fix as 5901e2223, applied to the Java 17 parser: check the source text directly for the attribute name instead of relying on an end-position lookup that's unset for record-component copies of the annotation tree. Fixes #8723 Assisted-by: Claude Sonnet 5 Signed-off-by: Sam Barker --- .../java/isolated/ReloadableJava17ParserVisitor.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java b/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java index b297f34ba9e..8e18ec9888f 100644 --- a/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java +++ b/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java @@ -133,11 +133,15 @@ public J visitAnnotation(AnnotationTree node, Space fmt) { List> expressions; if (node.getArguments().size() == 1) { ExpressionTree arg = node.getArguments().get(0); - if (arg instanceof JCAssign) { - if (endPos(arg) < 0) { - expressions = singletonList(convert(((JCAssign) arg).rhs, t -> sourceBefore(")"))); - } else { + if (arg instanceof JCAssign assign && assign.lhs instanceof JCIdent attributeName) { + int saveCursor = cursor; + whitespace(); + boolean namedInSource = source.startsWith(attributeName.name.toString(), cursor); + cursor = saveCursor; + if (namedInSource) { expressions = singletonList(convert(arg, t -> sourceBefore(")"))); + } else { + expressions = singletonList(convert(assign.rhs, t -> sourceBefore(")"))); } } else { expressions = singletonList(convert(arg, t -> sourceBefore(")"))); From f1491adff5fa97d145fbe07262a30c8b5d346417 Mon Sep 17 00:00:00 2001 From: Sam Barker Date: Tue, 1 Sep 2026 10:11:18 +1200 Subject: [PATCH 4/6] Fix named annotation attribute on record component (Java 25) Same fix as 5901e2223, applied to the Java 25 parser: check the source text directly for the attribute name instead of relying on an end-position lookup that's unset for record-component copies of the annotation tree. Fixes #8723 Assisted-by: Claude Sonnet 5 Signed-off-by: Sam Barker --- .../java/isolated/ReloadableJava25ParserVisitor.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25ParserVisitor.java b/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25ParserVisitor.java index 091f32a31ec..114415decc5 100644 --- a/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25ParserVisitor.java +++ b/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25ParserVisitor.java @@ -136,11 +136,15 @@ public J visitAnnotation(AnnotationTree node, Space fmt) { List> expressions; if (node.getArguments().size() == 1) { ExpressionTree arg = node.getArguments().get(0); - if (arg instanceof JCAssign) { - if (endPos(arg) < 0) { - expressions = singletonList(convert(((JCAssign) arg).rhs, t -> sourceBefore(")"))); - } else { + if (arg instanceof JCAssign assign && assign.lhs instanceof JCIdent attributeName) { + int saveCursor = cursor; + whitespace(); + boolean namedInSource = source.startsWith(attributeName.name.toString(), cursor); + cursor = saveCursor; + if (namedInSource) { expressions = singletonList(convert(arg, t -> sourceBefore(")"))); + } else { + expressions = singletonList(convert(assign.rhs, t -> sourceBefore(")"))); } } else { expressions = singletonList(convert(arg, t -> sourceBefore(")"))); From 9abc4191e25a83beb6345742a97de0ced352bcf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Merlin=20B=C3=B6gershausen?= Date: Wed, 2 Sep 2026 15:48:17 +0200 Subject: [PATCH 5/6] Tell an elided annotation attribute name from a written one by position javac's `Annotate#enterAnnotation` rewrites `@A(expr)` into `@A(value = expr)` in the tree it hands us, so a single-element annotation always arrives as a `JCAssign` whether or not the source wrote the name. The `endPosTable` used to answer which it was, but it cannot for record components: javac copies their annotations onto the generated field or constructor parameter, and the copies carry no end positions. The name was dropped whenever the annotation could not land on the field, so `@Target({RECORD_COMPONENT, PARAMETER})` reproduces it and adding `FIELD` hides it. Compare the attribute name's position with the value expression's instead. `Annotate` builds the synthetic name with `make.at(rhs.pos)`, so the two coincide only when the name was elided, and positions survive the copy. Probing the source text at the cursor was the other candidate and gets `@A(value)` wrong, where the argument is a constant that happens to be named after the attribute. Extend the record test with a component that spaces and comments its attribute, and cover the constant-named-`value` case next to the other elided-argument tests in `AnnotationTest`. --- .../isolated/ReloadableJava17ParserVisitor.java | 14 ++++++-------- .../isolated/ReloadableJava21ParserVisitor.java | 14 ++++++-------- .../isolated/ReloadableJava25ParserVisitor.java | 14 ++++++-------- .../openrewrite/java/tree/AnnotationTest.java | 17 +++++++++++++++++ .../org/openrewrite/java/tree/RecordTest.java | 6 ++++++ 5 files changed, 41 insertions(+), 24 deletions(-) diff --git a/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java b/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java index 8e18ec9888f..ebdf5a82497 100644 --- a/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java +++ b/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java @@ -133,15 +133,13 @@ public J visitAnnotation(AnnotationTree node, Space fmt) { List> expressions; if (node.getArguments().size() == 1) { ExpressionTree arg = node.getArguments().get(0); - if (arg instanceof JCAssign assign && assign.lhs instanceof JCIdent attributeName) { - int saveCursor = cursor; - whitespace(); - boolean namedInSource = source.startsWith(attributeName.name.toString(), cursor); - cursor = saveCursor; - if (namedInSource) { - expressions = singletonList(convert(arg, t -> sourceBefore(")"))); - } else { + if (arg instanceof JCAssign assign && assign.lhs instanceof JCIdent) { + // javac's `Annotate#enterAnnotation` builds an elided `value =` with `make.at(rhs.pos)` + boolean attributeNameElidedInSource = assign.lhs.pos == assign.rhs.pos; + if (attributeNameElidedInSource) { expressions = singletonList(convert(assign.rhs, t -> sourceBefore(")"))); + } else { + expressions = singletonList(convert(arg, t -> sourceBefore(")"))); } } else { expressions = singletonList(convert(arg, t -> sourceBefore(")"))); diff --git a/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java b/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java index 0fd633a1b21..9f546365805 100644 --- a/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java +++ b/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java @@ -135,15 +135,13 @@ public J visitAnnotation(AnnotationTree node, Space fmt) { List> expressions; if (node.getArguments().size() == 1) { ExpressionTree arg = node.getArguments().get(0); - if (arg instanceof JCAssign assign && assign.lhs instanceof JCIdent attributeName) { - int saveCursor = cursor; - whitespace(); - boolean namedInSource = source.startsWith(attributeName.name.toString(), cursor); - cursor = saveCursor; - if (namedInSource) { - expressions = singletonList(convert(arg, t -> sourceBefore(")"))); - } else { + if (arg instanceof JCAssign assign && assign.lhs instanceof JCIdent) { + // javac's `Annotate#enterAnnotation` builds an elided `value =` with `make.at(rhs.pos)` + boolean attributeNameElidedInSource = assign.lhs.pos == assign.rhs.pos; + if (attributeNameElidedInSource) { expressions = singletonList(convert(assign.rhs, t -> sourceBefore(")"))); + } else { + expressions = singletonList(convert(arg, t -> sourceBefore(")"))); } } else { expressions = singletonList(convert(arg, t -> sourceBefore(")"))); diff --git a/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25ParserVisitor.java b/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25ParserVisitor.java index 114415decc5..83ce41b871b 100644 --- a/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25ParserVisitor.java +++ b/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25ParserVisitor.java @@ -136,15 +136,13 @@ public J visitAnnotation(AnnotationTree node, Space fmt) { List> expressions; if (node.getArguments().size() == 1) { ExpressionTree arg = node.getArguments().get(0); - if (arg instanceof JCAssign assign && assign.lhs instanceof JCIdent attributeName) { - int saveCursor = cursor; - whitespace(); - boolean namedInSource = source.startsWith(attributeName.name.toString(), cursor); - cursor = saveCursor; - if (namedInSource) { - expressions = singletonList(convert(arg, t -> sourceBefore(")"))); - } else { + if (arg instanceof JCAssign assign && assign.lhs instanceof JCIdent) { + // javac's `Annotate#enterAnnotation` builds an elided `value =` with `make.at(rhs.pos)` + boolean attributeNameElidedInSource = assign.lhs.pos == assign.rhs.pos; + if (attributeNameElidedInSource) { expressions = singletonList(convert(assign.rhs, t -> sourceBefore(")"))); + } else { + expressions = singletonList(convert(arg, t -> sourceBefore(")"))); } } else { expressions = singletonList(convert(arg, t -> sourceBefore(")"))); diff --git a/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/AnnotationTest.java b/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/AnnotationTest.java index 8d7f2b490d8..da7790bc69e 100644 --- a/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/AnnotationTest.java +++ b/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/AnnotationTest.java @@ -58,6 +58,23 @@ public class A {} ); } + @Test + void annotationWithDefaultArgumentNamedLikeTheAttribute() { + rewriteRun( + java( + """ + class A { + static final String value = "ALL"; + + @SuppressWarnings(value) + void m() { + } + } + """ + ) + ); + } + @Test void annotationWithArgument() { rewriteRun( diff --git a/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/RecordTest.java b/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/RecordTest.java index fa4e728ba82..aaf9d5207fe 100644 --- a/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/RecordTest.java +++ b/rewrite-java-tck/src/main/java/org/openrewrite/java/tree/RecordTest.java @@ -144,6 +144,12 @@ void namedAttributeAnnotationOnComponent() { public record MyRecord(@A(value = "a") String name) { } """ + ), + java( + """ + public record MyRecordWithBlanks( @A( /* don't trip */ value = "a" ) String name) { + } + """ ) ); } From 269127a6af6374534e3b03bc036cacf06a8b5e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Merlin=20B=C3=B6gershausen?= Date: Wed, 2 Sep 2026 16:44:39 +0200 Subject: [PATCH 6/6] Inline the elided-name check into its condition --- .../java/isolated/ReloadableJava17ParserVisitor.java | 3 +-- .../java/isolated/ReloadableJava21ParserVisitor.java | 3 +-- .../java/isolated/ReloadableJava25ParserVisitor.java | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java b/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java index ebdf5a82497..6b76461427a 100644 --- a/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java +++ b/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17ParserVisitor.java @@ -135,8 +135,7 @@ public J visitAnnotation(AnnotationTree node, Space fmt) { ExpressionTree arg = node.getArguments().get(0); if (arg instanceof JCAssign assign && assign.lhs instanceof JCIdent) { // javac's `Annotate#enterAnnotation` builds an elided `value =` with `make.at(rhs.pos)` - boolean attributeNameElidedInSource = assign.lhs.pos == assign.rhs.pos; - if (attributeNameElidedInSource) { + if (assign.lhs.pos == assign.rhs.pos) { expressions = singletonList(convert(assign.rhs, t -> sourceBefore(")"))); } else { expressions = singletonList(convert(arg, t -> sourceBefore(")"))); diff --git a/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java b/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java index 9f546365805..a32a4b6fd26 100644 --- a/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java +++ b/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21ParserVisitor.java @@ -137,8 +137,7 @@ public J visitAnnotation(AnnotationTree node, Space fmt) { ExpressionTree arg = node.getArguments().get(0); if (arg instanceof JCAssign assign && assign.lhs instanceof JCIdent) { // javac's `Annotate#enterAnnotation` builds an elided `value =` with `make.at(rhs.pos)` - boolean attributeNameElidedInSource = assign.lhs.pos == assign.rhs.pos; - if (attributeNameElidedInSource) { + if (assign.lhs.pos == assign.rhs.pos) { expressions = singletonList(convert(assign.rhs, t -> sourceBefore(")"))); } else { expressions = singletonList(convert(arg, t -> sourceBefore(")"))); diff --git a/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25ParserVisitor.java b/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25ParserVisitor.java index 83ce41b871b..5b13c454a98 100644 --- a/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25ParserVisitor.java +++ b/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25ParserVisitor.java @@ -138,8 +138,7 @@ public J visitAnnotation(AnnotationTree node, Space fmt) { ExpressionTree arg = node.getArguments().get(0); if (arg instanceof JCAssign assign && assign.lhs instanceof JCIdent) { // javac's `Annotate#enterAnnotation` builds an elided `value =` with `make.at(rhs.pos)` - boolean attributeNameElidedInSource = assign.lhs.pos == assign.rhs.pos; - if (attributeNameElidedInSource) { + if (assign.lhs.pos == assign.rhs.pos) { expressions = singletonList(convert(assign.rhs, t -> sourceBefore(")"))); } else { expressions = singletonList(convert(arg, t -> sourceBefore(")")));