Skip to content

fix(java): correctly parse named attribute assignment in annotations (#8723) - #8724

Open
SamBarker wants to merge 5 commits into
openrewrite:mainfrom
SamBarker:fix/annotated-recordComponents
Open

fix(java): correctly parse named attribute assignment in annotations (#8723)#8724
SamBarker wants to merge 5 commits into
openrewrite:mainfrom
SamBarker:fix/annotated-recordComponents

Conversation

@SamBarker

@SamBarker SamBarker commented Aug 31, 2026

Copy link
Copy Markdown

What's changed?

  • Fixes parsing of a record-component annotation that uses an explicit named
    attribute (e.g. @A(value = "x")), for the Java 17, 21, and 25 parsers.
  • Adds regression tests covering the named-attribute case, including one
    where the annotated component is followed by additional record
    components (to catch parser-cursor corruption, not just a crash).

What's your motivation?

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 end-position table; the other copies report
no end position at all, even when the attribute was genuinely named in
source. The parser was using 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 either way, it
misread real explicit assignments as shorthand, converted only the value,
and left its cursor stuck mid-attribute-name, corrupting the parsed source
range for anything that followed.

The fix checks the source text directly for the attribute name instead of
relying on the unreliable end-position lookup. Applied identically to the
three JDK-version parser modules that support records (17, 21, 25) — Java
8 and 11 don't need it, since records didn't exist as a standard feature
until JDK 16.

This started as a minimal reproducer for an issue encountered running
OpenRewrite on
https://github.com/kroxylicious/kroxylicious/blob/main/kroxylicious-api/src/main/java/io/kroxylicious/proxy/config/tls/TlsCredentialSupplierConfig.java

Anything in particular you'd like reviewers to focus on?

Whether checking the source text for the attribute name (rather than
relying on endPos()/the end-position table) is the right general
approach here, or if there's a more idiomatic way to detect explicit vs.
compiler-synthesized annotation arguments in this codebase.

Anyone you would like to review specifically?

Have you considered any alternatives or workarounds?

Tried comparing assign.lhs.getStartPosition() to assign.rhs.getStartPosition()
to distinguish explicit vs. synthesized assigns without touching source
text — this broke on other shorthand cases (e.g. @Retention(RetentionPolicy.RUNTIME),
which is also represented as a JCAssign internally with non-equal
positions), so it wasn't reliable enough. The source-text check is simpler.

Any additional context

Checklist

Signed-off-by: Sam Barker <sam@quadrocket.co.uk>
@renechoi

Copy link
Copy Markdown
Contributor

The compact constructor isn't required: Range [82, 69) also reproduces with no constructor and any @Target. Trigger: exactly one named argument; @A("type") and @A(a = 1, b = 2) are fine.

Record-component annotation trees carry no end position on the argument, so endPos(arg) < 0 (ReloadableJava21ParserVisitor:139) holds for a real name =, not just javac's synthesized value =. That branch converts only the RHS, so the cursor never passes implNameProperty = and :969 substrings [82, 69).

Deciding :139 from the source after ( rather than endPos fixes it, java-21 TCK green.

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 openrewrite#8723

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Sam Barker <sam@quadrocket.co.uk>
Same fix as 5901e22, 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 openrewrite#8723

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Sam Barker <sam@quadrocket.co.uk>
Same fix as 5901e22, 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 openrewrite#8723

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Sam Barker <sam@quadrocket.co.uk>
@SamBarker

Copy link
Copy Markdown
Author

Thanks @renechoi I've added fixes to the PR so the tests now pass locally 😀

@SamBarker SamBarker changed the title Failing test for #8723 fix(java): correctly parse named attribute assignment in annotations (#8723) Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

JavaParsingException when processing annotated record components.

2 participants