FIR IDE: introduce KtType rendering

This commit is contained in:
Ilya Kirillov
2020-08-06 16:27:27 +03:00
parent 9f33d0147c
commit 36a161080f
2 changed files with 217 additions and 0 deletions
@@ -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<KtTypeArgument>, 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
)
}
}
@@ -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<String> = 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<Int>", expected = "kotlin.collections.List<kotlin.Int>")
}
fun testTypeArguments() {
doTestByTypeText(type = "Map<Int, V>", expected = "kotlin.collections.Map<kotlin.Int, V>", typeArguments = listOf("V"))
}
fun testNullable() {
doTestByTypeText(
type = "Map<List<Int?>, V?>?",
expected = "kotlin.collections.Map<kotlin.collections.List<kotlin.Int?>, V?>?",
typeArguments = listOf("V")
)
}
fun testNoFqNames() {
doTestByTypeText(
type = "Map<List<Int?>, V?>?",
expected = "Map<List<Int?>, 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)
)
}
}