Add AttributeValue trait for non-literal annotation attribute values#8166
Add AttributeValue trait for non-literal annotation attribute values#8166MBoegers wants to merge 9 commits into
Conversation
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"]) |
There was a problem hiding this comment.
elements having a size of 1 is tripping me up a bit here; would it not be 2 for a & b?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Surprised to see this false; do we have any cases where we do match isEnumConstant?
There was a problem hiding this comment.
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
What's changed?
A new cursor-bearing
AttributeValuetrait inorg.openrewrite.java.traitthat 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 newAnnotated#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 editsgetClassValue()/isClassLiteral(fqn)resolve class literals;getReferencedField()/isEnumConstant(fqn, name)cover enum constants and constant references in qualified, fully-qualified, and statically imported spellingsgetConstantValue()resolves constant references and constant expressions (Constants.NAME,"a" + "b") to the compiler's fold recorded inJavaType.Annotation.ElementValueon the annotated declaration — including constants from binary dependenciesasLiteral()/asAnnotated()bridge to the existing traits; accessor naming mirrorsSingleElementValue's constant/reference dualityOptional.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 theOptional<Literal>accessors conflateLiteralnow matches empty array initializers —{}parses with a phantomJ.Emptyelement, so@Foo(x = {})was indistinguishable from an absent attributeEXPRESSION, no constant fold, enum constants classify asCONSTANT_REFERENCEsince neither type mapping setsFlag.Enum; KotlinA::class/A::class.javaclassify as class literals)What's your motivation?
Annotatedtrait cannot read non-literal attribute values (class literals, enum constants, constant references) #8160 — class literals, enum constants, and constant references were unreachable throughAnnotated, forcing recipes to hand-rollJ.Annotation#getArguments()walking (e.g.MinimumJreConditionsin rewrite-testing-frameworks). Motivating case: a Spring Boot 4.0 migration recipe appendingUserDetailsServiceAutoConfiguration.classtoexcludeattributes that are arrays of class literals.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 presentLiteralwith zero values, matching runtime reflection semantics (an annotation attribute is never "absent" at runtime;{}is an empty array). Per an org-wideFindMethodsrun 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 migrateSpringRequestMappingto 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@Deprecatedon the old accessors.