Skip to content

fix: Offer and resolve bean names in Kotlin @Qualifier values (#697) - #706

Merged
imuromtsev merged 3 commits into
mainfrom
imuromtsev/kotlin-qualifier-reference
Oct 15, 2026
Merged

imuromtsev merged 3 commits into
mainfrom
imuromtsev/kotlin-qualifier-reference

Conversation

@imuromtsev

Copy link
Copy Markdown
Collaborator

Closes #697

A Kotlin @Qualifier("…") value had no bean-name reference: no completion, no resolve, no rename — while the same code in Java
had all three.

Why

Three blockers in a row, each verified on public/main:

  • QualifierReferenceContributor.getReferencesForInjectionHost started with uExpression as? ULiteralExpression. For the
    injection host the provider is asked about, Kotlin UAST builds a KotlinStringTemplateUPolyadicExpression, not a
    ULiteralExpression, so the provider returned EMPTY_ARRAY immediately.
  • It then required valueLiteral.javaPsi, which is always null for Kotlin.
  • CompleteBeanReference.getPsiType() resolved the injection point through element.parentOfType<PsiAnnotation>()?.parent?.parent.
    A KtStringTemplateExpression has no PsiAnnotation ancestor, so even with a reference the completion would have been untyped.

What changed

  • The provider reads the value with evaluateString() and the range with ElementManipulators.getValueTextRange(host), the same
    shape CommonAnnotationReferenceProvider already uses. The annotation type now comes from uAnnotation.resolve() (one resolve
    instead of the previous two).
  • getPsiType() walks UAST instead: annotation → owning UVariable → its type. That covers a field, a Kotlin property, and a
    constructor, setter or function parameter, in both languages.
  • isPlainStringLiteral() keeps interpolated templates out: a ULiteralExpression, or a UPolyadicExpression whose operands are
    all literals, is a reference; "${PREFIX}a" is not, even though evaluateString() would happily fold it.

Two consequences worth naming:

  • Java also changes, intentionally: the reference range used to be TextRange(0, decodedLength) shifted to the value start, which
    cut off the tail of an escaped value. It is now the host's whole value range. There is a Java test for it.
  • SpringCoreClasses.QUALIFIERS includes the JSR-330 qualifier, so Kotlin @Named("…") and custom meta-qualifiers gain the same
    reference.

Tests

modules/spring-core/src/test/kotlin/com/explyt/spring/core/reference/{java,kotlin}/QualifierBeanReferenceTest.kt, written red
first (8 Kotlin failures on the first commit), with the Java twin as the green control: completion filtered by the injection
point's type, resolve to the @Bean factory, Kotlin property / @field: use site / constructor val / setter parameter /
function parameter / meta-qualifier, plus two controls — an interpolated template yields no reference, and an escaped value keeps
the whole value range.

The range assertions are deliberately independent of the production call: they compare against TextRange(1, textLength - 1) and
the decoded reference.value, not against ElementManipulators.getValueTextRange. Forcing the production range to
TextRange(0, 1) fails exactly those two tests (expected:<(1,9)> but was:<(0,1)>), so they can actually fail.

Verification

  • :spring-core:test --tests 'com.explyt.spring.core.reference.*' — 190 tests, 0 failures.
  • :spring-core:test --tests '*Qualifier*' --tests '*Autowir*' — 258 tests, 0 failures.
  • Full :spring-core:test — 2504 tests, 0 failures, 0 errors.

Backport

All lines 242–262: both touched files are identical on every release/* branch and on every gen35 head, and the fix uses only
long-standing UAST/platform API (evaluateString, ElementManipulators.getValueTextRange, getParentOfType, UAnnotation.resolve,
UVariable.type, UPolyadicExpression.operands). It goes into the generation-35 refresh pass, not as a separate cherry-pick.

@github-actions

Copy link
Copy Markdown
TestsPassed ✅Skipped ⚠️Failed
JUnit Test Report4592 ran4586 passed6 skipped0 failed

@imuromtsev
imuromtsev merged commit 5b982aa into main Oct 15, 2026
2 checks passed
@imuromtsev
imuromtsev deleted the imuromtsev/kotlin-qualifier-reference branch October 15, 2026 08:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kotlin @Qualifier on a property gets no bean name completion or reference

1 participant