Skip to content

Add AttributeValue trait for non-literal annotation attribute values#8166

Open
MBoegers wants to merge 9 commits into
mainfrom
rewrite-8160
Open

Add AttributeValue trait for non-literal annotation attribute values#8166
MBoegers wants to merge 9 commits into
mainfrom
rewrite-8160

Conversation

@MBoegers

@MBoegers MBoegers commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What's changed?

A new cursor-bearing AttributeValue trait in org.openrewrite.java.trait that classifies every shape a JLS §9.7.1 annotation element value can take — literals, class literals, enum constants, constant references, nested annotations, and arrays — plus new Annotated#getAttributeValue(String) / #getDefaultAttributeValue(@Nullable String) accessors returning it.

  • getElements() iterates braced arrays and the brace-less single-element form (exclude = A.class) uniformly, one cursor-bearing value per element for surgical edits
  • getClassValue() / isClassLiteral(fqn) resolve class literals; getReferencedField() / isEnumConstant(fqn, name) cover enum constants and constant references in qualified, fully-qualified, and statically imported spellings
  • getConstantValue() resolves constant references and constant expressions (Constants.NAME, "a" + "b") to the compiler's fold recorded in JavaType.Annotation.ElementValue on the annotated declaration — including constants from binary dependencies
  • asLiteral() / asAnnotated() bridge to the existing traits; accessor naming mirrors SingleElementValue's constant/reference duality
  • Optional.empty() now means only "attribute not explicitly written"; a present value whose typed accessors return null means "present but not resolvable that way" — the two conditions the Optional<Literal> accessors conflate
  • Bug fix: Literal now matches empty array initializers — {} parses with a phantom J.Empty element, so @Foo(x = {}) was indistinguishable from an absent attribute
  • Groovy/Kotlin behavior is pinned by tests in both modules (list literals degrade to EXPRESSION, no constant fold, enum constants classify as CONSTANT_REFERENCE since neither type mapping sets Flag.Enum; Kotlin A::class / A::class.java classify as class literals)

What's your motivation?

Any additional context

The existing Optional<Literal> accessors are untouched and not yet deprecated; all current consumers keep their exact behavior. The one deliberate behavior change is the {} fix: an explicitly empty array attribute now yields a present Literal with zero values, matching runtime reflection semantics (an annotation attribute is never "absent" at runtime; {} is an empty array). Per an org-wide FindMethods run over the recipe artifacts, only two artifacts currently consume the affected accessors and may be affected: https://app.moderne.io/results/C7GrmeXMY — follow-up PRs will add the one-line guards there and migrate SpringRequestMapping to the new API as its first adoption.

Planned follow-ups (separate PRs): docs (traits.md), rewrite-recipe-starter, a migration recipe in rewrite-rewrite, and only then @Deprecated on the old accessors.

MBoegers added 7 commits July 2, 2026 11:34
An empty array initializer `{}` parses with a single phantom `J.Empty`
element, which `Literal.Matcher` rejected. As a result an explicitly
empty array attribute like `@Example(other = {})` was indistinguishable
from an absent attribute through `Annotated#getAttribute(..)`.

Skip `J.Empty` both when matching and when extracting values (the
latter would otherwise fail with a `ClassCastException` once `{}`
matches).

Relates to #8160
`Annotated#getAttribute(..)` returns `Optional<Literal>` and therefore
cannot surface class literals, enum constants, constant references,
nested annotations, or arrays of these — recipes had to hand-roll over
`J.Annotation#getArguments()`.

Introduce the cursor-bearing `AttributeValue` trait, classifying every
JLS 9.7.1 element-value shape syntax-first (only the enum-vs-constant
distinction needs type attribution), and return it from the new
`Annotated#getAttributeValue(String)` / `#getDefaultAttributeValue(..)`
accessors:

- `getElements()` iterates braced arrays and the brace-less
  single-element form uniformly, one cursor-bearing value per element
- `getClassValue()` / `isClassLiteral(fqn)` resolve class literals,
  `getReferencedField()` / `isEnumConstant(fqn, name)` cover enum
  constants and constant references in all spellings
- `asLiteral()` / `asAnnotated()` bridge to the existing traits
- `Optional.empty()` now means only "attribute not explicitly present";
  present-but-unresolvable values answer null from typed accessors

The `Optional<Literal>` accessors are untouched, so all existing
consumers keep their exact behavior, including relying on `empty()` as
a "not a plain literal" signal. Accessor naming mirrors the
constant-vs-reference duality of `JavaType.Annotation.ElementValue`.

Resolving constant references to the compiler's folded constant follows
in a subsequent commit.

Fixes #8160
`JavaType.Variable` carries no constant value, so a reference like
`Constants.NAME` cannot be resolved from the expression's own
attribution. javac, however, constant-folds annotation element values
(constant references, statically imported names, string concatenation)
into the `JavaType.Annotation.ElementValue`s it records on the
annotated declaration's symbol — the use-site `J.Annotation#getType()`
is a plain `JavaType.Class` and carries none of this.

`getConstantValue()` now walks from the value to the enclosing
annotation (tracking attribute name and array index), then to the
annotated variable/method/class declaration, matches the declaration's
`JavaType.Annotation` by fully qualified name, and reads the folded
`constantValue` — bailing to null on any ambiguity (e.g. repeated
annotations, which javac stores under their container type) rather
than ever returning another occurrence's value.

