FIR checker: expose API to check if two types are compatible

This is useful for quickfixes offering casts. We don't want to offer
user to cast incompatible types.

Also, explicitly allow compare to `Nothing` and handle `Nothing` from intersection
This commit is contained in:
Tianyu Geng
2021-08-03 18:55:47 -07:00
committed by teamcityserver
parent 5b0ca06e95
commit d77db2cda6
13 changed files with 239 additions and 8 deletions
@@ -5,6 +5,8 @@
package org.jetbrains.kotlin.analysis.api.fir.components
import org.jetbrains.kotlin.fir.analysis.checkers.ConeTypeCompatibilityChecker
import org.jetbrains.kotlin.fir.analysis.checkers.ConeTypeCompatibilityChecker.isCompatible
import org.jetbrains.kotlin.fir.declarations.FirResolvePhase
import org.jetbrains.kotlin.fir.expressions.FirCallableReferenceAccess
import org.jetbrains.kotlin.fir.expressions.FirDelegatedConstructorCall
@@ -94,5 +96,12 @@ internal class KtFirTypeProvider(
require(type is KtFirType)
return type.coneType.withNullability(newNullability.toConeNullability(), rootModuleSession.typeContext).asKtType()
}
override fun haveCommonSubtype(a: KtType, b: KtType): Boolean {
return analysisSession.rootModuleSession.typeContext.isCompatible(
a.coneType,
b.coneType
) == ConeTypeCompatibilityChecker.Compatibility.COMPATIBLE
}
}
@@ -0,0 +1,109 @@
/*
* Copyright 2010-2021 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.analysis.api.fir.components
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.analysis.api.analyse
import org.jetbrains.kotlin.analysis.api.fir.test.framework.AbstractHLApiSingleFileTest
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtTreeVisitorVoid
import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives
import org.jetbrains.kotlin.test.directives.model.RegisteredDirectives
import org.jetbrains.kotlin.test.model.TestFile
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.AdditionalSourceProvider
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.assertions
import java.io.File
abstract class AbstractHasCommonSubtypeTest : AbstractHLApiSingleFileTest() {
override fun doTestByFileStructure(ktFile: KtFile, module: TestModule, testServices: TestServices) {
val errors = mutableListOf<String>()
val originalText = ktFile.text
val actualTextBuilder = StringBuilder()
analyse(ktFile) {
val visitor = object : KtTreeVisitorVoid() {
override fun visitElement(element: PsiElement) {
if (element.firstChild == null) {
actualTextBuilder.append(element.text)
}
super.visitElement(element)
}
override fun visitCallExpression(expression: KtCallExpression) {
val haveCommonSubtype = when (expression.calleeExpression?.text) {
"typesHaveCommonSubtype" -> true
"typesHaveNoCommonSubtype" -> false
else -> {
super.visitCallExpression(expression)
return
}
}
val valueArguments = expression.valueArguments
require(valueArguments.size == 2) {
"Illegal call of ${expression.name} at ${expression.positionString}"
}
val a = valueArguments[0]
val aType = a.getArgumentExpression()?.getKtType()
if (aType == null) {
errors.add("'${a.text}' has no type at ${a.positionString}")
super.visitCallExpression(expression)
return
}
val b = valueArguments[1]
val bType = b.getArgumentExpression()?.getKtType()
if (bType == null) {
errors.add("'${b.text}' has no type at ${b.positionString}")
super.visitCallExpression(expression)
return
}
if (haveCommonSubtype != aType.hasCommonSubTypeWith(bType)) {
if (haveCommonSubtype) {
actualTextBuilder.append("typesHaveNoCommonSubtype")
} else {
actualTextBuilder.append("typesHaveCommonSubtype")
}
actualTextBuilder.append(expression.valueArgumentList!!.text)
} else {
super.visitCallExpression(expression)
}
}
}
visitor.visitFile(ktFile)
}
if (errors.isNotEmpty()) {
testServices.assertions.fail { errors.joinToString("\n") }
}
val actualText = actualTextBuilder.toString()
if (actualText != originalText) {
testServices.assertions.assertEqualsToFile(testDataPath, actualText)
}
}
override fun configureTest(builder: TestConfigurationBuilder) {
super.configureTest(builder)
builder.useAdditionalSourceProviders(::TestHelperProvider)
builder.defaultDirectives {
+JvmEnvironmentConfigurationDirectives.WITH_STDLIB
}
}
private class TestHelperProvider(testServices: TestServices) : AdditionalSourceProvider(testServices) {
override fun produceAdditionalFiles(globalDirectives: RegisteredDirectives, module: TestModule): List<TestFile> {
return listOf(File("idea/idea-frontend-fir/testData/helpers/hasCommonSubtype/helpers.kt").toTestFile())
}
}
private val PsiElement.positionString: String
get() {
val illegalCallPos = StringUtil.offsetToLineColumn(containingFile.text, textRange.startOffset)
return "${containingFile.virtualFile.path}:${illegalCallPos.line + 1}:${illegalCallPos.column + 1}"
}
}
@@ -0,0 +1,50 @@
/*
* Copyright 2010-2021 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.analysis.api.fir.components;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.util.KtTestUtil;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link GenerateNewCompilerTests.kt}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("analysis/analysis-api/testData/components/hasCommonSubtype")
@TestDataPath("$PROJECT_ROOT")
public class HasCommonSubtypeTestGenerated extends AbstractHasCommonSubtypeTest {
@Test
public void testAllFilesPresentInHasCommonSubtype() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/analysis-api/testData/components/hasCommonSubtype"), Pattern.compile("^(.+)\\.kt$"), null, true);
}
@Test
@TestMetadata("collections.kt")
public void testCollections() throws Exception {
runTest("analysis/analysis-api/testData/components/hasCommonSubtype/collections.kt");
}
@Test
@TestMetadata("dataClasses.kt")
public void testDataClasses() throws Exception {
runTest("analysis/analysis-api/testData/components/hasCommonSubtype/dataClasses.kt");
}
@Test
@TestMetadata("enums.kt")
public void testEnums() throws Exception {
runTest("analysis/analysis-api/testData/components/hasCommonSubtype/enums.kt");
}
@Test
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
runTest("analysis/analysis-api/testData/components/hasCommonSubtype/simple.kt");
}
}
@@ -27,6 +27,8 @@ public abstract class KtTypeProvider : KtAnalysisSessionComponent() {
public abstract fun getReceiverTypeForDoubleColonExpression(expression: KtDoubleColonExpression): KtType?
public abstract fun withNullability(type: KtType, newNullability: KtTypeNullability): KtType
public abstract fun haveCommonSubtype(a: KtType, b: KtType): Boolean
}
public interface KtTypeProviderMixIn : KtAnalysisSessionMixIn {
@@ -76,6 +78,9 @@ public interface KtTypeProviderMixIn : KtAnalysisSessionMixIn {
public fun KtType.upperBoundIfFlexible(): KtType = (this as? KtFlexibleType)?.upperBound ?: this
public fun KtType.lowerBoundIfFlexible(): KtType = (this as? KtFlexibleType)?.lowerBound ?: this
/** Check whether this type is compatible with that type. If they are compatible, it means they can have a common subtype. */
public fun KtType.hasCommonSubTypeWith(that: KtType): Boolean = analysisSession.typeProvider.haveCommonSubtype(this, that)
}
@Suppress("PropertyName")
@@ -0,0 +1,9 @@
fun test() {
typesHaveCommonSubtype(listOf(1), listOf(1))
typesHaveCommonSubtype(listOf(1), setOf(1))
typesHaveCommonSubtype(listOf(1), mutableSetOf(1))
typesHaveNoCommonSubtype(listOf(1), listOf(""))
typesHaveNoCommonSubtype(listOf(1), setOf(""))
typesHaveNoCommonSubtype(listOf(1), mutableSetOf(""))
}
@@ -0,0 +1,14 @@
interface I
data class Foo(val i: Int) : I
data class Bar(val s: String)
fun test(f1: Foo, f2: Foo, f3: Foo?, b1: Bar, b2: Bar?, i: I) {
typesHaveCommonSubtype(f1, f2)
typesHaveCommonSubtype(f1, f3)
typesHaveCommonSubtype(f3, b2)
typesHaveCommonSubtype(f1, i)
typesHaveNoCommonSubtype(f1, b1)
typesHaveNoCommonSubtype(f1, b2)
typesHaveNoCommonSubtype(b1, i)
}
@@ -0,0 +1,15 @@
enum class A {
A1, A2
}
enum class B {
B1, B2
}
fun test(a1: A, a2: A, b: B) {
typesHaveCommonSubtype(a1, a2)
typesHaveCommonSubtype(a1, A.A1)
typesHaveNoCommonSubtype(a1, b)
typesHaveNoCommonSubtype(A.A1, b)
}
@@ -0,0 +1,10 @@
fun test() {
typesHaveCommonSubtype(1, 1)
typesHaveCommonSubtype("", "")
typesHaveCommonSubtype(null, null)
typesHaveNoCommonSubtype("", null)
typesHaveNoCommonSubtype(1, 1L)
typesHaveNoCommonSubtype(1, 1.0)
typesHaveNoCommonSubtype(1, "")
}
@@ -43,7 +43,7 @@ import org.jetbrains.kotlin.types.Variance
* covariant and contravariant bounds is empty. For example, a range like `[Collection, List]` is empty and hence invalid because `List` is
* not a super class/interface of `Collection`
*/
internal object ConeTypeCompatibilityChecker {
object ConeTypeCompatibilityChecker {
/**
* The result returned by [ConeTypeCompatibilityChecker]. Note the order of enum entries matters.
@@ -63,6 +63,8 @@ internal object ConeTypeCompatibilityChecker {
}
fun ConeInferenceContext.isCompatible(a: ConeKotlinType, b: ConeKotlinType): Compatibility {
// Don't report explicit comparison with `Nothing`
if (a.isNothing || b.isNothing) return Compatibility.COMPATIBLE
if (a is ConeIntersectionType) {
return a.intersectedTypes.minOf { isCompatible(it, b) }
}
@@ -70,11 +72,13 @@ internal object ConeTypeCompatibilityChecker {
return b.intersectedTypes.minOf { isCompatible(a, it) }
}
val intersectionType = intersectTypesOrNull(listOf(a, b)) as? ConeIntersectionType ?: return Compatibility.COMPATIBLE
return intersectionType.intersectedTypes.areCompatible(this)
return when (val intersectionType = intersectTypesOrNull(listOf(a, b))) {
is ConeIntersectionType -> intersectionType.intersectedTypes.getCompatibility(this)
else -> if (intersectionType?.isNothing == true) Compatibility.HARD_INCOMPATIBLE else Compatibility.COMPATIBLE
}
}
private fun Collection<ConeKotlinType>.areCompatible(ctx: ConeInferenceContext): Compatibility {
private fun Collection<ConeKotlinType>.getCompatibility(ctx: ConeInferenceContext): Compatibility {
// If all types are nullable, then `null` makes the given types compatible.
if (all { with(ctx) { it.isNullableType() } }) return Compatibility.COMPATIBLE
@@ -90,7 +94,7 @@ internal object ConeTypeCompatibilityChecker {
// This is to stay compatible with FE1.0.
else -> Compatibility.SOFT_INCOMPATIBLE
}
return ctx.areCompatible(flatMap { it.collectUpperBounds() }.toSet(), emptySet(), compatibilityUpperBound)
return ctx.getCompatibility(flatMap { it.collectUpperBounds() }.toSet(), emptySet(), compatibilityUpperBound)
}
private fun ConeKotlinType.isConcreteType(): Boolean {
@@ -110,7 +114,7 @@ internal object ConeTypeCompatibilityChecker {
* `MyCustom<out Int>`, we let them do so since we do not know what class `MyCustom` uses the type parameter for. Empty containers are
* another example: `emptyList<Int>() == emptyList<String>()`.
*/
private fun ConeInferenceContext.areCompatible(
private fun ConeInferenceContext.getCompatibility(
upperBounds: Set<ConeClassLikeType>,
lowerBounds: Set<ConeClassLikeType>,
compatibilityUpperBound: Compatibility,
@@ -146,7 +150,7 @@ internal object ConeTypeCompatibilityChecker {
val typeArgumentMapping = mutableMapOf<FirTypeParameterSymbol, BoundTypeArguments>().apply {
for (type in upperBounds) {
collectTypeArgumentMapping(type, this@areCompatible, compatibilityUpperBound)
collectTypeArgumentMapping(type, this@getCompatibility, compatibilityUpperBound)
}
}
var result = Compatibility.COMPATIBLE
@@ -158,7 +162,7 @@ internal object ConeTypeCompatibilityChecker {
Compatibility.COMPATIBLE
} else {
checkedTypeParameters.add(paramRef)
areCompatible(upper, lower, compatibility, checkedTypeParameters)
getCompatibility(upper, lower, compatibility, checkedTypeParameters)
}
}
for (compatibility in typeArgsCompatibility) {
@@ -84,6 +84,10 @@ fun main(args: Array<String>) {
testClass<AbstractHLImportOptimizerTest> {
model("components/importOptimizer", pattern = TestGeneratorUtil.KT_WITHOUT_DOTS_IN_NAME)
}
testClass<AbstractHasCommonSubtypeTest> {
model("components/hasCommonSubtype")
}
}
testGroup("analysis/low-level-api-fir/tests", "compiler/fir/raw-fir/psi2fir/testData") {
@@ -0,0 +1,2 @@
fun typesHaveCommonSubtype(a: Any?, b: Any?) {}
fun typesHaveNoCommonSubtype(a: Any?, b: Any?) {}