AA: handle PsiType conversion for recursive type parameter case

^KT-59598 Fixed
diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirPsiTypeProvider.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirPsiTypeProvider.kt
index 678d2fc..06f442a 100644
--- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirPsiTypeProvider.kt
+++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirPsiTypeProvider.kt
@@ -11,6 +11,7 @@
 import com.intellij.psi.impl.compiled.ClsTypeElementImpl
 import com.intellij.psi.impl.compiled.SignatureParsing
 import com.intellij.psi.impl.compiled.StubBuildingVisitor
+import java.text.StringCharacterIterator
 import org.jetbrains.kotlin.analysis.api.components.KtPsiTypeProvider
 import org.jetbrains.kotlin.analysis.api.fir.KtFirAnalysisSession
 import org.jetbrains.kotlin.analysis.api.fir.types.KtFirType
@@ -42,7 +43,6 @@
 import org.jetbrains.kotlin.psi.KtFile
 import org.jetbrains.kotlin.psi.psiUtil.parents
 import org.jetbrains.kotlin.types.model.SimpleTypeMarker
-import java.text.StringCharacterIterator
 
 internal class KtFirPsiTypeProvider(
     override val analysisSession: KtFirAnalysisSession,
@@ -238,7 +238,15 @@
         if (type !is ConeClassLikeType) return null
 
         val hasStableName = type.classId?.isLocal == true
-        if (!hasStableName) return null
+        if (!hasStableName) {
+            // Make sure we're not going to expand type argument over and over again.
+            // If so, i.e., if there is a recursive type argument, return the current, non-null [type]
+            // to prevent the following [substituteTypeOr*] from proceeding to its own (recursive) substitution.
+            if (type.hasRecursiveTypeArgument()) return type
+            // Return `null` means we will use [fir.resolve.substitution.Substitutors]'s [substituteRecursive]
+            // that literally substitutes type arguments recursively.
+            return null
+        }
 
         val firClassNode = type.lookupTag.toSymbol(session) as? FirClassSymbol
         if (firClassNode != null) {
@@ -248,4 +256,22 @@
         return if (type.nullability.isNullable) session.builtinTypes.nullableAnyType.type
         else session.builtinTypes.anyType.type
     }
+
+    private fun ConeKotlinType.hasRecursiveTypeArgument(
+        visited: MutableSet<ConeKotlinType> = mutableSetOf()
+    ): Boolean {
+        if (typeArguments.isEmpty()) return false
+        visited.add(this)
+        for (projection in typeArguments) {
+            // E.g., Test : Comparable<Test>
+            val type = (projection as? ConeKotlinTypeProjection)?.type ?: continue
+            // E.g., Comparable<Test>
+            val newType = substituteOrNull(type) ?: continue
+            if (newType in visited) return true
+            // Visit new type: e.g., Test, as a type argument, is substituted with Comparable<Test>, again.
+            if (newType.hasRecursiveTypeArgument(visited)) return true
+        }
+        return false
+    }
+
 }
diff --git a/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/psiTypeProvider/FirIdeDependentAnalysisSourceModuleAnalysisApiExpressionPsiTypeProviderTestGenerated.java b/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/psiTypeProvider/FirIdeDependentAnalysisSourceModuleAnalysisApiExpressionPsiTypeProviderTestGenerated.java
index b8bad01..ecafad0 100644
--- a/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/psiTypeProvider/FirIdeDependentAnalysisSourceModuleAnalysisApiExpressionPsiTypeProviderTestGenerated.java
+++ b/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/psiTypeProvider/FirIdeDependentAnalysisSourceModuleAnalysisApiExpressionPsiTypeProviderTestGenerated.java
@@ -65,6 +65,12 @@
     }
 
     @Test
+    @TestMetadata("recursiveTypeParameter.kt")
+    public void testRecursiveTypeParameter() throws Exception {
+        runTest("analysis/analysis-api/testData/components/psiTypeProvider/psiType/forExpression/recursiveTypeParameter.kt");
+    }
+
+    @Test
     @TestMetadata("typeParamFlexibleUpperBound.kt")
     public void testTypeParamFlexibleUpperBound() throws Exception {
         runTest("analysis/analysis-api/testData/components/psiTypeProvider/psiType/forExpression/typeParamFlexibleUpperBound.kt");
diff --git a/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/psiTypeProvider/FirIdeNormalAnalysisSourceModuleAnalysisApiExpressionPsiTypeProviderTestGenerated.java b/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/psiTypeProvider/FirIdeNormalAnalysisSourceModuleAnalysisApiExpressionPsiTypeProviderTestGenerated.java
index e7026eb..6477699 100644
--- a/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/psiTypeProvider/FirIdeNormalAnalysisSourceModuleAnalysisApiExpressionPsiTypeProviderTestGenerated.java
+++ b/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/psiTypeProvider/FirIdeNormalAnalysisSourceModuleAnalysisApiExpressionPsiTypeProviderTestGenerated.java
@@ -65,6 +65,12 @@
     }
 
     @Test
