From 13be7803bb7962ef1ef27b7238f1864d34f77c68 Mon Sep 17 00:00:00 2001 From: Mikhail Glukhikh Date: Fri, 11 May 2018 15:01:05 +0300 Subject: [PATCH] Introduce "redundant async" inspection #KT-24235 Fixed --- .../RedundantAsync.html | 6 ++ idea/src/META-INF/plugin.xml | 9 +++ idea/src/META-INF/plugin.xml.172 | 9 +++ idea/src/META-INF/plugin.xml.173 | 9 +++ idea/src/META-INF/plugin.xml.182 | 9 +++ idea/src/META-INF/plugin.xml.as31 | 9 +++ idea/src/META-INF/plugin.xml.as32 | 9 +++ .../collections/SimplifyCallChainFix.kt | 4 +- .../coroutines/RedundantAsyncInspection.kt | 63 +++++++++++++++++++ .../coroutines/redundantAsync/.inspection | 1 + .../coroutines/redundantAsync/simple.kt | 48 ++++++++++++++ .../coroutines/redundantAsync/simple.kt.after | 48 ++++++++++++++ .../coroutines/redundantAsync/simplest.kt | 48 ++++++++++++++ .../redundantAsync/simplest.kt.after | 48 ++++++++++++++ .../coroutines/redundantAsync/withParent.kt | 49 +++++++++++++++ .../redundantAsync/withStartAndContext.kt | 48 ++++++++++++++ .../withStartAndContext.kt.after | 48 ++++++++++++++ .../redundantAsync/withStartNoContext.kt | 49 +++++++++++++++ .../LocalInspectionTestGenerated.java | 51 +++++++++++++++ 19 files changed, 564 insertions(+), 1 deletion(-) create mode 100644 idea/resources/inspectionDescriptions/RedundantAsync.html create mode 100644 idea/src/org/jetbrains/kotlin/idea/inspections/coroutines/RedundantAsyncInspection.kt create mode 100644 idea/testData/inspectionsLocal/coroutines/redundantAsync/.inspection create mode 100644 idea/testData/inspectionsLocal/coroutines/redundantAsync/simple.kt create mode 100644 idea/testData/inspectionsLocal/coroutines/redundantAsync/simple.kt.after create mode 100644 idea/testData/inspectionsLocal/coroutines/redundantAsync/simplest.kt create mode 100644 idea/testData/inspectionsLocal/coroutines/redundantAsync/simplest.kt.after create mode 100644 idea/testData/inspectionsLocal/coroutines/redundantAsync/withParent.kt create mode 100644 idea/testData/inspectionsLocal/coroutines/redundantAsync/withStartAndContext.kt create mode 100644 idea/testData/inspectionsLocal/coroutines/redundantAsync/withStartAndContext.kt.after create mode 100644 idea/testData/inspectionsLocal/coroutines/redundantAsync/withStartNoContext.kt diff --git a/idea/resources/inspectionDescriptions/RedundantAsync.html b/idea/resources/inspectionDescriptions/RedundantAsync.html new file mode 100644 index 00000000000..80fc8d5af2f --- /dev/null +++ b/idea/resources/inspectionDescriptions/RedundantAsync.html @@ -0,0 +1,6 @@ + + +This inspection reports async call that is immediately followed by await. +Such a call can be replaced with a kind of blocking call. + + \ No newline at end of file diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 0613d807c03..d6c0e4a9726 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -2839,6 +2839,15 @@ language="kotlin" /> + + diff --git a/idea/src/META-INF/plugin.xml.172 b/idea/src/META-INF/plugin.xml.172 index 81376c12a6c..517a196ea2b 100644 --- a/idea/src/META-INF/plugin.xml.172 +++ b/idea/src/META-INF/plugin.xml.172 @@ -2834,6 +2834,15 @@ language="kotlin" /> + + diff --git a/idea/src/META-INF/plugin.xml.173 b/idea/src/META-INF/plugin.xml.173 index 1a34f7f8814..11742499373 100644 --- a/idea/src/META-INF/plugin.xml.173 +++ b/idea/src/META-INF/plugin.xml.173 @@ -2834,6 +2834,15 @@ language="kotlin" /> + + diff --git a/idea/src/META-INF/plugin.xml.182 b/idea/src/META-INF/plugin.xml.182 index 564f726effd..2b91d6afc02 100644 --- a/idea/src/META-INF/plugin.xml.182 +++ b/idea/src/META-INF/plugin.xml.182 @@ -2840,6 +2840,15 @@ language="kotlin" /> + + diff --git a/idea/src/META-INF/plugin.xml.as31 b/idea/src/META-INF/plugin.xml.as31 index c5eb6b4a718..820c3d58d40 100644 --- a/idea/src/META-INF/plugin.xml.as31 +++ b/idea/src/META-INF/plugin.xml.as31 @@ -2834,6 +2834,15 @@ language="kotlin" /> + + diff --git a/idea/src/META-INF/plugin.xml.as32 b/idea/src/META-INF/plugin.xml.as32 index 67d9a2ea248..b9d588cd0b1 100644 --- a/idea/src/META-INF/plugin.xml.as32 +++ b/idea/src/META-INF/plugin.xml.as32 @@ -2834,6 +2834,15 @@ language="kotlin" /> + + diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/collections/SimplifyCallChainFix.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/collections/SimplifyCallChainFix.kt index 882c6608454..1ed03ccfb8c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/inspections/collections/SimplifyCallChainFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/collections/SimplifyCallChainFix.kt @@ -19,6 +19,7 @@ package org.jetbrains.kotlin.idea.inspections.collections import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemDescriptor import com.intellij.openapi.project.Project +import org.jetbrains.kotlin.idea.core.ShortenReferences import org.jetbrains.kotlin.idea.core.replaced import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty @@ -66,7 +67,8 @@ class SimplifyCallChainFix(val newName: String) : LocalQuickFix { argumentsText ) - secondQualifiedExpression.replaced(newQualifiedExpression) + val result = secondQualifiedExpression.replaced(newQualifiedExpression) + ShortenReferences.DEFAULT.process(result) } } } \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/coroutines/RedundantAsyncInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/coroutines/RedundantAsyncInspection.kt new file mode 100644 index 00000000000..87ea5e995a8 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/coroutines/RedundantAsyncInspection.kt @@ -0,0 +1,63 @@ +/* + * 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.coroutines + +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import org.jetbrains.kotlin.idea.inspections.collections.AbstractCallChainChecker +import org.jetbrains.kotlin.idea.inspections.collections.SimplifyCallChainFix +import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.psi.qualifiedExpressionVisitor +import org.jetbrains.kotlin.resolve.calls.model.DefaultValueArgument + +class RedundantAsyncInspection : AbstractCallChainChecker() { + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = + qualifiedExpressionVisitor(fun(expression) { + var defaultContext: Boolean? = null + var defaultStart: Boolean? = null + var defaultParent: Boolean? = null + val conversion = findQualifiedConversion(expression, conversionGroups) check@{ _, firstResolvedCall, _, _ -> + for ((parameterDescriptor, valueArgument) in firstResolvedCall.valueArguments) { + val default = valueArgument is DefaultValueArgument + when (parameterDescriptor.name.asString()) { + "context" -> defaultContext = default + "start" -> defaultStart = default + "parent" -> defaultParent = default + } + } + true + } ?: return + defaultContext ?: return + defaultStart ?: return + if (defaultParent != true) return + if (defaultContext!! && !defaultStart!!) return + + val replacement = + if (defaultContext!! && defaultStart!!) "kotlinx.coroutines.experimental.runBlocking" else conversion.replacement + val descriptor = holder.manager.createProblemDescriptor( + expression, + expression.firstCalleeExpression()!!.textRange.shiftRight(-expression.startOffset), + "Redundant 'async' call may be reduced to '$replacement'", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + isOnTheFly, + SimplifyCallChainFix(replacement) + ) + holder.registerProblem(descriptor) + }) + + private val conversionGroups = conversions.group() + + companion object { + + private val conversions = listOf( + Conversion( + "kotlinx.coroutines.experimental.async", + "kotlinx.coroutines.experimental.Deferred.await", + "kotlinx.coroutines.experimental.withContext" + ) + ) + } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/coroutines/redundantAsync/.inspection b/idea/testData/inspectionsLocal/coroutines/redundantAsync/.inspection new file mode 100644 index 00000000000..4e59c34818d --- /dev/null +++ b/idea/testData/inspectionsLocal/coroutines/redundantAsync/.inspection @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.inspections.coroutines.RedundantAsyncInspection diff --git a/idea/testData/inspectionsLocal/coroutines/redundantAsync/simple.kt b/idea/testData/inspectionsLocal/coroutines/redundantAsync/simple.kt new file mode 100644 index 00000000000..a0cff42a150 --- /dev/null +++ b/idea/testData/inspectionsLocal/coroutines/redundantAsync/simple.kt @@ -0,0 +1,48 @@ +// WITH_RUNTIME + +package kotlinx.coroutines.experimental + +interface Deferred { + suspend fun await(): T +} + +interface CoroutineContext + +object DefaultContext : CoroutineContext + +enum class CoroutineStart { + DEFAULT, + LAZY, + ATOMIC, + UNDISPATCHED +} + +interface Job + +fun async( + context: CoroutineContext = DefaultContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + parent: Job? = null, + f: suspend () -> T +): Deferred { + TODO() +} + +fun runBlocking( + context: CoroutineContext = DefaultContext, + f: suspend () -> T +) { + TODO() +} + +suspend fun withContext( + context: CoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + f: suspend () -> T +) { + TODO() +} + +suspend fun test(ctx: CoroutineContext) { + async(ctx) { 42 }.await() +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/coroutines/redundantAsync/simple.kt.after b/idea/testData/inspectionsLocal/coroutines/redundantAsync/simple.kt.after new file mode 100644 index 00000000000..ea3468a8619 --- /dev/null +++ b/idea/testData/inspectionsLocal/coroutines/redundantAsync/simple.kt.after @@ -0,0 +1,48 @@ +// WITH_RUNTIME + +package kotlinx.coroutines.experimental + +interface Deferred { + suspend fun await(): T +} + +interface CoroutineContext + +object DefaultContext : CoroutineContext + +enum class CoroutineStart { + DEFAULT, + LAZY, + ATOMIC, + UNDISPATCHED +} + +interface Job + +fun async( + context: CoroutineContext = DefaultContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + parent: Job? = null, + f: suspend () -> T +): Deferred { + TODO() +} + +fun runBlocking( + context: CoroutineContext = DefaultContext, + f: suspend () -> T +) { + TODO() +} + +suspend fun withContext( + context: CoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + f: suspend () -> T +) { + TODO() +} + +suspend fun test(ctx: CoroutineContext) { + withContext(ctx) { 42 } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/coroutines/redundantAsync/simplest.kt b/idea/testData/inspectionsLocal/coroutines/redundantAsync/simplest.kt new file mode 100644 index 00000000000..61372604ada --- /dev/null +++ b/idea/testData/inspectionsLocal/coroutines/redundantAsync/simplest.kt @@ -0,0 +1,48 @@ +// WITH_RUNTIME + +package kotlinx.coroutines.experimental + +interface Deferred { + suspend fun await(): T +} + +interface CoroutineContext + +object DefaultContext : CoroutineContext + +enum class CoroutineStart { + DEFAULT, + LAZY, + ATOMIC, + UNDISPATCHED +} + +interface Job + +fun async( + context: CoroutineContext = DefaultContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + parent: Job? = null, + f: suspend () -> T +): Deferred { + TODO() +} + +fun runBlocking( + context: CoroutineContext = DefaultContext, + f: suspend () -> T +) { + TODO() +} + +suspend fun withContext( + context: CoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + f: suspend () -> T +) { + TODO() +} + +suspend fun test() { + async { 42 }.await() +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/coroutines/redundantAsync/simplest.kt.after b/idea/testData/inspectionsLocal/coroutines/redundantAsync/simplest.kt.after new file mode 100644 index 00000000000..d6bf79b72bf --- /dev/null +++ b/idea/testData/inspectionsLocal/coroutines/redundantAsync/simplest.kt.after @@ -0,0 +1,48 @@ +// WITH_RUNTIME + +package kotlinx.coroutines.experimental + +interface Deferred { + suspend fun await(): T +} + +interface CoroutineContext + +object DefaultContext : CoroutineContext + +enum class CoroutineStart { + DEFAULT, + LAZY, + ATOMIC, + UNDISPATCHED +} + +interface Job + +fun async( + context: CoroutineContext = DefaultContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + parent: Job? = null, + f: suspend () -> T +): Deferred { + TODO() +} + +fun runBlocking( + context: CoroutineContext = DefaultContext, + f: suspend () -> T +) { + TODO() +} + +suspend fun withContext( + context: CoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + f: suspend () -> T +) { + TODO() +} + +suspend fun test() { + runBlocking { 42 } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/coroutines/redundantAsync/withParent.kt b/idea/testData/inspectionsLocal/coroutines/redundantAsync/withParent.kt new file mode 100644 index 00000000000..7759437c987 --- /dev/null +++ b/idea/testData/inspectionsLocal/coroutines/redundantAsync/withParent.kt @@ -0,0 +1,49 @@ +// PROBLEM: none +// WITH_RUNTIME + +package kotlinx.coroutines.experimental + +interface Deferred { + suspend fun await(): T +} + +interface CoroutineContext + +object DefaultContext : CoroutineContext + +enum class CoroutineStart { + DEFAULT, + LAZY, + ATOMIC, + UNDISPATCHED +} + +interface Job + +fun async( + context: CoroutineContext = DefaultContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + parent: Job? = null, + f: suspend () -> T +): Deferred { + TODO() +} + +fun runBlocking( + context: CoroutineContext = DefaultContext, + f: suspend () -> T +) { + TODO() +} + +suspend fun withContext( + context: CoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + f: suspend () -> T +) { + TODO() +} + +suspend fun test(ctx: CoroutineContext) { + async(parent = null) { 42 }.await() +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/coroutines/redundantAsync/withStartAndContext.kt b/idea/testData/inspectionsLocal/coroutines/redundantAsync/withStartAndContext.kt new file mode 100644 index 00000000000..fac0fcac8b7 --- /dev/null +++ b/idea/testData/inspectionsLocal/coroutines/redundantAsync/withStartAndContext.kt @@ -0,0 +1,48 @@ +// WITH_RUNTIME + +package kotlinx.coroutines.experimental + +interface Deferred { + suspend fun await(): T +} + +interface CoroutineContext + +object DefaultContext : CoroutineContext + +enum class CoroutineStart { + DEFAULT, + LAZY, + ATOMIC, + UNDISPATCHED +} + +interface Job + +fun async( + context: CoroutineContext = DefaultContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + parent: Job? = null, + f: suspend () -> T +): Deferred { + TODO() +} + +fun runBlocking( + context: CoroutineContext = DefaultContext, + f: suspend () -> T +) { + TODO() +} + +suspend fun withContext( + context: CoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + f: suspend () -> T +) { + TODO() +} + +suspend fun test(ctx: CoroutineContext) { + async(ctx, CoroutineStart.LAZY) { 42 }.await() +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/coroutines/redundantAsync/withStartAndContext.kt.after b/idea/testData/inspectionsLocal/coroutines/redundantAsync/withStartAndContext.kt.after new file mode 100644 index 00000000000..f9c2bb60107 --- /dev/null +++ b/idea/testData/inspectionsLocal/coroutines/redundantAsync/withStartAndContext.kt.after @@ -0,0 +1,48 @@ +// WITH_RUNTIME + +package kotlinx.coroutines.experimental + +interface Deferred { + suspend fun await(): T +} + +interface CoroutineContext + +object DefaultContext : CoroutineContext + +enum class CoroutineStart { + DEFAULT, + LAZY, + ATOMIC, + UNDISPATCHED +} + +interface Job + +fun async( + context: CoroutineContext = DefaultContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + parent: Job? = null, + f: suspend () -> T +): Deferred { + TODO() +} + +fun runBlocking( + context: CoroutineContext = DefaultContext, + f: suspend () -> T +) { + TODO() +} + +suspend fun withContext( + context: CoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + f: suspend () -> T +) { + TODO() +} + +suspend fun test(ctx: CoroutineContext) { + withContext(ctx, CoroutineStart.LAZY) { 42 } +} \ No newline at end of file diff --git a/idea/testData/inspectionsLocal/coroutines/redundantAsync/withStartNoContext.kt b/idea/testData/inspectionsLocal/coroutines/redundantAsync/withStartNoContext.kt new file mode 100644 index 00000000000..5fc9d46d605 --- /dev/null +++ b/idea/testData/inspectionsLocal/coroutines/redundantAsync/withStartNoContext.kt @@ -0,0 +1,49 @@ +// PROBLEM: none +// WITH_RUNTIME + +package kotlinx.coroutines.experimental + +interface Deferred { + suspend fun await(): T +} + +interface CoroutineContext + +object DefaultContext : CoroutineContext + +enum class CoroutineStart { + DEFAULT, + LAZY, + ATOMIC, + UNDISPATCHED +} + +interface Job + +fun async( + context: CoroutineContext = DefaultContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + parent: Job? = null, + f: suspend () -> T +): Deferred { + TODO() +} + +fun runBlocking( + context: CoroutineContext = DefaultContext, + f: suspend () -> T +) { + TODO() +} + +suspend fun withContext( + context: CoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + f: suspend () -> T +) { + TODO() +} + +suspend fun test(ctx: CoroutineContext) { + async(start = CoroutineStart.LAZY) { 42 }.await() +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java index ca59ffaf2d4..2467ef82db3 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/inspections/LocalInspectionTestGenerated.java @@ -1226,6 +1226,57 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest { } } + @TestMetadata("idea/testData/inspectionsLocal/coroutines") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class Coroutines extends AbstractLocalInspectionTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath); + } + + public void testAllFilesPresentInCoroutines() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/coroutines"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true); + } + + @TestMetadata("idea/testData/inspectionsLocal/coroutines/redundantAsync") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class RedundantAsync extends AbstractLocalInspectionTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath); + } + + public void testAllFilesPresentInRedundantAsync() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/coroutines/redundantAsync"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true); + } + + @TestMetadata("simple.kt") + public void testSimple() throws Exception { + runTest("idea/testData/inspectionsLocal/coroutines/redundantAsync/simple.kt"); + } + + @TestMetadata("simplest.kt") + public void testSimplest() throws Exception { + runTest("idea/testData/inspectionsLocal/coroutines/redundantAsync/simplest.kt"); + } + + @TestMetadata("withParent.kt") + public void testWithParent() throws Exception { + runTest("idea/testData/inspectionsLocal/coroutines/redundantAsync/withParent.kt"); + } + + @TestMetadata("withStartAndContext.kt") + public void testWithStartAndContext() throws Exception { + runTest("idea/testData/inspectionsLocal/coroutines/redundantAsync/withStartAndContext.kt"); + } + + @TestMetadata("withStartNoContext.kt") + public void testWithStartNoContext() throws Exception { + runTest("idea/testData/inspectionsLocal/coroutines/redundantAsync/withStartNoContext.kt"); + } + } + } + @TestMetadata("idea/testData/inspectionsLocal/deprecatedCallableAddReplaceWith") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)