Skip to content

A record component's annotations keep the type they were written with - #8751

Open
MBoegers wants to merge 5 commits into
mainfrom
test/record-component-annotation-type
Open

MBoegers wants to merge 5 commits into
mainfrom
test/record-component-annotation-type

Conversation

@MBoegers

@MBoegers MBoegers commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Annotated record components are around now. Jackson DTOs, Spring request bodies, validation constraints. Since v8.69.0 the parser has been giving those annotations the wrong type, or no type at all.
Two ways to hit:

  1. recipe tests touching such a record fail with LST contains missing or invalid type information. lead to more TypeValidation.none() usage
  2. when a component carries two annotations, the first can quietly take the second's type

Solution

The parser now takes each annotation's type from wherever javac actually recorded it, instead of assuming the annotations written in the record header line up with the ones that reached the component. They are distributed by javac.

A record component's annotations go to whichever of the component, the field,
the accessor and the constructor parameter each one applies to (JLS 9.7.4).
`Symbol.RecordComponent#getAnnotationMirrors()` lists only the ones that reached
the component, while `#getOriginalAnnos()` lists everything written in the
header, and `extractRecordComponentAnnotations` walks the two together by index.

Where the lists diverge the attribution slides: an annotation applicable only to
the accessor never gets a type, and when one precedes a component-applicable
annotation it takes that annotation's type instead. `@JsonGetter`, targeting
`{ANNOTATION_TYPE, METHOD}`, is enough to reach both.

The four cases are disabled pending a fix; the component-applicable case passes
today and guards against regressing it.
A record component's annotations are propagated to whichever of the component,
the field, the accessor and the canonical constructor parameter each one applies
to (JLS 9.7.4), and javac attributes each one only where it landed. The trees in
`getOriginalAnnos()`, the ones written in the header, it leaves untyped.
`extractRecordComponentAnnotations` walked those together with
`getAnnotationMirrors()` by index, but the mirrors cover only the
component-applicable annotations, so the two line up only when every annotation
in the header applies to the component. Where they diverge an annotation loses
its type, or worse, takes the neighbouring one's.

Take each type from where javac left it instead: from the accessor's copy in
`RecordComponent#accessorMeth`, which sits at the same source position, and
failing that from the component's mirrors, matched by name. Repeated annotations
are attributed as their container, so its elements are candidates too. The
field's and the constructor parameter's copies were already merged over the
originals by position.

The accessor's copies are read for their type alone rather than merged into the
position table the way the field's and the parameter's are: they are javac's
desugared trees, spelling `@A("x")` as `@A(value = "x")` and carrying no end
positions.
`@OnMethod(value = "full_name")` in a record header only parses since #8724:
`getOriginalAnnos()` hands back copies that are absent from the end position
table, so `endPos(arg) < 0` holds for every annotation written there and a
written attribute name was taken for an elided one.
Type validation cannot see the failure this guards: an annotation that takes its
neighbour's type is well formed. `@OnMethod @OnComponentAndField` is the shape
where nothing is left untyped to give it away, since the field's attributed copy
supplies a type for the annotation the mirror was shifted off. Against the index
walk it parses to `OnMethod -> OnComponentAndField` with validation green.

`componentApplicableTypes` collected every candidate up front, including the
elements of a repeated annotation's container, which only a repeated annotation
ever matches. Match against the mirrors directly and walk into a container only
once nothing has matched, which drops the intermediate list.
@MBoegers MBoegers changed the title Tests: record component annotations lose or swap their type A record component's annotations keep the type they were written with Sep 3, 2026
@MBoegers
MBoegers marked this pull request as ready for review September 3, 2026 08:14
@MBoegers
MBoegers requested review from jkschneider and sambsnyd and removed request for jkschneider and sambsnyd September 9, 2026 16:06
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.

1 participant