Inspection for experimental coroutines imports migration (KT-25251)
#KT-25251 In Progress
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
<html>
|
||||
<body>
|
||||
This inspection reports experimental coroutines usages that are incompatible with Kotlin 1.3+ and should be updated.
|
||||
</body>
|
||||
</html>
|
||||
@@ -2918,6 +2918,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.migration.ObsoleteExperimentalCoroutinesInspection"
|
||||
displayName="Experimental coroutines usages are deprecated since 1.3"
|
||||
groupPath="Kotlin"
|
||||
groupName="Migration"
|
||||
enabledByDefault="true"
|
||||
level="ERROR"
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
|
||||
|
||||
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
|
||||
|
||||
@@ -2915,6 +2915,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.migration.ObsoleteExperimentalCoroutinesInspection"
|
||||
displayName="Experimental coroutines usages are deprecated since 1.3"
|
||||
groupPath="Kotlin"
|
||||
groupName="Migration"
|
||||
enabledByDefault="true"
|
||||
level="ERROR"
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
|
||||
|
||||
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
|
||||
|
||||
@@ -2918,6 +2918,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.migration.ObsoleteExperimentalCoroutinesInspection"
|
||||
displayName="Experimental coroutines usages are deprecated since 1.3"
|
||||
groupPath="Kotlin"
|
||||
groupName="Migration"
|
||||
enabledByDefault="true"
|
||||
level="ERROR"
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
|
||||
|
||||
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
|
||||
|
||||
@@ -2919,6 +2919,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.migration.ObsoleteExperimentalCoroutinesInspection"
|
||||
displayName="Experimental coroutines usages are deprecated since 1.3"
|
||||
groupPath="Kotlin"
|
||||
groupName="Migration"
|
||||
enabledByDefault="true"
|
||||
level="ERROR"
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
|
||||
|
||||
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
|
||||
|
||||
@@ -2918,6 +2918,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.migration.ObsoleteExperimentalCoroutinesInspection"
|
||||
displayName="Experimental coroutines usages are deprecated since 1.3"
|
||||
groupPath="Kotlin"
|
||||
groupName="Migration"
|
||||
enabledByDefault="true"
|
||||
level="ERROR"
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
|
||||
|
||||
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
|
||||
|
||||
@@ -2918,6 +2918,15 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.migration.ObsoleteExperimentalCoroutinesInspection"
|
||||
displayName="Experimental coroutines usages are deprecated since 1.3"
|
||||
groupPath="Kotlin"
|
||||
groupName="Migration"
|
||||
enabledByDefault="true"
|
||||
level="ERROR"
|
||||
language="kotlin"
|
||||
/>
|
||||
|
||||
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
|
||||
|
||||
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. 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.idea.inspections.migration
|
||||
|
||||
import com.intellij.codeInspection.*
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import org.jetbrains.kotlin.config.LanguageVersion
|
||||
import org.jetbrains.kotlin.descriptors.CallableDescriptor
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
|
||||
import org.jetbrains.kotlin.idea.imports.importableFqName
|
||||
import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection
|
||||
import org.jetbrains.kotlin.idea.project.languageVersionSettings
|
||||
import org.jetbrains.kotlin.idea.references.KtSimpleNameReference
|
||||
import org.jetbrains.kotlin.idea.references.mainReference
|
||||
import org.jetbrains.kotlin.idea.references.resolveMainReferenceToDescriptors
|
||||
import org.jetbrains.kotlin.idea.stubindex.KotlinTopLevelFunctionFqnNameIndex
|
||||
import org.jetbrains.kotlin.idea.util.ImportInsertHelper
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.parents
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
|
||||
|
||||
class ObsoleteExperimentalCoroutinesInspection : AbstractKotlinInspection(), CleanupLocalInspectionTool {
|
||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): KtVisitorVoid {
|
||||
return simpleNameExpressionVisitor(fun(simpleNameExpression) {
|
||||
run {
|
||||
val versionAtLeast13 = simpleNameExpression.languageVersionSettings.languageVersion >= LanguageVersion.KOTLIN_1_3
|
||||
if (!versionAtLeast13 && !ApplicationManager.getApplication().isUnitTestMode) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
when (simpleNameExpression.text) {
|
||||
RESUME_MARKER, RESUME_WITH_EXCEPTION_MARKER -> {
|
||||
simpleNameExpression.parent as? KtCallExpression ?: return
|
||||
|
||||
val descriptor = simpleNameExpression.resolveMainReferenceToDescriptors().firstOrNull() ?: return
|
||||
val callableDescriptor = descriptor as? CallableDescriptor ?: return
|
||||
|
||||
val resolvedToFqName = callableDescriptor.fqNameOrNull()?.asString() ?: return
|
||||
val fixFqName = when (resolvedToFqName) {
|
||||
RESUME_FUNCTION_FQNAME -> RESUME_FUNCTION_FIX_FQNAME
|
||||
RESUME_WITH_EXCEPTION_FQNAME -> RESUME_WITH_EXCEPTION_FIX_FQNAME
|
||||
else -> null
|
||||
} ?: return
|
||||
|
||||
val problemDescriptor = holder.manager.createProblemDescriptor(
|
||||
simpleNameExpression,
|
||||
simpleNameExpression,
|
||||
"Methods are absent in coroutines class since 1.3",
|
||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
||||
isOnTheFly,
|
||||
ImportExtensionFunctionFix(fixFqName)
|
||||
)
|
||||
|
||||
holder.registerProblem(problemDescriptor)
|
||||
}
|
||||
|
||||
EXPERIMENTAL_COROUTINES_MARKER -> {
|
||||
val parent = simpleNameExpression.parent as? KtExpression ?: return
|
||||
val reportExpression = parent as? KtDotQualifiedExpression ?: simpleNameExpression
|
||||
|
||||
findBinding(simpleNameExpression) ?: return
|
||||
|
||||
val problemDescriptor = holder.manager.createProblemDescriptor(
|
||||
reportExpression,
|
||||
reportExpression,
|
||||
"Experimental coroutines usages are obsolete since 1.3",
|
||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
||||
isOnTheFly,
|
||||
ObsoleteCoroutineImportFix()
|
||||
)
|
||||
|
||||
holder.registerProblem(problemDescriptor)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXPERIMENTAL_COROUTINES_MARKER = "experimental"
|
||||
|
||||
private const val RESUME_MARKER = "resume"
|
||||
private const val RESUME_FUNCTION_FQNAME = "kotlin.coroutines.experimental.Continuation.resume"
|
||||
private const val RESUME_FUNCTION_FIX_FQNAME = "kotlin.coroutines.resume"
|
||||
|
||||
private const val RESUME_WITH_EXCEPTION_MARKER = "resumeWithException"
|
||||
private const val RESUME_WITH_EXCEPTION_FQNAME = "kotlin.coroutines.experimental.Continuation.resumeWithException"
|
||||
private const val RESUME_WITH_EXCEPTION_FIX_FQNAME = "kotlin.coroutines.resumeWithException"
|
||||
|
||||
// All quick-fixes should have same name to be executable together
|
||||
private const val QUICK_FIX_NAME = "Fix experimental coroutines usage"
|
||||
|
||||
private class Binding(
|
||||
val bindTo: FqName,
|
||||
val shouldRemove: Boolean,
|
||||
val importDirective: KtImportDirective
|
||||
)
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
private val PACKAGE_BINDING = mapOf(
|
||||
"kotlinx.coroutines.experimental" to "kotlinx.coroutines",
|
||||
"kotlin.coroutines.experimental" to "kotlin.coroutines"
|
||||
)
|
||||
|
||||
private val IMPORTS_TO_REMOVE = setOf(
|
||||
"kotlin.coroutines.experimental.buildSequence"
|
||||
)
|
||||
|
||||
private fun findBinding(simpleNameExpression: KtSimpleNameExpression): Binding? {
|
||||
if (simpleNameExpression.text != EXPERIMENTAL_COROUTINES_MARKER) return null
|
||||
|
||||
val importDirective = simpleNameExpression.parents
|
||||
.takeWhile { it is KtDotQualifiedExpression || it is KtImportDirective }
|
||||
.lastOrNull() as? KtImportDirective ?: return null
|
||||
|
||||
val fqNameStr = importDirective.importedFqName?.asString() ?: return null
|
||||
|
||||
val bindEntry = PACKAGE_BINDING.entries.find { (affectedImportPrefix, _) ->
|
||||
fqNameStr.startsWith(affectedImportPrefix)
|
||||
} ?: return null
|
||||
|
||||
return Binding(FqName(bindEntry.value), fqNameStr in IMPORTS_TO_REMOVE, importDirective)
|
||||
}
|
||||
}
|
||||
|
||||
private class ObsoleteCoroutineImportFix : LocalQuickFix {
|
||||
override fun getFamilyName() = QUICK_FIX_NAME
|
||||
|
||||
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
||||
val element = descriptor.psiElement
|
||||
val simpleNameExpression = when (element) {
|
||||
is KtSimpleNameExpression -> element
|
||||
is KtDotQualifiedExpression -> element.selectorExpression as? KtSimpleNameExpression
|
||||
else -> null
|
||||
} ?: return
|
||||
|
||||
val binding = ObsoleteExperimentalCoroutinesInspection.findBinding(simpleNameExpression) ?: return
|
||||
|
||||
if (binding.shouldRemove) {
|
||||
binding.importDirective.delete()
|
||||
} else {
|
||||
simpleNameExpression.mainReference.bindToFqName(
|
||||
binding.bindTo, shorteningMode = KtSimpleNameReference.ShorteningMode.NO_SHORTENING
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ImportExtensionFunctionFix(val fqName: String) : LocalQuickFix {
|
||||
override fun getFamilyName() = QUICK_FIX_NAME
|
||||
|
||||
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
||||
val element = descriptor.psiElement
|
||||
element as? KtSimpleNameExpression ?: return
|
||||
|
||||
val importFun =
|
||||
KotlinTopLevelFunctionFqnNameIndex.getInstance().get(fqName, element.project, GlobalSearchScope.allScope(element.project))
|
||||
.asSequence()
|
||||
.map { it.resolveToDescriptorIfAny() }
|
||||
.find { it != null && it.importableFqName?.asString() == fqName } ?: return
|
||||
|
||||
ImportInsertHelper.getInstance(element.project).importDescriptor(element.containingKtFile, importFun, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.jetbrains.kotlin.idea.inspections.migration.ObsoleteExperimentalCoroutinesInspection
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Fix experimental coroutines usage" "true"
|
||||
// ERROR: Unresolved reference: buildSequence
|
||||
import kotlin.coroutines.<caret>experimental.buildSequence
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val lazySeq = buildSequence<Int> {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Fix experimental coroutines usage" "true"
|
||||
// ERROR: Unresolved reference: buildSequence
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val lazySeq = buildSequence<Int> {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Fix experimental coroutines usage" "true"
|
||||
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
|
||||
// WITH_RUNTIME
|
||||
|
||||
import kotlin.coroutines.experimental.Continuation
|
||||
|
||||
fun test(con: Continuation<Int>) {
|
||||
con.<caret>resume(12)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Fix experimental coroutines usage" "true"
|
||||
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
|
||||
// WITH_RUNTIME
|
||||
|
||||
import kotlin.coroutines.experimental.Continuation
|
||||
|
||||
fun test(con: Continuation<Int>) {
|
||||
con.<caret>resume(12)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Fix experimental coroutines usage" "true"
|
||||
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
|
||||
// WITH_RUNTIME
|
||||
|
||||
import kotlin.coroutines.experimental.Continuation
|
||||
|
||||
fun test(con: Continuation<Int>) {
|
||||
con.<caret>resumeWithException(RuntimeException("Haha"))
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Fix experimental coroutines usage" "true"
|
||||
// SHOULD_BE_AVAILABLE_AFTER_EXECUTION
|
||||
// WITH_RUNTIME
|
||||
|
||||
import kotlin.coroutines.experimental.Continuation
|
||||
|
||||
fun test(con: Continuation<Int>) {
|
||||
con.<caret>resumeWithException(RuntimeException("Haha"))
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// "Fix experimental coroutines usage" "true"
|
||||
// ERROR: Unresolved reference: coroutines
|
||||
|
||||
import kotlin.coroutines.<caret>experimental.*
|
||||
@@ -0,0 +1,4 @@
|
||||
// "Fix experimental coroutines usage" "true"
|
||||
// ERROR: Unresolved reference: coroutines
|
||||
|
||||
import kotlin.coroutines.*
|
||||
@@ -0,0 +1,4 @@
|
||||
// "Fix experimental coroutines usage" "true"
|
||||
// ERROR: Unresolved reference: kotlinx
|
||||
|
||||
import kotlinx.coroutines.<caret>experimental.delay
|
||||
@@ -0,0 +1,4 @@
|
||||
// "Fix experimental coroutines usage" "true"
|
||||
// ERROR: Unresolved reference: kotlinx
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
+13
@@ -3026,6 +3026,19 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/quickfix/obsoleteCoroutines")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
public static class ObsoleteCoroutines extends AbstractQuickFixMultiFileTest {
|
||||
private void runTest(String testDataFilePath) throws Exception {
|
||||
KotlinTestUtils.runTest(this::doTestWithExtraFile, TargetBackend.ANY, testDataFilePath);
|
||||
}
|
||||
|
||||
public void testAllFilesPresentInObsoleteCoroutines() throws Exception {
|
||||
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/obsoleteCoroutines"), Pattern.compile("^(\\w+)\\.((before\\.Main\\.\\w+)|(test))$"), TargetBackend.ANY, true);
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/quickfix/optimizeImports")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
|
||||
@@ -1916,6 +1916,19 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/quickfix/obsoleteCoroutines")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
public static class ObsoleteCoroutines extends AbstractQuickFixMultiFileTest {
|
||||
private void runTest(String testDataFilePath) throws Exception {
|
||||
KotlinTestUtils.runTest(this::doTestWithExtraFile, TargetBackend.ANY, testDataFilePath);
|
||||
}
|
||||
|
||||
public void testAllFilesPresentInObsoleteCoroutines() throws Exception {
|
||||
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/obsoleteCoroutines"), Pattern.compile("^(\\w+)\\.((before\\.Main\\.\\w+)|(test))$"), TargetBackend.ANY, true);
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/quickfix/optimizeImports")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
|
||||
@@ -8146,6 +8146,44 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/quickfix/obsoleteCoroutines")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
public static class ObsoleteCoroutines extends AbstractQuickFixTest {
|
||||
private void runTest(String testDataFilePath) throws Exception {
|
||||
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
|
||||
}
|
||||
|
||||
public void testAllFilesPresentInObsoleteCoroutines() throws Exception {
|
||||
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/obsoleteCoroutines"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
|
||||
}
|
||||
|
||||
@TestMetadata("buildSequence.kt")
|
||||
public void testBuildSequence() throws Exception {
|
||||
runTest("idea/testData/quickfix/obsoleteCoroutines/buildSequence.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("resume.kt")
|
||||
public void testResume() throws Exception {
|
||||
runTest("idea/testData/quickfix/obsoleteCoroutines/resume.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("resumeWithException.kt")
|
||||
public void testResumeWithException() throws Exception {
|
||||
runTest("idea/testData/quickfix/obsoleteCoroutines/resumeWithException.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("starImport.kt")
|
||||
public void testStarImport() throws Exception {
|
||||
runTest("idea/testData/quickfix/obsoleteCoroutines/starImport.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("unresolvedKotlinxImport.kt")
|
||||
public void testUnresolvedKotlinxImport() throws Exception {
|
||||
runTest("idea/testData/quickfix/obsoleteCoroutines/unresolvedKotlinxImport.kt");
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/quickfix/optimizeImports")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
|
||||
Reference in New Issue
Block a user