mirror of https://github.com/astral-sh/ruff
Implement missing await for coroutine `RUF065`
This commit is contained in:
parent
bfb0902446
commit
c790c1d957
|
|
@ -0,0 +1,179 @@
|
|||
import asyncio
|
||||
|
||||
# Violation cases: RUF065
|
||||
|
||||
|
||||
async def test_coroutine_without_await():
|
||||
async def coro():
|
||||
pass
|
||||
|
||||
coro() # RUF065
|
||||
|
||||
|
||||
async def test_coroutine_without_await():
|
||||
async def coro():
|
||||
pass
|
||||
|
||||
result = coro() # RUF065
|
||||
|
||||
|
||||
async def test_coroutine_without_await():
|
||||
def not_coro():
|
||||
pass
|
||||
|
||||
async def coro():
|
||||
pass
|
||||
|
||||
not_coro()
|
||||
coro() # RUF065
|
||||
|
||||
|
||||
async def test_coroutine_without_await():
|
||||
async def coro():
|
||||
another_coro() # RUF065
|
||||
|
||||
async def another_coro():
|
||||
pass
|
||||
|
||||
await coro()
|
||||
|
||||
|
||||
async def test_asyncio_api_without_await():
|
||||
asyncio.sleep(0.5) # RUF065
|
||||
|
||||
|
||||
async def test_asyncio_api_without_await():
|
||||
async def coro():
|
||||
asyncio.sleep(0.5) # RUF065
|
||||
|
||||
await asyncio.wait(coro)
|
||||
|
||||
|
||||
async def test_asyncio_api_without_await():
|
||||
async def coro():
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
asyncio.wait_for(coro) # RUF065
|
||||
|
||||
|
||||
async def test_asyncio_api_without_await():
|
||||
async def coro1():
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def coro2():
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
tasks = [coro1(), coro2()]
|
||||
asyncio.gather(*tasks) # RUF065
|
||||
|
||||
|
||||
# Non-violation cases: RUF065
|
||||
|
||||
|
||||
async def test_coroutine_with_await():
|
||||
async def coro():
|
||||
pass
|
||||
|
||||
await coro() # OK
|
||||
|
||||
|
||||
async def test_coroutine_with_await():
|
||||
def not_coro():
|
||||
pass
|
||||
|
||||
async def coro():
|
||||
pass
|
||||
|
||||
not_coro()
|
||||
await coro() # OK
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
# define an asynchronous context manager
|
||||
class AsyncContextManager:
|
||||
# enter the async context manager
|
||||
async def __aenter__(self):
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
|
||||
# define a simple coroutine
|
||||
async def custom_coroutine():
|
||||
# create and use the asynchronous context manager
|
||||
async with AsyncContextManager(): # OK
|
||||
...
|
||||
|
||||
|
||||
async def test_coroutine_in_func_arg():
|
||||
async def another_coro():
|
||||
pass
|
||||
|
||||
async def coro(cr):
|
||||
await cr
|
||||
|
||||
await coro(another_coro()) # OK
|
||||
|
||||
|
||||
async def test_coroutine_with_yield():
|
||||
async def another_coro():
|
||||
pass
|
||||
|
||||
async def coro():
|
||||
yield another_coro()
|
||||
|
||||
await coro() # OK
|
||||
|
||||
|
||||
async def test_coroutine_with_return():
|
||||
async def another_coro():
|
||||
pass
|
||||
|
||||
async def coro():
|
||||
return another_coro()
|
||||
|
||||
await coro() # OK
|
||||
|
||||
|
||||
async def test_coroutine_with_async_iterator():
|
||||
class Counter:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
pass
|
||||
|
||||
async def main():
|
||||
async for c in Counter(): # OK
|
||||
pass
|
||||
|
||||
|
||||
async def test_asyncio_api_with_await():
|
||||
async def task_coro(value):
|
||||
await asyncio.sleep(1)
|
||||
return value * 10
|
||||
|
||||
# main coroutine
|
||||
async def main():
|
||||
awaitables = [task_coro(i) for i in range(10)]
|
||||
await asyncio.gather(*awaitables) # OK
|
||||
|
||||
|
||||
async def test_coroutine_inside_collections():
|
||||
async def coro():
|
||||
pass
|
||||
|
||||
[coro(), coro()] # OK
|
||||
(coro(), coro()) # OK
|
||||
{coro(), coro()} # OK
|
||||
{"coro": coro()} # OK
|
||||
|
||||
|
||||
async def test_func_used_in_arg_should_not_raise(func):
|
||||
func() # OK
|
||||
|
|
@ -1297,6 +1297,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
if checker.is_rule_enabled(Rule::NonOctalPermissions) {
|
||||
ruff::rules::non_octal_permissions(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::MissingAwaitForCoroutine) {
|
||||
ruff::rules::missing_await_for_coroutine(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::AssertRaisesException) {
|
||||
flake8_bugbear::rules::assert_raises_exception_call(checker, call);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1051,6 +1051,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises),
|
||||
(Ruff, "063") => (RuleGroup::Preview, rules::ruff::rules::AccessAnnotationsFromClassDict),
|
||||
(Ruff, "064") => (RuleGroup::Preview, rules::ruff::rules::NonOctalPermissions),
|
||||
(Ruff, "065") => (RuleGroup::Preview, rules::ruff::rules::MissingAwaitForCoroutine),
|
||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
|
||||
(Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode),
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ mod tests {
|
|||
#[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_warns.py"))]
|
||||
#[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_deprecated_call.py"))]
|
||||
#[test_case(Rule::NonOctalPermissions, Path::new("RUF064.py"))]
|
||||
#[test_case(Rule::MissingAwaitForCoroutine, Path::new("RUF065.py"))]
|
||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))]
|
||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))]
|
||||
#[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
use ruff_python_ast::{
|
||||
AtomicNodeIndex, Expr, ExprAwait, ExprCall, ExprName, Stmt, StmtAssign, StmtExpr,
|
||||
StmtFunctionDef,
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for coroutines that are not awaited. This rule is only active in async contexts.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Coroutines are not executed until they are awaited. If a coroutine is not awaited, it will
|
||||
/// not be executed, and the program will not behave as expected. This is a common mistake when
|
||||
/// using `asyncio.sleep` instead of `await asyncio.sleep`.
|
||||
///
|
||||
/// Python's asyncio runtime will emit a warning when a coroutine is not awaited.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// async def foo():
|
||||
/// pass
|
||||
///
|
||||
///
|
||||
/// async def bar():
|
||||
/// foo()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// async def foo():
|
||||
/// pass
|
||||
///
|
||||
///
|
||||
/// async def bar():
|
||||
/// await foo()
|
||||
/// ```
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct MissingAwaitForCoroutine;
|
||||
|
||||
impl Violation for MissingAwaitForCoroutine {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Coroutine is not awaited".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Coroutine is not awaited".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF065
|
||||
pub(crate) fn missing_await_for_coroutine(checker: &Checker, call: &ExprCall) {
|
||||
// Only check for missing await in async context
|
||||
if !checker.semantic().in_async_context() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to detect possible scenarios where await is missing and ignore other cases
|
||||
// For example, if the call is not a direct child of an statement expression or assignment statement
|
||||
// then it's not reliable to determine if await is missing.
|
||||
// User might return coroutine object from a function or pass it as an argument
|
||||
if !possibly_missing_await(call, checker.semantic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let is_awaitable = is_awaitable_from_asyncio(call.func.as_ref(), checker.semantic())
|
||||
|| is_awaitable_func(call.func.as_ref(), checker.semantic());
|
||||
|
||||
// If call does not originate from asyncio or is not an async function, then it's not awaitable
|
||||
if !is_awaitable {
|
||||
return;
|
||||
}
|
||||
|
||||
checker
|
||||
.report_diagnostic(MissingAwaitForCoroutine, call.range())
|
||||
.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
checker.generator().expr(&generate_fix(call)),
|
||||
call.range(),
|
||||
)));
|
||||
}
|
||||
|
||||
fn is_awaitable_from_asyncio(func: &Expr, semantic: &SemanticModel) -> bool {
|
||||
if let Some(call_path) = semantic.resolve_qualified_name(func) {
|
||||
return matches!(
|
||||
call_path.segments(),
|
||||
["asyncio", "sleep" | "wait" | "wait_for" | "gather"]
|
||||
);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_awaitable_func(func: &Expr, semantic: &SemanticModel) -> bool {
|
||||
let Expr::Name(ExprName { id, .. }) = func else {
|
||||
return false;
|
||||
};
|
||||
let Some(binding_id) = semantic.lookup_symbol(id) else {
|
||||
return false;
|
||||
};
|
||||
let binding = semantic.binding(binding_id);
|
||||
if let Some(node_id) = binding.source {
|
||||
let node = semantic.statement(node_id);
|
||||
if let Stmt::FunctionDef(StmtFunctionDef { is_async, name, .. }) = node {
|
||||
return *is_async && name.as_str() == id;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn possibly_missing_await(call: &ExprCall, semantic: &SemanticModel) -> bool {
|
||||
if let Stmt::Expr(StmtExpr { value, .. }) = semantic.current_statement() {
|
||||
if let Expr::Call(expr_call) = value.as_ref() {
|
||||
return expr_call == call;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Stmt::Assign(StmtAssign { value, .. })) = semantic.current_statement_parent() {
|
||||
if let Expr::Call(expr_call) = value.as_ref() {
|
||||
return expr_call == call;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Generate a [`Fix`] to add `await` for coroutine.
|
||||
///
|
||||
/// For example:
|
||||
/// - Given `asyncio.sleep(1)`, generate `await asyncio.sleep(1)`.
|
||||
fn generate_fix(call: &ExprCall) -> Expr {
|
||||
Expr::Await(ExprAwait {
|
||||
node_index: AtomicNodeIndex::default(),
|
||||
value: Box::new(Expr::Call(call.clone())),
|
||||
range: TextRange::default(),
|
||||
})
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ pub(crate) use invalid_pyproject_toml::*;
|
|||
pub(crate) use invalid_rule_code::*;
|
||||
pub(crate) use legacy_form_pytest_raises::*;
|
||||
pub(crate) use map_int_version_parsing::*;
|
||||
pub(crate) use missing_await_for_coroutine::*;
|
||||
pub(crate) use missing_fstring_syntax::*;
|
||||
pub(crate) use mutable_class_default::*;
|
||||
pub(crate) use mutable_dataclass_default::*;
|
||||
|
|
@ -87,6 +88,7 @@ mod invalid_pyproject_toml;
|
|||
mod invalid_rule_code;
|
||||
mod legacy_form_pytest_raises;
|
||||
mod map_int_version_parsing;
|
||||
mod missing_await_for_coroutine;
|
||||
mod missing_fstring_syntax;
|
||||
mod mutable_class_default;
|
||||
mod mutable_dataclass_default;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF065 [*] Coroutine is not awaited
|
||||
--> RUF065.py:10:5
|
||||
|
|
||||
8 | pass
|
||||
9 |
|
||||
10 | coro() # RUF065
|
||||
| ^^^^^^
|
||||
|
|
||||
help: Coroutine is not awaited
|
||||
7 | async def coro():
|
||||
8 | pass
|
||||
9 |
|
||||
- coro() # RUF065
|
||||
10 + await coro() # RUF065
|
||||
11 |
|
||||
12 |
|
||||
13 | async def test_coroutine_without_await():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF065 [*] Coroutine is not awaited
|
||||
--> RUF065.py:28:5
|
||||
|
|
||||
27 | not_coro()
|
||||
28 | coro() # RUF065
|
||||
| ^^^^^^
|
||||
|
|
||||
help: Coroutine is not awaited
|
||||
25 | pass
|
||||
26 |
|
||||
27 | not_coro()
|
||||
- coro() # RUF065
|
||||
28 + await coro() # RUF065
|
||||
29 |
|
||||
30 |
|
||||
31 | async def test_coroutine_without_await():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF065 [*] Coroutine is not awaited
|
||||
--> RUF065.py:33:9
|
||||
|
|
||||
31 | async def test_coroutine_without_await():
|
||||
32 | async def coro():
|
||||
33 | another_coro() # RUF065
|
||||
| ^^^^^^^^^^^^^^
|
||||
34 |
|
||||
35 | async def another_coro():
|
||||
|
|
||||
help: Coroutine is not awaited
|
||||
30 |
|
||||
31 | async def test_coroutine_without_await():
|
||||
32 | async def coro():
|
||||
- another_coro() # RUF065
|
||||
33 + await another_coro() # RUF065
|
||||
34 |
|
||||
35 | async def another_coro():
|
||||
36 | pass
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF065 [*] Coroutine is not awaited
|
||||
--> RUF065.py:42:5
|
||||
|
|
||||
41 | async def test_asyncio_api_without_await():
|
||||
42 | asyncio.sleep(0.5) # RUF065
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Coroutine is not awaited
|
||||
39 |
|
||||
40 |
|
||||
41 | async def test_asyncio_api_without_await():
|
||||
- asyncio.sleep(0.5) # RUF065
|
||||
42 + await asyncio.sleep(0.5) # RUF065
|
||||
43 |
|
||||
44 |
|
||||
45 | async def test_asyncio_api_without_await():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF065 [*] Coroutine is not awaited
|
||||
--> RUF065.py:47:9
|
||||
|
|
||||
45 | async def test_asyncio_api_without_await():
|
||||
46 | async def coro():
|
||||
47 | asyncio.sleep(0.5) # RUF065
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
48 |
|
||||
49 | await asyncio.wait(coro)
|
||||
|
|
||||
help: Coroutine is not awaited
|
||||
44 |
|
||||
45 | async def test_asyncio_api_without_await():
|
||||
46 | async def coro():
|
||||
- asyncio.sleep(0.5) # RUF065
|
||||
47 + await asyncio.sleep(0.5) # RUF065
|
||||
48 |
|
||||
49 | await asyncio.wait(coro)
|
||||
50 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF065 [*] Coroutine is not awaited
|
||||
--> RUF065.py:56:5
|
||||
|
|
||||
54 | await asyncio.sleep(0.5)
|
||||
55 |
|
||||
56 | asyncio.wait_for(coro) # RUF065
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Coroutine is not awaited
|
||||
53 | async def coro():
|
||||
54 | await asyncio.sleep(0.5)
|
||||
55 |
|
||||
- asyncio.wait_for(coro) # RUF065
|
||||
56 + await asyncio.wait_for(coro) # RUF065
|
||||
57 |
|
||||
58 |
|
||||
59 | async def test_asyncio_api_without_await():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
RUF065 [*] Coroutine is not awaited
|
||||
--> RUF065.py:67:5
|
||||
|
|
||||
66 | tasks = [coro1(), coro2()]
|
||||
67 | asyncio.gather(*tasks) # RUF065
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: Coroutine is not awaited
|
||||
64 | await asyncio.sleep(0.5)
|
||||
65 |
|
||||
66 | tasks = [coro1(), coro2()]
|
||||
- asyncio.gather(*tasks) # RUF065
|
||||
67 + await asyncio.gather(*tasks) # RUF065
|
||||
68 |
|
||||
69 |
|
||||
70 | # Non-violation cases: RUF065
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
|
@ -4057,6 +4057,7 @@
|
|||
"RUF061",
|
||||
"RUF063",
|
||||
"RUF064",
|
||||
"RUF065",
|
||||
"RUF1",
|
||||
"RUF10",
|
||||
"RUF100",
|
||||
|
|
|
|||
Loading…
Reference in New Issue