Kotlin: fix type of nested class reached via imported outer type#8238
Open
vlsi wants to merge 2 commits into
Open
Kotlin: fix type of nested class reached via imported outer type#8238vlsi wants to merge 2 commits into
vlsi wants to merge 2 commits into
Conversation
vlsi
force-pushed
the
kotlin-shorten-fqn
branch
2 times, most recently
from
July 11, 2026 06:47
238cabb to
2b0c74b
Compare
When a nested type is referenced through an imported outer type (`import org.apiguardian.api.API`, then `API.Status`), the parser gave the reference the outer type `org.apiguardian.api.API` instead of the nested type `org.apiguardian.api.API$Status`. `matchClassId0` compared the reference text only against the fully-qualified class name, so the import-shortened form `API.Status` never matched and the lookup walked up the `outerClassId` chain to the top-level class, leaving `owningClass` null. Match the package-relative class name too, and fall back to the leaf name plus nesting depth for alias imports (`import ... API as GuardApi`, then `GuardApi.Status`) and to a trailing run of the nested names for imported nested types (`import foo.bar.A.B`, then `B.A.C`). A nested type reached any of these ways now keeps its own class id and populates `owningClass`, so `ShortenFullyQualifiedTypeReferences` leaves `API.Status.EXPERIMENTAL` alone instead of shortening it to an unresolved `Status.EXPERIMENTAL`. See openrewrite#8237 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
timtebeek
self-requested a review
July 11, 2026 13:01
Author
|
I just saw the build produced 27635 lines. That is huge. Have you considered reducing the output? I see you have |
| implementation(kotlin("stdlib", kotlinVersion)) | ||
|
|
||
| testImplementation("org.junit-pioneer:junit-pioneer:latest.release") | ||
| testImplementation("org.apiguardian:apiguardian-api:1.1.2") |
Contributor
There was a problem hiding this comment.
I suggest we rewrite the test cases to be generic, i.e. possibly use some standard library references or existing dependencies. This way we can refrain from adding this (test) dependency.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
On Kotlin sources,
ShortenFullyQualifiedTypeReferencescorrupts a nested type reached through an imported outer type. Given:it rewrites
API.Status.EXPERIMENTALtoStatus.EXPERIMENTALwithout adding an import, soStatusno longer resolves and the file fails to compile. The equivalent Java source is left untouched. This surfaced runningCodeCleanupover Apache JMeter's mixed Java/Kotlin sources — #8237.What
The root cause is in Kotlin type attribution, not the recipe.
PsiElementAssociations.matchClassId0matched a reference's source text only against the fully-qualified class name (org.apiguardian.api.API.Status). The import-shortened formAPI.Statusnever matched, so the lookup walked up theouterClassIdchain and returned the outer classorg.apiguardian.api.API. The nested reference therefore carried the outer type, whoseowningClassis null — which is exactly the guardShortenFullyQualifiedTypeReferencesuses to decide a reference is top-level and safe to shorten.matchClassId0now resolves every way a nested type can be written to its own class id:foo.bar.A.B) and relative to an imported outer (A.B) match by text;import ... API as GuardApi, thenGuardApi.Status) matches by leaf name plus nesting depth, since the aliased root matches neither the fully-qualified nor the package-relative name;import foo.bar.A.B, thenB.A.C) matches a trailing run of the nested names.The two multi-segment fallbacks require at least two segments, so a single receiver name whose leaf repeats deeper (the outer
AofA.B.A.C) is not matched too deep.matchClassId0to the alias and imported-nested forms.nestedFieldAccessType/nestedFieldAccessWithPackagecases: import-shortened at one and multiple levels, same simple name across different outers, and an imported nested class.How to verify
./gradlew :rewrite-kotlin:test— the full module passes, including the newShortenFullyQualifiedTypeReferencesKotlinTestcases and theKotlinTypeMappingTestnestedFieldAccess*/nestedTypesWithSameSimpleNameFromDifferentOuterscases.