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:
committed by
teamcityserver
parent
5b0ca06e95
commit
d77db2cda6
+9
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+109
@@ -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}"
|
||||
}
|
||||
}
|
||||
+50
@@ -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");
|
||||
}
|
||||
}
|
||||
+5
@@ -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, "")
|
||||
}
|
||||
Reference in New Issue
Block a user