diff --git a/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/types/KtTypeRenderer.kt b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/types/KtTypeRenderer.kt new file mode 100644 index 00000000000..1ceabb5f396 --- /dev/null +++ b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/types/KtTypeRenderer.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2010-2020 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. + */ + +package org.jetbrains.kotlin.idea.frontend.api.types + +import org.jetbrains.kotlin.idea.frontend.api.* +import org.jetbrains.kotlin.name.ClassId + +object KtTypeRenderer { + fun render(type: KtType, options: KtTypeRendererOptions = KtTypeRendererOptions.DEFAULT): String = type.withValidityAssertion { + buildString { render(type, options) } + } + + private fun StringBuilder.render(type: KtType, options: KtTypeRendererOptions) { + when (type) { + is KtDenotableType -> when (type) { + is KtClassType -> { + render(type.classId, options) + renderTypeArgumentsIfNotEmpty(type.typeArguments, options) + renderNullability(type.nullability) + } + is KtTypeParameterType -> { + append(type.name.asString()) + renderNullability(type.nullability) + } + } + is KtNonDenotableType -> when (type) { + is KtFlexibleType -> inParens { + render(type.lowerBound, options) + append("..") + render(type.upperBound, options) + } + is KtIntersectionType -> inParens { + type.conjuncts.forEachIndexed { index, conjunct -> + render(conjunct, options) + if (index != type.conjuncts.lastIndex) { + append("&") + } + } + } + } + is KtErrorType -> { + append(type.error) + } + else -> error("Unsupported type ${type::class}") + } + } + + private fun StringBuilder.renderTypeArgumentsIfNotEmpty(typeArguments: List, options: KtTypeRendererOptions) { + if (typeArguments.isNotEmpty()) { + append("<") + typeArguments.forEachIndexed { index, typeArgument -> + render(typeArgument, options) + if (index != typeArguments.lastIndex) { + append(", ") + } + } + append(">") + } + } + + private fun StringBuilder.render(typeArgument: KtTypeArgument, options: KtTypeRendererOptions) { + when (typeArgument) { + KtStarProjectionTypeArgument -> { + append("*") + } + is KtTypeArgumentWithVariance -> { + val varianceWithSpace = when (typeArgument.variance) { + KtTypeArgumentVariance.COVARIANT -> "out " + KtTypeArgumentVariance.CONTRAVARIANT -> "in " + KtTypeArgumentVariance.INVARIANT -> "" + } + append(varianceWithSpace) + render(typeArgument.type, options) + } + } + } + + private fun StringBuilder.renderNullability(nullability: KtTypeNullability) { + if (nullability == KtTypeNullability.NULLABLE) { + append("?") + } + } + + private fun StringBuilder.render(classId: ClassId, options: KtTypeRendererOptions) { + if (options.renderFqNames) { + append(classId.asString().replace('/', '.')) + } else { + append(classId.shortClassName.asString()) + } + } + + private inline fun StringBuilder.inParens(render: StringBuilder.() -> Unit) { + append("(") + render() + append(")") + + } +} + +data class KtTypeRendererOptions( + val renderFqNames: Boolean, +) { + companion object { + val DEFAULT = KtTypeRendererOptions( + renderFqNames = true + ) + } +} \ No newline at end of file diff --git a/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/frontend/api/types/KtTypeRendererTest.kt b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/frontend/api/types/KtTypeRendererTest.kt new file mode 100644 index 00000000000..89ef0b39459 --- /dev/null +++ b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/frontend/api/types/KtTypeRendererTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2010-2020 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. + */ + +package org.jetbrains.kotlin.idea.frontend.api.types + +import com.intellij.testFramework.LightProjectDescriptor +import org.jetbrains.kotlin.idea.executeOnPooledThreadInReadAction +import org.jetbrains.kotlin.idea.frontend.api.analyze +import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase +import org.jetbrains.kotlin.idea.test.KotlinLightProjectDescriptor +import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtFunction +import org.jetbrains.kotlin.psi.KtProperty + +class KtTypeRendererTest : KotlinLightCodeInsightFixtureTestCase() { + private fun doTestByTypeText( + type: String, + expected: String, + typeArguments: List = emptyList(), + rendererOptions: KtTypeRendererOptions = KtTypeRendererOptions.DEFAULT, + ) { + val typeArgumentsRendered = typeArguments + .takeIf { it.isNotEmpty() } + ?.joinToString(prefix = "<", postfix = ">") + .orEmpty() + val fakeKtFile = myFixture.configureByText("file.kt", "fun ${typeArgumentsRendered}foo(): $type = 1") as KtFile + val property = fakeKtFile.declarations.single() as KtFunction + val renderedType = executeOnPooledThreadInReadAction { + analyze(fakeKtFile) { + val ktType = getReturnTypeForKtDeclaration(property) + KtTypeRenderer.render(ktType, rendererOptions) + } + } + assertEquals(expected, renderedType) + } + + private fun doTestByExpression( + expression: String, + expected: String, + rendererOptions: KtTypeRendererOptions = KtTypeRendererOptions.DEFAULT, + ) { + val fakeKtFile = myFixture.configureByText("file.kt", "val a = $expression") as KtFile + val property = fakeKtFile.declarations.single() as KtProperty + val renderedType = executeOnPooledThreadInReadAction { + analyze(fakeKtFile) { + val ktType = getReturnTypeForKtDeclaration(property) + KtTypeRenderer.render(ktType, rendererOptions) + } + } + assertEquals(expected, renderedType) + } + + override fun getProjectDescriptor(): LightProjectDescriptor = KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE + + fun testInt() { + doTestByTypeText(type = "Int", expected = "kotlin.Int") + } + + fun testList() { + doTestByTypeText(type = "List", expected = "kotlin.collections.List") + } + + fun testTypeArguments() { + doTestByTypeText(type = "Map", expected = "kotlin.collections.Map", typeArguments = listOf("V")) + } + + fun testNullable() { + doTestByTypeText( + type = "Map, V?>?", + expected = "kotlin.collections.Map, V?>?", + typeArguments = listOf("V") + ) + } + + fun testNoFqNames() { + doTestByTypeText( + type = "Map, V?>?", + expected = "Map, V?>?", + typeArguments = listOf("V"), + rendererOptions = KtTypeRendererOptions(renderFqNames = false) + ) + } + + fun testFlexibleType() { + doTestByExpression( + expression = "java.lang.String.CASE_INSENSITIVE_ORDER", + expected = "(Comparator<(String..String?)>..Comparator<(String..String?)>?)", + rendererOptions = KtTypeRendererOptions(renderFqNames = false) + ) + } + + fun testIntersectionType() { + doTestByExpression( + expression = """ + run { + val x: Any = 10 + if (x is String && x is Int) x else null + }""".trimIndent(), + expected = "(String?&Int?)", + rendererOptions = KtTypeRendererOptions(renderFqNames = false) + ) + } +} \ No newline at end of file