From d77db2cda64cf9d2cf59ecb78ceaa5f632c61249 Mon Sep 17 00:00:00 2001 From: Tianyu Geng Date: Tue, 3 Aug 2021 18:55:47 -0700 Subject: [PATCH] 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 --- .../api/fir/components/KtFirTypeProvider.kt | 9 ++ .../AbstractHasCommonSubtypeTest.kt | 109 ++++++++++++++++++ .../HasCommonSubtypeTestGenerated.java | 50 ++++++++ .../analysis/api/components/KtTypeProvider.kt | 5 + .../hasCommonSubtype/collections.kt | 9 ++ .../hasCommonSubtype/dataClasses.kt | 14 +++ .../components/hasCommonSubtype/enums.kt | 15 +++ .../components/hasCommonSubtype/simple.kt | 10 ++ .../checkers/ConeTypeCompatibilityChecker.kt | 20 ++-- .../tests/analysis/api/GenerateTests.kt | 4 + .../frontend-api-generator/build.gradle.kts | 0 .../tests/frontend/api/GenerateTests.kt | 0 .../helpers/hasCommonSubtype/helpers.kt | 2 + 13 files changed, 239 insertions(+), 8 deletions(-) create mode 100644 analysis/analysis-api-fir/tests/org/jetbrains/kotlin/analysis/api/fir/components/AbstractHasCommonSubtypeTest.kt create mode 100644 analysis/analysis-api-fir/tests/org/jetbrains/kotlin/analysis/api/fir/components/HasCommonSubtypeTestGenerated.java create mode 100644 analysis/analysis-api/testData/components/hasCommonSubtype/collections.kt create mode 100644 analysis/analysis-api/testData/components/hasCommonSubtype/dataClasses.kt create mode 100644 analysis/analysis-api/testData/components/hasCommonSubtype/enums.kt create mode 100644 analysis/analysis-api/testData/components/hasCommonSubtype/simple.kt delete mode 100644 generators/frontend-api-generator/build.gradle.kts delete mode 100644 generators/frontend-api-generator/tests/org/jetbrains/kotlin/generators/tests/frontend/api/GenerateTests.kt create mode 100644 idea/idea-frontend-fir/testData/helpers/hasCommonSubtype/helpers.kt diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirTypeProvider.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirTypeProvider.kt index 65b4ebc7d9f..d90631cf905 100644 --- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirTypeProvider.kt +++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirTypeProvider.kt @@ -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 + } } diff --git a/analysis/analysis-api-fir/tests/org/jetbrains/kotlin/analysis/api/fir/components/AbstractHasCommonSubtypeTest.kt b/analysis/analysis-api-fir/tests/org/jetbrains/kotlin/analysis/api/fir/components/AbstractHasCommonSubtypeTest.kt new file mode 100644 index 00000000000..02274f96041 --- /dev/null +++ b/analysis/analysis-api-fir/tests/org/jetbrains/kotlin/analysis/api/fir/components/AbstractHasCommonSubtypeTest.kt @@ -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() + 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 { + 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}" + } +} \ No newline at end of file diff --git a/analysis/analysis-api-fir/tests/org/jetbrains/kotlin/analysis/api/fir/components/HasCommonSubtypeTestGenerated.java b/analysis/analysis-api-fir/tests/org/jetbrains/kotlin/analysis/api/fir/components/HasCommonSubtypeTestGenerated.java new file mode 100644 index 00000000000..880241cb53d --- /dev/null +++ b/analysis/analysis-api-fir/tests/org/jetbrains/kotlin/analysis/api/fir/components/HasCommonSubtypeTestGenerated.java @@ -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"); + } +} diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/components/KtTypeProvider.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/components/KtTypeProvider.kt index 4acba62a683..9e8209a6d09 100644 --- a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/components/KtTypeProvider.kt +++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/components/KtTypeProvider.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") diff --git a/analysis/analysis-api/testData/components/hasCommonSubtype/collections.kt b/analysis/analysis-api/testData/components/hasCommonSubtype/collections.kt new file mode 100644 index 00000000000..ed57d0272fd --- /dev/null +++ b/analysis/analysis-api/testData/components/hasCommonSubtype/collections.kt @@ -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("")) +} \ No newline at end of file diff --git a/analysis/analysis-api/testData/components/hasCommonSubtype/dataClasses.kt b/analysis/analysis-api/testData/components/hasCommonSubtype/dataClasses.kt new file mode 100644 index 00000000000..ce6f37d008c --- /dev/null +++ b/analysis/analysis-api/testData/components/hasCommonSubtype/dataClasses.kt @@ -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) +} diff --git a/analysis/analysis-api/testData/components/hasCommonSubtype/enums.kt b/analysis/analysis-api/testData/components/hasCommonSubtype/enums.kt new file mode 100644 index 00000000000..5e3d5ef0c44 --- /dev/null +++ b/analysis/analysis-api/testData/components/hasCommonSubtype/enums.kt @@ -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) +} \ No newline at end of file diff --git a/analysis/analysis-api/testData/components/hasCommonSubtype/simple.kt b/analysis/analysis-api/testData/components/hasCommonSubtype/simple.kt new file mode 100644 index 00000000000..7c09421af07 --- /dev/null +++ b/analysis/analysis-api/testData/components/hasCommonSubtype/simple.kt @@ -0,0 +1,10 @@ +fun test() { + typesHaveCommonSubtype(1, 1) + typesHaveCommonSubtype("", "") + typesHaveCommonSubtype(null, null) + + typesHaveNoCommonSubtype("", null) + typesHaveNoCommonSubtype(1, 1L) + typesHaveNoCommonSubtype(1, 1.0) + typesHaveNoCommonSubtype(1, "") +} \ No newline at end of file diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/ConeTypeCompatibilityChecker.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/ConeTypeCompatibilityChecker.kt index fbe28db4084..48c7fce15f5 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/ConeTypeCompatibilityChecker.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/ConeTypeCompatibilityChecker.kt @@ -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.areCompatible(ctx: ConeInferenceContext): Compatibility { + private fun Collection.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`, we let them do so since we do not know what class `MyCustom` uses the type parameter for. Empty containers are * another example: `emptyList() == emptyList()`. */ - private fun ConeInferenceContext.areCompatible( + private fun ConeInferenceContext.getCompatibility( upperBounds: Set, lowerBounds: Set, compatibilityUpperBound: Compatibility, @@ -146,7 +150,7 @@ internal object ConeTypeCompatibilityChecker { val typeArgumentMapping = mutableMapOf().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) { diff --git a/generators/analysis-api-generator/tests/org/jetbrains/kotlin/generators/tests/analysis/api/GenerateTests.kt b/generators/analysis-api-generator/tests/org/jetbrains/kotlin/generators/tests/analysis/api/GenerateTests.kt index 0eee306851e..858fc84c8b8 100644 --- a/generators/analysis-api-generator/tests/org/jetbrains/kotlin/generators/tests/analysis/api/GenerateTests.kt +++ b/generators/analysis-api-generator/tests/org/jetbrains/kotlin/generators/tests/analysis/api/GenerateTests.kt @@ -84,6 +84,10 @@ fun main(args: Array) { testClass { model("components/importOptimizer", pattern = TestGeneratorUtil.KT_WITHOUT_DOTS_IN_NAME) } + + testClass { + model("components/hasCommonSubtype") + } } testGroup("analysis/low-level-api-fir/tests", "compiler/fir/raw-fir/psi2fir/testData") { diff --git a/generators/frontend-api-generator/build.gradle.kts b/generators/frontend-api-generator/build.gradle.kts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/generators/frontend-api-generator/tests/org/jetbrains/kotlin/generators/tests/frontend/api/GenerateTests.kt b/generators/frontend-api-generator/tests/org/jetbrains/kotlin/generators/tests/frontend/api/GenerateTests.kt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/idea/idea-frontend-fir/testData/helpers/hasCommonSubtype/helpers.kt b/idea/idea-frontend-fir/testData/helpers/hasCommonSubtype/helpers.kt new file mode 100644 index 00000000000..939d6b69e0f --- /dev/null +++ b/idea/idea-frontend-fir/testData/helpers/hasCommonSubtype/helpers.kt @@ -0,0 +1,2 @@ +fun typesHaveCommonSubtype(a: Any?, b: Any?) {} +fun typesHaveNoCommonSubtype(a: Any?, b: Any?) {}