Add inspection for not awaited coroutines (PY-17292)

This commit is contained in:
Elizaveta Shashkova
2018-06-07 20:17:02 +03:00
parent 7a1bd912f2
commit 94c9f9b818
27 changed files with 395 additions and 6 deletions
@@ -0,0 +1,5 @@
<html>
<body>
This inspection highlights coroutines which were called without await
</body>
</html>
@@ -430,6 +430,7 @@
<localInspection language="Python" shortName="PyDataclassInspection" suppressId="PyDataclass" displayName="Dataclass definition and usages" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyDataclassInspection"/>
<localInspection language="Python" shortName="PyProtocolInspection" suppressId="PyProtocol" displayName="Protocol definition and usages" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyProtocolInspection"/>
<localInspection language="Python" shortName="PyTypeHintsInspection" suppressId="PyTypeHints" displayName="Type hints definitions and usages" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyTypeHintsInspection"/>
<localInspection language="Python" shortName="PyAsyncCallInspection" suppressId="PyAsyncCall" displayName="Coroutine is not awaited" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyAsyncCallInspection"/>
<defaultLiveTemplatesProvider implementation="com.jetbrains.python.codeInsight.liveTemplates.PyDefaultLiveTemplatesProvider"/>
<liveTemplateContext implementation="com.jetbrains.python.codeInsight.liveTemplates.PythonTemplateContextType$General"/>
@@ -603,6 +603,8 @@ INSP.NAME.global.$0.undefined=Global variable ''{0}'' is undefined at the module
INSP.NAME.assignment.to.loop.or.with.parameter.display.name=Assignment to 'for' loop or 'with' statement parameter
INSP.NAME.assignment.to.loop.or.with.parameter.display.message=Variable ''{0}'' already declared in ''for'' loop or ''with'' statement above
#PyAsyncCallInspection
INSP.NAME.coroutine.is.not.awaited=Coroutine ''{0}'' is not awaited
# PyTestParametrizedInspection
INSP.NAME.pytest-parametrized=Checks that functions decorated by pytest parametrize have correct arguments
@@ -0,0 +1,104 @@
// Copyright 2000-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 file.
package com.jetbrains.python.inspections
import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElementVisitor
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyNames
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.resolve.PyResolveContext
import com.jetbrains.python.psi.types.PyABCUtil
import com.jetbrains.python.psi.types.PyClassType
import com.jetbrains.python.psi.types.TypeEvalContext
import com.jetbrains.python.refactoring.PyReplaceExpressionUtil
class PyAsyncCallInspection : PyInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
return Visitor(holder, session)
}
private class Visitor(holder: ProblemsHolder, session: LocalInspectionToolSession) : PyInspectionVisitor(holder, session) {
override fun visitPyExpressionStatement(node: PyExpressionStatement) {
val expr = node.expression
if (expr is PyCallExpression && isAwaitableCall(expr)) {
val awaitableType = when {
isOuterFunctionAsync(expr) -> AwaitableType.AWAITABLE
isOuterFunctionCoroutine(expr, myTypeEvalContext) -> AwaitableType.COROUTINE
else -> return
}
// no need to check whether await or yield from is missed, because it's PyCallExpression already, not PyPrefixExpression
val functionName = getCalledCoroutineName(expr, resolveContext) ?: return
registerProblem(node, PyBundle.message("INSP.NAME.coroutine.is.not.awaited", functionName),
PyAddAwaitCallForCoroutineFix(awaitableType))
}
}
private fun isAwaitableCall(callExpr: PyCallExpression): Boolean {
val type = myTypeEvalContext.getType(callExpr) ?: return false
return PyABCUtil.isSubtype(type, PyNames.AWAITABLE, myTypeEvalContext) ||
type is PyClassType &&
PyTypingTypeProvider.GENERATOR == type.classQName &&
PyKnownDecoratorUtil.isResolvedToGeneratorBasedCoroutine(callExpr, resolveContext, myTypeEvalContext)
}
}
companion object {
enum class AwaitableType {
AWAITABLE, COROUTINE
}
const val coroutineIsNotAwaited = "Coroutine is not awaited"
private fun getCalledCoroutineName(callExpression: PyCallExpression, resolveContext: PyResolveContext): String? {
val callee = callExpression.callee as? PyReferenceExpression ?: return null
val function = callExpression.multiResolveCalleeFunction(resolveContext).firstOrNull() as? PyFunction ?: return null
return if (function.name == PyNames.INIT) callee.name else function.name
}
fun isOuterFunctionAsync(node: PyExpression): Boolean {
return (ScopeUtil.getScopeOwner(node) as? PyFunction)?.isAsync ?: false
}
private fun isOuterFunctionCoroutine(node: PyExpression, typeEvalContext: TypeEvalContext): Boolean {
val pyFunction = (ScopeUtil.getScopeOwner(node) as? PyFunction) ?: return false
return PyKnownDecoratorUtil.hasGeneratorBasedCoroutineDecorator(pyFunction, typeEvalContext)
}
}
private class PyAddAwaitCallForCoroutineFix(val type: AwaitableType) : LocalQuickFix {
override fun getFamilyName() = coroutineIsNotAwaited
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val psiElement = descriptor.psiElement
if (psiElement is PyExpressionStatement) {
val sb = StringBuilder()
when (type) {
AwaitableType.AWAITABLE -> {
sb.append("""async def foo():
| """.trimMargin()).append(PyNames.AWAIT).append(" ").append(psiElement.text)
}
AwaitableType.COROUTINE -> {
sb.append("""def foo():
| """.trimMargin()).append(PyNames.YIELD).append(" ").append(PyNames.FROM).append(" ")
.append(psiElement.text)
}
}
val generator = PyElementGenerator.getInstance(project)
val function = generator.createFromText(LanguageLevel.forElement(psiElement), PyFunction::class.java, sb.toString())
val awaitedStatement = function.statementList.statements.firstOrNull()
if (awaitedStatement != null) {
PyReplaceExpressionUtil.replaceExpression(psiElement, awaitedStatement)
}
}
}
}
}
@@ -877,12 +877,7 @@ public class PyUnresolvedReferencesInspection extends PyInspection {
final PyExpression receiver = ((PyOperatorReference)reference).getReceiver();
if (receiver instanceof PyCallExpression) {
final boolean resolvedToGeneratorBasedCoroutine = StreamEx
.of(((PyCallExpression)receiver).multiResolveCalleeFunction(getResolveContext()))
.select(PyFunction.class)
.anyMatch(function -> PyKnownDecoratorUtil.hasGeneratorBasedCoroutineDecorator(function, myTypeEvalContext));
if (resolvedToGeneratorBasedCoroutine) return true;
return PyKnownDecoratorUtil.isResolvedToGeneratorBasedCoroutine((PyCallExpression)receiver, getResolveContext(), myTypeEvalContext);
}
}
@@ -6,6 +6,7 @@ import com.intellij.psi.PsiReference;
import com.intellij.psi.util.QualifiedName;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.types.TypeEvalContext;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
@@ -210,6 +211,15 @@ public class PyKnownDecoratorUtil {
return ContainerUtil.exists(getKnownDecorators(function, context), GENERATOR_BASED_COROUTINE_DECORATORS::contains);
}
public static boolean isResolvedToGeneratorBasedCoroutine(@NotNull PyCallExpression receiver,
@NotNull PyResolveContext resolveContext,
@NotNull TypeEvalContext typeEvalContext) {
return StreamEx
.of((receiver).multiResolveCalleeFunction(resolveContext))
.select(PyFunction.class)
.anyMatch(function -> hasGeneratorBasedCoroutineDecorator(function, typeEvalContext));
}
public static boolean hasRedeclarationDecorator(@NotNull PyFunction function, @NotNull TypeEvalContext context) {
return getKnownDecorators(function, context).contains(TYPING_OVERLOAD);
}
@@ -97,6 +97,9 @@ public class PyABCUtil {
if (PyNames.ASYNC_ITERABLE.equals(superClassName)) {
return hasMethod(subClass, PyNames.AITER, inherited, context);
}
if (PyNames.AWAITABLE.equals(superClassName)) {
return hasMethod(subClass, PyNames.DUNDER_AWAIT, inherited, context);
}
if (PyNames.PATH_LIKE.equals(superClassName)) {
return hasMethod(subClass, PyNames.FSPATH, inherited, context);
}
@@ -0,0 +1,15 @@
import asyncio
@asyncio.coroutine
def bar():
yield from asyncio.sleep(1)
@asyncio.coroutine
def check():
<warning descr="Coroutine 'bar' is not awaited">bar()</warning>
async def check():
<warning descr="Coroutine 'bar' is not awaited">bar()</warning>
@@ -0,0 +1,5 @@
from .coroutines import *
def sleep(s):
pass
@@ -0,0 +1,2 @@
def coroutine(fn):
pass
@@ -0,0 +1,22 @@
import asyncio
@asyncio.coroutine
def foo():
return 24
@asyncio.coroutine
def baz():
yield from foo()
@asyncio.coroutine
def gen():
return foo()
@asyncio.coroutine
def wrap(co):
res = yield from co
return res
@@ -0,0 +1,5 @@
from .coroutines import *
def sleep(s):
pass
@@ -0,0 +1,2 @@
def coroutine(fn):
pass
@@ -0,0 +1,14 @@
import types
def foo():
return 23
@types.coroutine
def bar():
yield from foo()
async def check():
<warning descr="Coroutine 'bar' is not awaited">bar()</warning>
@@ -0,0 +1,2 @@
def coroutine(fn):
pass
@@ -0,0 +1,9 @@
async def bar():
return "hey"
async def foo():
<warning descr="Coroutine 'bar' is not awaited">bar()</warning>
return True
@@ -0,0 +1,38 @@
import asyncio
async def foo():
return 24
async def with_await():
await foo()
async def gen():
return foo()
async def wrap(co):
await_co = await co
return await_co
async def baz():
cor = foo()
return await wrap(cor)
async def wrap_twice(co):
await_co = await co
return await await_co
async def bar():
return await wrap_twice(gen())
loop = asyncio.get_event_loop()
loop.run_until_complete(bar())
loop.run_until_complete(baz())
loop.close()
@@ -0,0 +1,10 @@
import asyncio
class FutureLike:
def __await__(self):
yield from asyncio.sleep(2)
async def foo():
<warning descr="Coroutine 'FutureLike' is not awaited">FutureLike()</warning>
@@ -0,0 +1,9 @@
async def bar():
return "hey"
async def foo():
<warning descr="Coroutine 'bar' is not awaited"><caret>bar()</warning>
return True
@@ -0,0 +1,9 @@
async def bar():
return "hey"
async def foo():
await bar()
return True
@@ -0,0 +1,13 @@
import asyncio
@asyncio.coroutine
def bar():
yield from asyncio.sleep(2)
return "hey"
@asyncio.coroutine
def foo():
<caret>bar()
return True
@@ -0,0 +1,13 @@
import asyncio
@asyncio.coroutine
def bar():
yield from asyncio.sleep(2)
return "hey"
@asyncio.coroutine
def foo():
yield from bar()
return True
@@ -0,0 +1,5 @@
from .coroutines import *
def sleep(s):
pass
@@ -0,0 +1,2 @@
def coroutine(fn):
pass
@@ -0,0 +1,65 @@
// Copyright 2000-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 file.
package com.jetbrains.python.inspections;
import com.intellij.testFramework.LightProjectDescriptor;
import com.jetbrains.python.fixtures.PyInspectionTestCase;
import com.jetbrains.python.psi.LanguageLevel;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class PyAsyncCallInspectionTest extends PyInspectionTestCase {
// PY-17292
public void testAsyncFromAsyncCall() {
doTest();
}
// PY-17292
public void testCorrectAsyncioCorCall() {
doMultiFileTest("a.py");
}
// PY-17292
public void testTypesCorFromAsyncCall() {
doMultiFileTest("a.py");
}
// PY-17292
public void testAsyncioCorFromAsyncioCorCall() {
doMultiFileTest("a.py");
}
// PY-17292
public void testFutureLikeFromAsyncCall() {
doTest();
}
// PY-17292
public void testCorrectCalls() {
doTest();
}
@Override
protected void doMultiFileTest(@NotNull String filename) {
runWithLanguageLevel(LanguageLevel.PYTHON35, () -> super.doMultiFileTest(filename));
}
@Override
protected void doTest() {
runWithLanguageLevel(LanguageLevel.PYTHON35, () -> super.doTest());
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {
return PyAsyncCallInspection.class;
}
@Nullable
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return ourPy3Descriptor;
}
}
@@ -0,0 +1,29 @@
// Copyright 2000-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 file.
package com.jetbrains.python.quickFixes;
import com.intellij.testFramework.LightProjectDescriptor;
import com.jetbrains.python.PyQuickFixTestCase;
import com.jetbrains.python.inspections.PyAsyncCallInspection;
import com.jetbrains.python.psi.LanguageLevel;
import org.jetbrains.annotations.Nullable;
public class PyAsyncCallQuickFixTest extends PyQuickFixTestCase {
// PY-17292
public void testAddAwaitBeforeCall() {
doQuickFixTest(PyAsyncCallInspection.class, PyAsyncCallInspection.coroutineIsNotAwaited, LanguageLevel.PYTHON35);
}
// PY-17292
public void testAddYieldFromBeforeCall() {
runWithLanguageLevel(LanguageLevel.PYTHON35,
() -> doMultifilesTest(PyAsyncCallInspection.class, PyAsyncCallInspection.coroutineIsNotAwaited,
new String[]{"addYieldFromBeforeCall.py", "asyncio/__init__.py", "asyncio/coroutines.py"}));
}
@Nullable
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return ourPy3Descriptor;
}
}