mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Add inspection for not awaited coroutines (PY-17292)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-6
@@ -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>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
from .coroutines import *
|
||||
|
||||
|
||||
def sleep(s):
|
||||
pass
|
||||
+2
@@ -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
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
from .coroutines import *
|
||||
|
||||
|
||||
def sleep(s):
|
||||
pass
|
||||
+2
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user