Covers constants from binary dependencies (folded by javac from the
class file's ConstantValue attribute) and annotations on fields,
methods, parameters, and classes. Groovy sources and reflection-mapped
types never populate element values; there the fold degrades to null
as documented.

Relates to #8160
The trait classifies syntax-first over J, so most shapes work across
languages, but two documented degradations need pinning so they do not
drift silently:

- Groovy/Kotlin list literals (G.ListLiteral / K.ListLiteral) are not
  J.NewArray and cannot be referenced from rewrite-java: they classify
  as Kind.EXPRESSION and getElements() does not normalize them
- Groovy never populates JavaType.Annotation element values, so
  getConstantValue() on constant references returns null

Also pins the parts that do work: Groovy implicit positional values
and synthetic named-attribute assignments, and Kotlin A::class
references resolving through J.MemberReference to the referenced type.

Relates to #8160
…tlin/Groovy limits

Review findings on the previous commits:

- The fold walk dispatched on the annotation's direct parent tree, but
  an annotation written after a modifier parents under J.Modifier or
  J.AnnotatedType (`private @example(name = CONST) String f;`), so its
  constant silently failed to resolve even though javac records the
  fold on the field symbol. Skip exactly these two wrapper shapes when
  walking to the declaration; deeper nestings (type arguments etc.)
  still bail to null.
- Enum constants on Kotlin and Groovy sources classify as
  CONSTANT_REFERENCE even when fully attributed: neither type mapping
  sets Flag.Enum on the referenced variable. Documented on the class
  and isEnumConstant javadoc and pinned in both module tests. (Setting
  the flag in those mappings is a possible follow-up that would upgrade
  classification without an API change.)
- getConstantValue()'s availability javadoc now names Kotlin alongside
  Groovy, with a no-fold pin in the Kotlin test.
- New pins: Kotlin implicit value, Extension::class.java (the dedicated
  getKind/getClassValue branches were previously untested), Groovy bare
  class reference (`type = String` degrades to CONSTANT_REFERENCE);
  fold of a positional value and of a brace-less constant on an
  array-typed attribute; asLiteral() presence for `{}` after the
  Literal fix, also noted in Literal's javadoc.

Relates to #8160
Drop narrating and provenance comments, condense javadoc to terse
contracts, and remove the no-arg isEnumConstant() — kind checks go
through getKind(); the explicit isEnumConstant(fqn, name) remains for
matching.

Relates to #8160
…ld bail-outs

- Positional `@Example({CONST, "b"})` and parenthesized `{("a"), (CONST)}`
  array elements: per-element kind, fold index, and getName()
- getValue(TypeReference) list coercion and failed-coercion null
- getConstantValue() is null for every non-constant kind, and the
  Matcher's custom mapper() is exercised
- Fold bails to null on enum-constant declarations and type parameters
  (declaration positions the walk deliberately does not support)
- Old getDefaultAttribute stops at a positional non-literal where the
  new accessor classifies it
- Test hygiene: `String[] other;` was not a legal @interface member,
  `int d = 0.0` did not compile; marker on elementsAreIdentityBoundToTheTree
  so the recipe run asserts a change

Relates to #8160
String[] tags()
}

/*~~(EXPRESSION:elements=1)~~>*/@Example(tags = ["a", "b"])

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.

elements having a size of 1 is tripping me up a bit here; would it not be 2 for a & b?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This test shows pins down that for groovy the trait does not extract theG.ListLiteral elements; it handles it as a list. therefore getElements("tags") = [ singletonList("a", "b") ] which has size 1.
The behavior differs for J.NewArray where we unpack the list. This was meant as a simplification for implementation, but it is an unpleasant asymetry.

E e()
}

/*~~(CONSTANT_REFERENCE:isEnum=false)~~>*/@Example(e = E.ONE)

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.

Surprised to see this false; do we have any cases where we do match isEnumConstant?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

groovy (and infact kotlin as well) thing.
Both TypeMappings do not set the Enum flag as Java does. That leads to a degradation to ConstantRef (static final) because fieldType.hasFlags(Flag.Enum).
To overcome KotlinTypeMapping and GroovyParserVisitor are to fix.

…pectedToFail

The previous degradation pins asserted the current divergent behavior,
which reads as if CONSTANT_REFERENCE for an enum constant were the
intended semantics on Groovy and Kotlin. Instead, assert the same
results the trait produces on javac-attributed Java sources and mark
the known gaps @ExpectedToFail, so each test flips to red the moment
the underlying parser/type-mapping gap is fixed:

- Kotlin: KotlinTypeMapping#variableType does not set Flag.Enum on enum
  entries (mapToFlagsBitmap only maps visibility/modality/static, while
  the enum CLASS does get the flag)
- Groovy: GroovyParserVisitor#visitPropertyExpression hard-codes
  fieldType=null on property-access references, so neither enum
  discrimination nor getReferencedField() can work; same root cause
  degrades idiomatic bare class references (type = String)
- Both: no JavaType.Annotation element values are built, so
  getConstantValue() cannot resolve the compiler's constant fold
- Both: list/collection literals are G.ListLiteral / K.ListLiteral,
  not J.NewArray, so getElements() cannot normalize them

Relates to #8160
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.

Annotated trait cannot read non-literal attribute values (class literals, enum constants, constant references)

2 participants