KT-39869 Refactor ObsoleteExperimentalCoroutinesInspection.kt

- We need this to reuse the logic for migration inspections
This commit is contained in:
Roman Golyshev
2020-06-30 14:56:38 +03:00
committed by Roman Golyshev
parent 6b5c31e2fc
commit 871bb30dbe
2 changed files with 174 additions and 150 deletions
@@ -37,30 +37,46 @@ import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.parents
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
class ObsoleteExperimentalCoroutinesInspection : AbstractKotlinInspection(), CleanupLocalInspectionTool, MigrationFix { internal abstract class ObsoleteCodeMigrationInspection() : AbstractKotlinInspection(), CleanupLocalInspectionTool, MigrationFix {
override fun isApplicable(migrationInfo: MigrationInfo): Boolean { protected abstract val fromVersion: LanguageVersion
return migrationInfo.isLanguageVersionUpdate(LanguageVersion.KOTLIN_1_2, LanguageVersion.KOTLIN_1_3) protected abstract val toVersion: LanguageVersion
protected abstract val problems: List<ObsoleteCodeMigrationProblem>
final override fun isApplicable(migrationInfo: MigrationInfo): Boolean {
return migrationInfo.isLanguageVersionUpdate(fromVersion, toVersion)
} }
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): KtVisitorVoid { final override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): KtVisitorVoid {
return simpleNameExpressionVisitor(fun(simpleNameExpression) { return simpleNameExpressionVisitor(fun(simpleNameExpression) {
run { run {
val versionAtLeast13 = simpleNameExpression.languageVersionSettings.languageVersion >= LanguageVersion.KOTLIN_1_3 val versionIsSatisfied = simpleNameExpression.languageVersionSettings.languageVersion >= toVersion
if (!versionAtLeast13 && !ApplicationManager.getApplication().isUnitTestMode) { if (!versionIsSatisfied && !ApplicationManager.getApplication().isUnitTestMode) {
return return
} }
} }
for (registeredProblem in PROBLEMS) { for (registeredProblem in problems) {
if (registeredProblem.report(holder, isOnTheFly, simpleNameExpression)) { if (registeredProblem.report(holder, isOnTheFly, simpleNameExpression)) {
return return
} }
} }
}) })
} }
}
companion object { internal interface ObsoleteCodeMigrationProblem {
private val PROBLEMS = listOf( fun report(holder: ProblemsHolder, isOnTheFly: Boolean, simpleNameExpression: KtSimpleNameExpression): Boolean
}
private interface ObsoleteCodeFix {
fun applyFix(project: Project, descriptor: ProblemDescriptor)
}
internal class ObsoleteExperimentalCoroutinesInspection : ObsoleteCodeMigrationInspection() {
override val fromVersion: LanguageVersion = LanguageVersion.KOTLIN_1_2
override val toVersion: LanguageVersion = LanguageVersion.KOTLIN_1_3
override val problems = listOf(
ObsoleteTopLevelFunctionUsage( ObsoleteTopLevelFunctionUsage(
"buildSequence", "buildSequence",
"kotlin.coroutines.experimental.buildSequence", "kotlin.coroutines.experimental.buildSequence",
@@ -81,33 +97,21 @@ class ObsoleteExperimentalCoroutinesInspection : AbstractKotlinInspection(), Cle
"kotlin.coroutines.experimental.Continuation.resumeWithException", "kotlin.coroutines.experimental.Continuation.resumeWithException",
"kotlin.coroutines.resumeWithException" "kotlin.coroutines.resumeWithException"
), ),
ExperimentalImportUsage() ObsoleteCoroutinesImportsUsage
) )
} }
}
private interface CoroutineMigrationProblem {
fun report(holder: ProblemsHolder, isOnTheFly: Boolean, simpleNameExpression: KtSimpleNameExpression): Boolean
}
// Shortcut quick fix for running inspection in the project scope. // Shortcut quick fix for running inspection in the project scope.
// Should work like RunInspectionAction.runInspection. // Should work like RunInspectionAction.runInspection.
private class ObsoleteCoroutineUsageInWholeFix : LocalQuickFix { private abstract class ObsoleteCodeInWholeProjectFix : LocalQuickFix {
override fun getFamilyName(): String = KotlinBundle.message("obsolete.coroutine.usage.in.whole.fix.family.name") protected abstract val inspectionName: String
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val toolWrapper = InspectionProjectProfileManager.getInstance(project).currentProfile.getInspectionTool(
ObsoleteExperimentalCoroutinesInspection().shortName,
project
)!!
final override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val toolWrapper = InspectionProjectProfileManager.getInstance(project).currentProfile.getInspectionTool(inspectionName, project)!!
runToolInProject(project, toolWrapper) runToolInProject(project, toolWrapper)
} }
override fun startInWriteAction(): Boolean = false final override fun startInWriteAction(): Boolean = false
companion object {
val INSTANCE = ObsoleteCoroutineUsageInWholeFix()
private fun runToolInProject(project: Project, toolWrapper: InspectionToolWrapper<*, *>) { private fun runToolInProject(project: Project, toolWrapper: InspectionToolWrapper<*, *>) {
val managerEx = InspectionManager.getInstance(project) as InspectionManagerEx val managerEx = InspectionManager.getInstance(project) as InspectionManagerEx
@@ -126,7 +130,7 @@ private class ObsoleteCoroutineUsageInWholeFix : LocalQuickFix {
} }
// Overcome failure during profile creating because of absent tools in tests // Overcome failure during profile creating because of absent tools in tests
inline fun <T> runInInspectionProfileInitMode(runnable: () -> T): T { private inline fun <T> runInInspectionProfileInitMode(runnable: () -> T): T {
return if (!ApplicationManager.getApplication().isUnitTestMode) { return if (!ApplicationManager.getApplication().isUnitTestMode) {
runnable() runnable()
} else { } else {
@@ -140,23 +144,23 @@ private class ObsoleteCoroutineUsageInWholeFix : LocalQuickFix {
} }
} }
} }
private object ObsoleteCoroutineUsageInWholeProjectFix : ObsoleteCodeInWholeProjectFix() {
override val inspectionName = ObsoleteExperimentalCoroutinesInspection().shortName
override fun getFamilyName(): String = KotlinBundle.message("obsolete.coroutine.usage.in.whole.fix.family.name")
} }
/** /**
* There should be a single fix class with the same family name, this way it can be executed for all found coroutines problems from UI. * There should be a single fix class with the same family name, this way it can be executed for all found coroutines problems from UI.
*/ */
private class ObsoleteCoroutineUsageFix(val delegate: CoroutineFix) : LocalQuickFix { private abstract class ObsoleteCodeFixDelegateQuickFix(private val delegate: ObsoleteCodeFix) : LocalQuickFix {
override fun getFamilyName(): String = KotlinBundle.message("obsolete.coroutine.usage.fix.family.name") final override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
delegate.applyFix(project, descriptor) delegate.applyFix(project, descriptor)
} }
}
companion object { private class ObsoleteCoroutinesDelegateQuickFix(delegate: ObsoleteCodeFix) : ObsoleteCodeFixDelegateQuickFix(delegate) {
interface CoroutineFix { override fun getFamilyName(): String = KotlinBundle.message("obsolete.coroutine.usage.fix.family.name")
fun applyFix(project: Project, descriptor: ProblemDescriptor)
}
}
} }
private fun isTopLevelCallForReplace(simpleNameExpression: KtSimpleNameExpression, oldFqName: String, newFqName: String): Boolean { private fun isTopLevelCallForReplace(simpleNameExpression: KtSimpleNameExpression, oldFqName: String, newFqName: String): Boolean {
@@ -177,17 +181,17 @@ private fun isTopLevelCallForReplace(simpleNameExpression: KtSimpleNameExpressio
return !isInIndex return !isInIndex
} }
private fun fixesWithWholeProject(isOnTheFly: Boolean, fix: LocalQuickFix): Array<LocalQuickFix> { private fun fixesWithWholeProject(isOnTheFly: Boolean, fix: LocalQuickFix, wholeProjectFix: LocalQuickFix): Array<LocalQuickFix> {
if (!isOnTheFly) { if (!isOnTheFly) {
return arrayOf(fix) return arrayOf(fix)
} }
return arrayOf(fix, ObsoleteCoroutineUsageInWholeFix.INSTANCE) return arrayOf(fix, wholeProjectFix)
} }
private class ObsoleteTopLevelFunctionUsage( private class ObsoleteTopLevelFunctionUsage(
val textMarker: String, val oldFqName: String, val newFqName: String val textMarker: String, val oldFqName: String, val newFqName: String
) : CoroutineMigrationProblem { ) : ObsoleteCodeMigrationProblem {
override fun report(holder: ProblemsHolder, isOnTheFly: Boolean, simpleNameExpression: KtSimpleNameExpression): Boolean { override fun report(holder: ProblemsHolder, isOnTheFly: Boolean, simpleNameExpression: KtSimpleNameExpression): Boolean {
if (simpleNameExpression.text != textMarker) return false if (simpleNameExpression.text != textMarker) return false
@@ -199,7 +203,7 @@ private class ObsoleteTopLevelFunctionUsage(
simpleNameExpression, simpleNameExpression,
KotlinBundle.message("0.is.expected.to.be.used.since.kotlin.1.3", newFqName), KotlinBundle.message("0.is.expected.to.be.used.since.kotlin.1.3", newFqName),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
*fixesWithWholeProject(isOnTheFly, ObsoleteCoroutineUsageFix(fix)) *fixesWithWholeProject(isOnTheFly, ObsoleteCoroutinesDelegateQuickFix(fix), ObsoleteCoroutineUsageInWholeProjectFix)
) )
return true return true
@@ -208,7 +212,7 @@ private class ObsoleteTopLevelFunctionUsage(
private val fix = RebindReferenceFix(newFqName) private val fix = RebindReferenceFix(newFqName)
companion object { companion object {
private class RebindReferenceFix(val fqName: String) : ObsoleteCoroutineUsageFix.Companion.CoroutineFix { private class RebindReferenceFix(val fqName: String) : ObsoleteCodeFix {
override fun applyFix(project: Project, descriptor: ProblemDescriptor) { override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement val element = descriptor.psiElement
if (element !is KtSimpleNameExpression) return if (element !is KtSimpleNameExpression) return
@@ -223,7 +227,7 @@ private class ObsoleteTopLevelFunctionUsage(
private class ObsoleteExtensionFunctionUsage( private class ObsoleteExtensionFunctionUsage(
val textMarker: String, val oldFqName: String, val newFqName: String val textMarker: String, val oldFqName: String, val newFqName: String
) : CoroutineMigrationProblem { ) : ObsoleteCodeMigrationProblem {
override fun report(holder: ProblemsHolder, isOnTheFly: Boolean, simpleNameExpression: KtSimpleNameExpression): Boolean { override fun report(holder: ProblemsHolder, isOnTheFly: Boolean, simpleNameExpression: KtSimpleNameExpression): Boolean {
if (simpleNameExpression.text != textMarker) return false if (simpleNameExpression.text != textMarker) return false
@@ -235,7 +239,7 @@ private class ObsoleteExtensionFunctionUsage(
simpleNameExpression, simpleNameExpression,
KotlinBundle.message("methods.are.absent.in.coroutines.class.since.1.3"), KotlinBundle.message("methods.are.absent.in.coroutines.class.since.1.3"),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
*fixesWithWholeProject(isOnTheFly, ObsoleteCoroutineUsageFix(fix)) *fixesWithWholeProject(isOnTheFly, ObsoleteCoroutinesDelegateQuickFix(fix), ObsoleteCoroutineUsageInWholeProjectFix)
) )
return true return true
@@ -244,7 +248,7 @@ private class ObsoleteExtensionFunctionUsage(
private val fix = ImportExtensionFunctionFix(newFqName) private val fix = ImportExtensionFunctionFix(newFqName)
companion object { companion object {
private class ImportExtensionFunctionFix(val fqName: String) : ObsoleteCoroutineUsageFix.Companion.CoroutineFix { private class ImportExtensionFunctionFix(val fqName: String) : ObsoleteCodeFix {
override fun applyFix(project: Project, descriptor: ProblemDescriptor) { override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement val element = descriptor.psiElement
if (element !is KtSimpleNameExpression) return if (element !is KtSimpleNameExpression) return
@@ -256,16 +260,33 @@ private class ObsoleteExtensionFunctionUsage(
.map { it.resolveToDescriptorIfAny() } .map { it.resolveToDescriptorIfAny() }
.find { it != null && it.importableFqName?.asString() == fqName } ?: return .find { it != null && it.importableFqName?.asString() == fqName } ?: return
ImportInsertHelper.getInstance(element.project) ImportInsertHelper.getInstance(element.project).importDescriptor(
.importDescriptor(element.containingKtFile, importFun, forceAllUnderImport = false) element.containingKtFile,
importFun,
forceAllUnderImport = false
)
} }
} }
} }
} }
private class ExperimentalImportUsage : CoroutineMigrationProblem { private abstract class ObsoleteImportsUsage : ObsoleteCodeMigrationProblem {
override fun report(holder: ProblemsHolder, isOnTheFly: Boolean, simpleNameExpression: KtSimpleNameExpression): Boolean { /**
if (simpleNameExpression.text != EXPERIMENTAL_COROUTINES_MARKER) return false * Required to report the problem only on one psi element instead of the every single qualifier
* in the import statement.
*/
protected abstract val textMarker: String
protected abstract val packageBindings: Map<String, String>
protected open val importsToRemove: Set<String> = emptySet()
protected abstract val wholeProjectFix: LocalQuickFix
protected abstract fun problemMessage(): String
protected abstract fun wrapFix(fix: ObsoleteCodeFix): LocalQuickFix
final override fun report(holder: ProblemsHolder, isOnTheFly: Boolean, simpleNameExpression: KtSimpleNameExpression): Boolean {
if (simpleNameExpression.text != textMarker) return false
val parent = simpleNameExpression.parent as? KtExpression ?: return false val parent = simpleNameExpression.parent as? KtExpression ?: return false
val reportExpression = parent as? KtDotQualifiedExpression ?: simpleNameExpression val reportExpression = parent as? KtDotQualifiedExpression ?: simpleNameExpression
@@ -274,36 +295,16 @@ private class ExperimentalImportUsage : CoroutineMigrationProblem {
holder.registerProblem( holder.registerProblem(
reportExpression, reportExpression,
KotlinBundle.message("experimental.coroutines.usages.are.obsolete.since.1.3"), problemMessage(),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
*fixesWithWholeProject(isOnTheFly, ObsoleteCoroutineUsageFix(ObsoleteCoroutineImportFix)) *fixesWithWholeProject(isOnTheFly, wrapFix(ObsoleteCoroutineImportFix()), wholeProjectFix)
) )
return true return true
} }
companion object {
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",
"kotlin.coroutines.experimental.buildIterator"
)
private const val EXPERIMENTAL_COROUTINES_MARKER = "experimental"
private fun findBinding(simpleNameExpression: KtSimpleNameExpression): Binding? { private fun findBinding(simpleNameExpression: KtSimpleNameExpression): Binding? {
if (simpleNameExpression.text != EXPERIMENTAL_COROUTINES_MARKER) return null if (simpleNameExpression.text != textMarker) return null
val importDirective = simpleNameExpression.parents val importDirective = simpleNameExpression.parents
.takeWhile { it is KtDotQualifiedExpression || it is KtImportDirective } .takeWhile { it is KtDotQualifiedExpression || it is KtImportDirective }
@@ -311,18 +312,18 @@ private class ExperimentalImportUsage : CoroutineMigrationProblem {
val fqNameStr = importDirective.importedFqName?.asString() ?: return null val fqNameStr = importDirective.importedFqName?.asString() ?: return null
val bindEntry = PACKAGE_BINDING.entries.find { (affectedImportPrefix, _) -> val bindEntry = packageBindings.entries.find { (affectedImportPrefix, _) ->
fqNameStr.startsWith(affectedImportPrefix) fqNameStr.startsWith(affectedImportPrefix)
} ?: return null } ?: return null
return Binding( return Binding(
FqName(bindEntry.value), FqName(bindEntry.value),
fqNameStr in IMPORTS_TO_REMOVE, fqNameStr in importsToRemove,
importDirective importDirective
) )
} }
private object ObsoleteCoroutineImportFix : ObsoleteCoroutineUsageFix.Companion.CoroutineFix { private inner class ObsoleteCoroutineImportFix : ObsoleteCodeFix {
override fun applyFix(project: Project, descriptor: ProblemDescriptor) { override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement val element = descriptor.psiElement
val simpleNameExpression = when (element) { val simpleNameExpression = when (element) {
@@ -342,5 +343,28 @@ private class ExperimentalImportUsage : CoroutineMigrationProblem {
} }
} }
} }
private class Binding(
val bindTo: FqName,
val shouldRemove: Boolean,
val importDirective: KtImportDirective
)
} }
private object ObsoleteCoroutinesImportsUsage : ObsoleteImportsUsage() {
override val textMarker: String = "experimental"
override val packageBindings: Map<String, String> = mapOf(
"kotlinx.coroutines.experimental" to "kotlinx.coroutines",
"kotlin.coroutines.experimental" to "kotlin.coroutines"
)
override val importsToRemove: Set<String> = setOf(
"kotlin.coroutines.experimental.buildSequence",
"kotlin.coroutines.experimental.buildIterator"
)
override val wholeProjectFix: LocalQuickFix = ObsoleteCoroutineUsageInWholeProjectFix
override fun problemMessage(): String = KotlinBundle.message("experimental.coroutines.usages.are.obsolete.since.1.3")
override fun wrapFix(fix: ObsoleteCodeFix): LocalQuickFix = ObsoleteCoroutinesDelegateQuickFix(fix)
} }
+1 -1
View File
@@ -6,7 +6,7 @@ org.jetbrains.kotlin.idea.core.overrideImplement.ImplementMembersHandler
org.jetbrains.kotlin.idea.inspections.ConflictingExtensionPropertyInspection$DeleteRedundantExtensionAction org.jetbrains.kotlin.idea.inspections.ConflictingExtensionPropertyInspection$DeleteRedundantExtensionAction
org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection$HighPriorityIntentionBasedQuickFix org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection$HighPriorityIntentionBasedQuickFix
org.jetbrains.kotlin.idea.inspections.KotlinUnusedImportInspection$OptimizeImportsQuickFix org.jetbrains.kotlin.idea.inspections.KotlinUnusedImportInspection$OptimizeImportsQuickFix
org.jetbrains.kotlin.idea.inspections.migration.ObsoleteCoroutineUsageFix org.jetbrains.kotlin.idea.inspections.migration.ObsoleteCoroutinesDelegateQuickFix
org.jetbrains.kotlin.idea.inspections.SafeDeleteFix org.jetbrains.kotlin.idea.inspections.SafeDeleteFix
org.jetbrains.kotlin.idea.inspections.SuspiciousCollectionReassignmentInspection$ChangeTypeToMutableFix org.jetbrains.kotlin.idea.inspections.SuspiciousCollectionReassignmentInspection$ChangeTypeToMutableFix
org.jetbrains.kotlin.idea.inspections.SuspiciousCollectionReassignmentInspection$JoinWithInitializerFix org.jetbrains.kotlin.idea.inspections.SuspiciousCollectionReassignmentInspection$JoinWithInitializerFix