What version of OpenRewrite are you using?
rewrite 8.86 / rewrite-gradle-plugin 7.35, reproduced against main.
How are you running OpenRewrite?
ShortenFullyQualifiedTypeReferences (via a CodeCleanup composite) applied to a Kotlin source in a mixed Java/Kotlin module.
What is the smallest, simplest way to reproduce the problem?
import org.apiguardian.api.API
@API(status = API.Status.EXPERIMENTAL, since = "5.6")
class Foo
ShortenFullyQualifiedTypeReferences rewrites the nested enum access API.Status.EXPERIMENTAL to Status.EXPERIMENTAL without adding an import for Status:
import org.apiguardian.api.API
@API(status = Status.EXPERIMENTAL, since = "5.6")
class Foo
The result no longer compiles (unresolved reference: Status).
A test that reproduces it is in the accompanying PR.
What did you expect to see?
The reference is left unchanged — it already uses the simple, imported outer name API. This is what the recipe does on the equivalent Java source.
What happened instead?
The qualifier API. is dropped and no import is added, producing uncompilable Kotlin.
Root cause
The recipe guard is correct. visitFieldAccess only shortens a reference whose type is top-level: type instanceof JavaType.Class && ((JavaType.Class) type).getOwningClass() == null. For the nested enum org.apiguardian.api.API.Status the owning class is API, so a correctly attributed reference is skipped, as it is on Java.
The bug is in Kotlin type attribution: the nested reference API.Status was given the outer type org.apiguardian.api.API instead of the nested type org.apiguardian.api.API$Status. That outer type is genuinely top-level, so its owningClass is null, the guard passes, and the recipe shortens API.Status to Status. The follow-up maybeAddImport call then receives the outer name org.apiguardian.api.API and does nothing, because API is already imported, so no new import appears. (The earlier "maybeAddImport is a no-op for nested types" reading was the same wrong type surfacing again, not a second gap.)
Concretely, PsiElementAssociations.matchClassId0 matched the reference's source text only against the fully-qualified class name (org.apiguardian.api.API.Status). The import-shortened form API.Status never matched, so the lookup walked up the outerClassId chain and returned the outer class id. An aliased import (import org.apiguardian.api.API as GuardApi, then GuardApi.Status) and an imported nested type (import foo.bar.A.B, then B.A.C) hit the same path, because their source root matches neither the fully-qualified nor the package-relative name.
The fix resolves each of these forms to its own class id: it matches the package-relative name, falls back to the leaf name plus nesting depth for the alias form, and to a trailing run of the nested names for the imported-nested form. The nested reference then keeps its own class id, owningClass is API, and the guard correctly skips it.
Are you interested in contributing a fix to OpenRewrite?
Fixed in #8238: matchClassId0 resolves the package-relative, aliased, and imported-nested forms to their own class id, with type-mapping and recipe tests for the imported-outer, multi-level, aliased, same-simple-name, and imported-nested cases.
What version of OpenRewrite are you using?
rewrite 8.86 / rewrite-gradle-plugin 7.35, reproduced against
main.How are you running OpenRewrite?
ShortenFullyQualifiedTypeReferences(via aCodeCleanupcomposite) applied to a Kotlin source in a mixed Java/Kotlin module.What is the smallest, simplest way to reproduce the problem?
ShortenFullyQualifiedTypeReferencesrewrites the nested enum accessAPI.Status.EXPERIMENTALtoStatus.EXPERIMENTALwithout adding an import forStatus:The result no longer compiles (
unresolved reference: Status).A test that reproduces it is in the accompanying PR.
What did you expect to see?
The reference is left unchanged — it already uses the simple, imported outer name
API. This is what the recipe does on the equivalent Java source.What happened instead?
The qualifier
API.is dropped and no import is added, producing uncompilable Kotlin.Root cause
The recipe guard is correct.
visitFieldAccessonly shortens a reference whose type is top-level:type instanceof JavaType.Class && ((JavaType.Class) type).getOwningClass() == null. For the nested enumorg.apiguardian.api.API.Statusthe owning class isAPI, so a correctly attributed reference is skipped, as it is on Java.The bug is in Kotlin type attribution: the nested reference
API.Statuswas given the outer typeorg.apiguardian.api.APIinstead of the nested typeorg.apiguardian.api.API$Status. That outer type is genuinely top-level, so itsowningClassis null, the guard passes, and the recipe shortensAPI.StatustoStatus. The follow-upmaybeAddImportcall then receives the outer nameorg.apiguardian.api.APIand does nothing, becauseAPIis already imported, so no new import appears. (The earlier "maybeAddImportis a no-op for nested types" reading was the same wrong type surfacing again, not a second gap.)Concretely,
PsiElementAssociations.matchClassId0matched the 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 class id. An aliased import (import org.apiguardian.api.API as GuardApi, thenGuardApi.Status) and an imported nested type (import foo.bar.A.B, thenB.A.C) hit the same path, because their source root matches neither the fully-qualified nor the package-relative name.The fix resolves each of these forms to its own class id: it matches the package-relative name, falls back to the leaf name plus nesting depth for the alias form, and to a trailing run of the nested names for the imported-nested form. The nested reference then keeps its own class id,
owningClassisAPI, and the guard correctly skips it.Are you interested in contributing a fix to OpenRewrite?
Fixed in #8238:
matchClassId0resolves the package-relative, aliased, and imported-nested forms to their own class id, with type-mapping and recipe tests for the imported-outer, multi-level, aliased, same-simple-name, and imported-nested cases.