~ to fixup
diff --git a/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCliJavaFileManagerImpl.kt b/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCliJavaFileManagerImpl.kt index 915e450..22307cb 100644 --- a/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCliJavaFileManagerImpl.kt +++ b/compiler/cli/cli-base/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCliJavaFileManagerImpl.kt
@@ -57,7 +57,7 @@ private lateinit var singleJavaFileRootsIndex: SingleJavaFileRootsIndex private lateinit var packagePartProviders: List<PackagePartProvider> - private lateinit var javaModuleFinder: JavaModuleFinder + lateinit var javaModuleFinder: JavaModuleFinder private set /**
diff --git a/compiler/java-direct/src/org/jetbrains/kotlin/java/direct/JavaClassFinderOverBinaryIndex.kt b/compiler/java-direct/src/org/jetbrains/kotlin/java/direct/JavaClassFinderOverBinaryIndex.kt index 032028c..ec84b12 100644 --- a/compiler/java-direct/src/org/jetbrains/kotlin/java/direct/JavaClassFinderOverBinaryIndex.kt +++ b/compiler/java-direct/src/org/jetbrains/kotlin/java/direct/JavaClassFinderOverBinaryIndex.kt
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.cli.jvm.index.JavaFileExtensions import org.jetbrains.kotlin.cli.jvm.index.JavaRoot import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesIndex +import org.jetbrains.kotlin.K1Deprecation import org.jetbrains.kotlin.load.java.JavaClassFinder import org.jetbrains.kotlin.load.java.structure.JavaAnnotation import org.jetbrains.kotlin.load.java.structure.JavaClass
diff --git a/compiler/java-direct/src/org/jetbrains/kotlin/java/direct/model/JavaTypeOverAst.kt b/compiler/java-direct/src/org/jetbrains/kotlin/java/direct/model/JavaTypeOverAst.kt index e97c992..93ceee0 100644 --- a/compiler/java-direct/src/org/jetbrains/kotlin/java/direct/model/JavaTypeOverAst.kt +++ b/compiler/java-direct/src/org/jetbrains/kotlin/java/direct/model/JavaTypeOverAst.kt
@@ -238,17 +238,31 @@ return explicitArgs } + // Each parameter is kept together with the class that declares it: the implicit outer + // argument is a parameter *of that class*, so the only correct answer is that class's own + // instance — FIR matches `JavaTypeParameter`s to `FirTypeParameterSymbol`s by identity + // through the per-class `JavaTypeParameterStack`, which is keyed by exactly these objects. val outerTypeParams = mutableListOf<JavaTypeParameter>() + val outerTypeParamOwners = mutableListOf<JavaClass>() var outer = javaClass.outerClass while (outer != null && !outer.isStatic) { - outerTypeParams.addAll(outer.typeParameters) + for (typeParam in outer.typeParameters) { + outerTypeParams.add(typeParam) + outerTypeParamOwners.add(outer) + } outer = outer.outerClass } - // Resolve each outer type param through the current context so we get the caller's H - // (e.g., Outer.H) rather than the abstract H from the outer class declaration. - val lexicalArgs = outerTypeParams.map { typeParam -> - with(resolutionContext) { findTypeParameter(typeParam.name.asString()) } + // A declared parameter is available at this reference only if the reference is written + // inside the declaring class. This is an *identity* test on the enclosing chain, not a + // lexical lookup by name: a same-named parameter of a nested class or of the enclosing + // generic method shadows the outer one for name resolution, but it is not the parameter + // this implicit argument denotes (`class A<T> { class Inner<T> { Inner<String> foo(); } }` + // means `A<A.T>.Inner<String>`). Mirrors PSI, whose `JavaClassifierTypeImpl` substitutes an + // unmapped `PsiTypeParameter` to itself and never looks names up in the lexical scope. + // `null` means "not available here" and routes to the inherited recovery below. + val lexicalArgs = outerTypeParams.mapIndexed { index, typeParam -> + typeParam.takeIf { isInScopeOfDeclaringClass(outerTypeParamOwners[index]) } } // Inherited case: the inner class is non-static but its outer arguments are neither written @@ -277,6 +291,30 @@ } /** + * Whether this type reference is written inside [declaringClass], i.e. whether + * [declaringClass]'s own type parameters denote the enclosing instance's ones here. + * + * Walks the classes lexically enclosing the reference, innermost first. Per JLS a `static` + * class has no enclosing instance, which severs the chain of implicit outer type arguments — + * the same break the collection walk above and PSI's `PsiUtil.typeParametersIterable` make. + * + * Classes are compared by [JavaClass.classId] when both have one, so that a reference resolved + * through the class finder (a distinct instance for the same class) is still recognised; the + * identity comparison covers local/anonymous classes, which have no `ClassId`. + */ + private fun isInScopeOfDeclaringClass(declaringClass: JavaClass): Boolean { + val declaringClassId = declaringClass.classId + var enclosing: JavaClass? = resolutionContext.scopeContext.containingClass + while (enclosing != null) { + if (enclosing === declaringClass) return true + if (declaringClassId != null && enclosing.classId == declaringClassId) return true + if (enclosing.isStatic) return false + enclosing = enclosing.outerClass + } + return false + } + + /** * Recursively collects all REFERENCE_PARAMETER_LIST nodes in source order, * traversing into child JAVA_CODE_REFERENCE nodes (for nested qualified types). * For "A<T>.B<U>" → [paramList(<T>), paramList(<U>)] regardless of AST structure.
diff --git a/compiler/java-direct/src/org/jetbrains/kotlin/java/direct/resolution/JavaTypeResolver.kt b/compiler/java-direct/src/org/jetbrains/kotlin/java/direct/resolution/JavaTypeResolver.kt index 308ce0f..081aa03 100644 --- a/compiler/java-direct/src/org/jetbrains/kotlin/java/direct/resolution/JavaTypeResolver.kt +++ b/compiler/java-direct/src/org/jetbrains/kotlin/java/direct/resolution/JavaTypeResolver.kt
@@ -430,16 +430,17 @@ // of that supertype list sees an empty answer and the walk simply moves outward. var current: JavaClass? = containingClass while (current != null) { - val currentId = current.classId + val inheritingClass = current + val currentId = inheritingClass.classId if (currentId != null) { for (supertype in FirBackedJavaClassAdapter(currentId, session).supertypes) { val coneSupertype = (supertype as? FirBackedJavaClassifierType)?.coneType ?: continue val recovered = findTypeArgsForClassInHierarchy(coneSupertype, outerClassId, session, mutableSetOf()) - if (recovered != null) return recovered.map { recoveredOuterTypeArgument(it, session) } + if (recovered != null) return recovered.map { recoveredOuterTypeArgument(it, inheritingClass, session) } } } - if (current.isStatic) break - current = current.outerClass + if (inheritingClass.isStatic) break + current = inheritingClass.outerClass } return null } @@ -449,27 +450,52 @@ * * A recovered argument is often a type parameter of the containing class itself * (`class Outer<E1, E2> extends BaseOuter<Integer, E1>` recovers `Integer, E1`). Such an argument - * must be handed back as the model's *own* [JavaTypeParameter]: FIR matches `JavaTypeParameter`s to - * `FirTypeParameterSymbol`s by identity through the per-class `JavaTypeParameterStack`, which does - * not know resolution-time cone-backed wrappers, so the cone route - * ([firBackedJavaType]) would degrade the reference to an unbounded wildcard. The name is - * necessarily in scope — the parameter is declared by a class of the containing chain the recovery - * walked. + * must be handed back as the model's *own* [JavaTypeParameter], the one declared by [inheritingClass] + * or by one of its outer classes: + * `JavaTypeConversion.toConeKotlinTypeForFlexibleBound` resolves a [JavaTypeParameter] solely by + * looking it up in the class's `MutableJavaTypeParameterStack`, a map keyed by the very instances + * `FirJavaFacade.createFirJavaClass` took from `JavaClass.typeParameters`. A cone-backed wrapper is + * not a key there, so routing a type parameter through [firBackedJavaType] could not produce one: + * today that route has no type-parameter branch at all and degrades the reference to an unbounded + * wildcard; teaching it one would yield `ConeErrorType(ConeUnresolvedNameError)` instead, unless the + * identity protocol in shared FIR code were changed. + * + * The parameter is looked up in [inheritingClass]'s own declaration chain — the class whose + * supertype the argument was read off, then its outer classes — and not in the lexical scope: a + * same-named parameter of the enclosing generic method or of a nested class would shadow it there, + * and handing that one back would silently substitute a different symbol. Names within a single + * parameter list are unique per JLS, and the innermost-first walk mirrors Java shadowing, so the + * lookup is unambiguous. */ context(c: JavaResolutionContext) -private fun recoveredOuterTypeArgument(projection: ConeTypeProjection, session: FirSession): JavaType { +private fun recoveredOuterTypeArgument(projection: ConeTypeProjection, inheritingClass: JavaClass, session: FirSession): JavaType { // Arguments read off a resolved FIR supertype are flexible (`kotlin/Int!`, `E1!`), while the // Java model is nullability-agnostic and FIR re-derives flexibility when converting back — so // the lower bound is what has to be handed over. Without unwrapping, neither branch below // matches and everything degrades to an unbounded wildcard. val type = (projection as? ConeKotlinType)?.lowerBoundIfFlexible() ?: return firBackedJavaType(projection, session) if (type is ConeTypeParameterType) { - findTypeParameter(type.lookupTag.name.asString())?.let { return JavaTypeParameterTypeOverAst(it) } + findTypeParameterInDeclarationChain(inheritingClass, type.lookupTag.name)?.let { return JavaTypeParameterTypeOverAst(it) } } return firBackedJavaType(type, session) } /** + * Finds the [JavaTypeParameter] named [name] declared by [startClass] or, failing that, by one of + * its outer classes, innermost first. A `static` class has no enclosing instance and severs the + * chain, so its outer classes' parameters are not visible in its declarations. + */ +private fun findTypeParameterInDeclarationChain(startClass: JavaClass, name: Name): JavaTypeParameter? { + var current: JavaClass? = startClass + while (current != null) { + current.typeParameters.firstOrNull { it.name == name }?.let { return it } + if (current.isStatic) return null + current = current.outerClass + } + return null +} + +/** * Recursively searches [type]'s supertype hierarchy (via [FirBackedJavaClassAdapter.supertypes]) * for [targetClassId], substituting type arguments down each intermediate class so that, e.g., * `A<X> : Super<X>` instantiated as `A<String>` yields `Super<String>`. Returns the matched
diff --git a/compiler/java-direct/test/org/jetbrains/kotlin/java/direct/JavaParsingImplicitOuterTypeArgumentsTest.kt b/compiler/java-direct/test/org/jetbrains/kotlin/java/direct/JavaParsingImplicitOuterTypeArgumentsTest.kt new file mode 100644 index 0000000..3d48c1b6 --- /dev/null +++ b/compiler/java-direct/test/org/jetbrains/kotlin/java/direct/JavaParsingImplicitOuterTypeArgumentsTest.kt
@@ -0,0 +1,124 @@ +/* + * Copyright 2010-2026 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +@file:Suppress("UnstableApiUsage") + +package org.jetbrains.kotlin.java.direct + +import org.jetbrains.kotlin.load.java.structure.JavaClass +import org.jetbrains.kotlin.load.java.structure.JavaClassifierType +import org.jetbrains.kotlin.load.java.structure.JavaType +import org.jetbrains.kotlin.load.java.structure.JavaTypeParameter +import org.jetbrains.kotlin.name.Name +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Test + +/** + * The JLS-implicit type arguments of the enclosing instance, added for a bare reference to a + * non-static inner class (`Inner` inside `Outer<T>` denotes `Outer<T>.Inner`). + * + * Such an argument must be the *declaring* class's own [org.jetbrains.kotlin.load.java.structure.JavaTypeParameter] + * instance: FIR maps `JavaTypeParameter`s to `FirTypeParameterSymbol`s by object identity through + * the per-class `JavaTypeParameterStack`, so a same-named parameter of a different declaration is + * not merely a cosmetic difference — it substitutes a different symbol, silently. + */ +class JavaParsingImplicitOuterTypeArgumentsTest : JavaParsingTestBase() { + + @Test + fun testImplicitOuterArgumentIsOuterClassParameter() { + val source = """ + public class A<T> { + class Inner { + Inner foo() { return null; } + } + } + """.trimIndent() + val a = parseFirstClass(source) + val inner = a.findInnerClass(Name.identifier("Inner"))!! + + val args = inner.implicitOuterArgumentsOfReturnTypeOf("foo") + assertEquals(1, args.size, "`Inner` denotes `A<T>.Inner`, so it has one implicit outer argument") + assertSame(a.typeParameters[0], args[0], "The implicit outer argument must be A's own T") + } + + @Test + fun testImplicitOuterArgumentIsNotShadowedByInnerClassParameter() { + // `Inner<String>` inside `Inner` denotes `A<A.T>.Inner<String>`: the nested `T` shadows the + // outer one for name resolution, but the enclosing instance is still parameterized by A's T. + val source = """ + public class A<T> { + class Inner<T> { + Inner<String> foo() { return null; } + } + } + """.trimIndent() + val a = parseFirstClass(source) + val inner = a.findInnerClass(Name.identifier("Inner"))!! + + val returnType = inner.returnTypeOf("foo") + assertEquals( + listOf("String", "T"), + returnType.typeArguments.map { (it as JavaClassifierType).classifierQualifiedName }, + "Explicit argument first, then the implicit outer one", + ) + + val outerArgument = returnType.typeArguments[1]!!.classifierOfTypeParameter() + assertSame(a.typeParameters[0], outerArgument, "The implicit outer argument must be A's T, not Inner's T") + } + + @Test + fun testImplicitOuterArgumentIsNotShadowedByMethodTypeParameter() { + // A generic method whose parameter happens to be named like the outer class's one must not + // hijack the implicit outer argument of inner-class types written in its signature. + val source = """ + public class A<T> { + class Inner { } + <T> Inner foo() { return null; } + } + """.trimIndent() + val a = parseFirstClass(source) + + val args = a.implicitOuterArgumentsOfReturnTypeOf("foo") + assertEquals(1, args.size) + assertSame(a.typeParameters[0], args[0], "The implicit outer argument must be A's T, not foo's T") + } + + @Test + fun testImplicitOuterArgumentsOfNestedOuterChain() { + // Both enclosing levels contribute, innermost first, and neither is shadowed by `Inner`'s own `U`. + val source = """ + public class A<T> { + class Mid<U> { + class Inner<U> { + Inner<String> foo() { return null; } + } + } + } + """.trimIndent() + val a = parseFirstClass(source) + val mid = a.findInnerClass(Name.identifier("Mid"))!! + val inner = mid.findInnerClass(Name.identifier("Inner"))!! + + val args = inner.implicitOuterArgumentsOfReturnTypeOf("foo") + assertEquals(2, args.size, "`Inner` denotes `A<T>.Mid<U>.Inner`, so both outer levels contribute") + assertSame(mid.typeParameters[0], args[0], "First implicit outer argument must be Mid's own U") + assertSame(a.typeParameters[0], args[1], "Second implicit outer argument must be A's T") + } +} + +private fun JavaClass.returnTypeOf(methodName: String): JavaClassifierType = + methods.first { it.name.asString() == methodName }.returnType as JavaClassifierType + +/** + * The type-parameter arguments of the method's return type, in order. Explicitly written arguments + * are class-like (and, in these tests, unresolved), so filtering on the classifier isolates the + * implicit outer ones the model appends. + */ +private fun JavaClass.implicitOuterArgumentsOfReturnTypeOf(methodName: String): List<JavaTypeParameter> = + returnTypeOf(methodName).typeArguments.mapNotNull { (it as? JavaClassifierType)?.classifier as? JavaTypeParameter } + +private fun JavaType.classifierOfTypeParameter(): JavaTypeParameter = + (this as JavaClassifierType).classifier as JavaTypeParameter