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, "")
}