Inspection for experimental coroutines imports migration (KT-25251)

#KT-25251 In Progress
This commit is contained in:
Nikolay Krasko
2018-07-09 13:33:54 +03:00
parent 5b34498162
commit f9de91016a
22 changed files with 362 additions and 0 deletions
@@ -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>
+9
View File
@@ -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"/>
+9
View File
@@ -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"/>
+9
View File
@@ -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"/>
+9
View File
@@ -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"/>
+9
View File
@@ -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"/>
+9
View File
@@ -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"/>
@@ -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)
}
}
}
+1
View File
@@ -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> {
}
}
+9
View File
@@ -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
@@ -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)