+    @TestMetadata("recursiveTypeParameter.kt")
+    public void testRecursiveTypeParameter() throws Exception {
+        runTest("analysis/analysis-api/testData/components/psiTypeProvider/psiType/forExpression/recursiveTypeParameter.kt");
+    }
+
+    @Test
     @TestMetadata("typeParamFlexibleUpperBound.kt")
     public void testTypeParamFlexibleUpperBound() throws Exception {
         runTest("analysis/analysis-api/testData/components/psiTypeProvider/psiType/forExpression/typeParamFlexibleUpperBound.kt");
diff --git a/analysis/analysis-api-impl-base/tests/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/psiTypeProvider/AbstractAnalysisApiExpressionPsiTypeProviderTest.kt b/analysis/analysis-api-impl-base/tests/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/psiTypeProvider/AbstractAnalysisApiExpressionPsiTypeProviderTest.kt
index 0d348a1..44865d1 100644
--- a/analysis/analysis-api-impl-base/tests/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/psiTypeProvider/AbstractAnalysisApiExpressionPsiTypeProviderTest.kt
+++ b/analysis/analysis-api-impl-base/tests/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/psiTypeProvider/AbstractAnalysisApiExpressionPsiTypeProviderTest.kt
@@ -14,6 +14,7 @@
 import org.jetbrains.kotlin.psi.KtDeclaration
 import org.jetbrains.kotlin.psi.KtExpression
 import org.jetbrains.kotlin.psi.KtFile
+import org.jetbrains.kotlin.psi.KtValueArgument
 import org.jetbrains.kotlin.test.model.TestModule
 import org.jetbrains.kotlin.test.services.TestServices
 import org.jetbrains.kotlin.test.services.assertions
@@ -21,7 +22,11 @@
 
 abstract class AbstractAnalysisApiExpressionPsiTypeProviderTest : AbstractAnalysisApiSingleFileTest() {
     override fun doTestByFileStructure(ktFile: KtFile, module: TestModule, testServices: TestServices) {
-        val declarationAtCaret = testServices.expressionMarkerProvider.getSelectedElement(ktFile) as KtExpression
+        val declarationAtCaret = when (val element = testServices.expressionMarkerProvider.getSelectedElement(ktFile)) {
+            is KtExpression -> element
+            is KtValueArgument -> element.getArgumentExpression()!!
+            else -> error("Unexpected element: $element of ${element::class.java}")
+        }
         val containingDeclaration = declarationAtCaret.parentOfType<KtDeclaration>()
             ?: error("Can't find containing declaration for $declarationAtCaret")
         val containingClass = getContainingKtLightClass(containingDeclaration, ktFile)
diff --git a/analysis/analysis-api-standalone/tests-gen/org/jetbrains/kotlin/analysis/api/standalone/fir/test/cases/generated/cases/components/psiTypeProvider/FirStandaloneNormalAnalysisSourceModuleAnalysisApiExpressionPsiTypeProviderTestGenerated.java b/analysis/analysis-api-standalone/tests-gen/org/jetbrains/kotlin/analysis/api/standalone/fir/test/cases/generated/cases/components/psiTypeProvider/FirStandaloneNormalAnalysisSourceModuleAnalysisApiExpressionPsiTypeProviderTestGenerated.java
index f0fc229..5b91aa3 100644
--- a/analysis/analysis-api-standalone/tests-gen/org/jetbrains/kotlin/analysis/api/standalone/fir/test/cases/generated/cases/components/psiTypeProvider/FirStandaloneNormalAnalysisSourceModuleAnalysisApiExpressionPsiTypeProviderTestGenerated.java
+++ b/analysis/analysis-api-standalone/tests-gen/org/jetbrains/kotlin/analysis/api/standalone/fir/test/cases/generated/cases/components/psiTypeProvider/FirStandaloneNormalAnalysisSourceModuleAnalysisApiExpressionPsiTypeProviderTestGenerated.java
@@ -65,6 +65,12 @@
     }
 
     @Test
+    @TestMetadata("recursiveTypeParameter.kt")
+    public void testRecursiveTypeParameter() throws Exception {
+        runTest("analysis/analysis-api/testData/components/psiTypeProvider/psiType/forExpression/recursiveTypeParameter.kt");
+    }
+
+    @Test
     @TestMetadata("typeParamFlexibleUpperBound.kt")
     public void testTypeParamFlexibleUpperBound() throws Exception {
         runTest("analysis/analysis-api/testData/components/psiTypeProvider/psiType/forExpression/typeParamFlexibleUpperBound.kt");
diff --git a/analysis/analysis-api/testData/components/psiTypeProvider/psiType/forExpression/recursiveTypeParameter.kt b/analysis/analysis-api/testData/components/psiTypeProvider/psiType/forExpression/recursiveTypeParameter.kt
new file mode 100644
index 0000000..a00650b
--- /dev/null
+++ b/analysis/analysis-api/testData/components/psiTypeProvider/psiType/forExpression/recursiveTypeParameter.kt
@@ -0,0 +1,14 @@
+// WITH_STDLIB
+
+fun testNotTransitive() {
+  class Test(val v: Int): Comparable<Test> {
+    override fun compareTo(other: Test): Int {
+      return v.compareTo(-other.v)
+    }
+  }
+  val list = listOf(Test(1), Test(2), Test(3), Test(4))
+  checkTransitiveComparator(<expr>list</expr>)
+}
+
+fun <T : Comparable<T>> checkTransitiveComparator(list: List<T>) {
+}
diff --git a/analysis/analysis-api/testData/components/psiTypeProvider/psiType/forExpression/recursiveTypeParameter.txt b/analysis/analysis-api/testData/components/psiTypeProvider/psiType/forExpression/recursiveTypeParameter.txt
new file mode 100644
index 0000000..8885140
--- /dev/null
+++ b/analysis/analysis-api/testData/components/psiTypeProvider/psiType/forExpression/recursiveTypeParameter.txt
@@ -0,0 +1,2 @@
+KtType: kotlin.collections.List<Test>
+PsiType: PsiType:List<? extends Comparable<? super Test>>