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?) {}