diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_trio/TRIO115.py b/crates/ruff_linter/resources/test/fixtures/flake8_trio/TRIO115.py new file mode 100644 index 0000000000000..117c2270f2940 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_trio/TRIO115.py @@ -0,0 +1,25 @@ +import trio +from trio import sleep + + +async def func(): + await trio.sleep(0) # TRIO115 + await trio.sleep(1) # OK + await trio.sleep(0, 1) # OK + await trio.sleep(...) # OK + await trio.sleep() # OK + + trio.sleep(0) # TRIO115 + foo = 0 + trio.sleep(foo) # TRIO115 + trio.sleep(1) # OK + time.sleep(0) # OK + + sleep(0) # TRIO115 + + +trio.sleep(0) # TRIO115 + + +def func(): + trio.run(trio.sleep(0)) # TRIO115 diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index e8970a2fabb84..17da840116cf2 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -929,6 +929,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::TrioSyncCall) { flake8_trio::rules::sync_call(checker, call); } + if checker.enabled(Rule::TrioZeroSleepCall) { + flake8_trio::rules::zero_sleep_call(checker, call); + } } Expr::Dict( dict @ ast::ExprDict { diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index 5139a47358eaf..d1176c022a7c6 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -293,6 +293,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { // flake8-trio (Flake8Trio, "100") => (RuleGroup::Preview, rules::flake8_trio::rules::TrioTimeoutWithoutAwait), (Flake8Trio, "105") => (RuleGroup::Preview, rules::flake8_trio::rules::TrioSyncCall), + (Flake8Trio, "115") => (RuleGroup::Preview, rules::flake8_trio::rules::TrioZeroSleepCall), // flake8-builtins (Flake8Builtins, "001") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinVariableShadowing), diff --git a/crates/ruff_linter/src/rules/flake8_trio/mod.rs b/crates/ruff_linter/src/rules/flake8_trio/mod.rs index a07b2794f72c4..f68d797581856 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/mod.rs @@ -16,6 +16,7 @@ mod tests { #[test_case(Rule::TrioTimeoutWithoutAwait, Path::new("TRIO100.py"))] #[test_case(Rule::TrioSyncCall, Path::new("TRIO105.py"))] + #[test_case(Rule::TrioZeroSleepCall, Path::new("TRIO115.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/flake8_trio/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_trio/rules/mod.rs index 61875c489d8d5..ed3806b3f241f 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/rules/mod.rs @@ -1,5 +1,7 @@ pub(crate) use sync_call::*; pub(crate) use timeout_without_await::*; +pub(crate) use zero_sleep_call::*; mod sync_call; mod timeout_without_await; +mod zero_sleep_call; diff --git a/crates/ruff_linter/src/rules/flake8_trio/rules/zero_sleep_call.rs b/crates/ruff_linter/src/rules/flake8_trio/rules/zero_sleep_call.rs new file mode 100644 index 0000000000000..efb85e2120306 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_trio/rules/zero_sleep_call.rs @@ -0,0 +1,109 @@ +use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::Stmt; +use ruff_python_ast::{self as ast, Expr, ExprCall, Int}; +use ruff_text_size::Ranged; + +use crate::checkers::ast::Checker; +use crate::importer::ImportRequest; + +/// ## What it does +/// Checks for uses of `trio.sleep(0)`. +/// +/// ## Why is this bad? +/// `trio.sleep(0)` is equivalent to calling `trio.lowlevel.checkpoint()`. +/// However, the latter better conveys the intent of the code. +/// +/// ## Example +/// ```python +/// async def func(): +/// await trio.sleep(0) +/// ``` +/// +/// Use instead: +/// ```python +/// async def func(): +/// await trio.lowlevel.checkpoint() +/// ``` +#[violation] +pub struct TrioZeroSleepCall; + +impl AlwaysFixableViolation for TrioZeroSleepCall { + #[derive_message_formats] + fn message(&self) -> String { + format!("Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`") + } + + fn fix_title(&self) -> String { + format!("Replace with `trio.lowlevel.checkpoint()`") + } +} + +/// TRIO115 +pub(crate) fn zero_sleep_call(checker: &mut Checker, call: &ExprCall) { + if !checker + .semantic() + .resolve_call_path(call.func.as_ref()) + .is_some_and(|call_path| matches!(call_path.as_slice(), ["trio", "sleep"])) + { + return; + } + + if call.arguments.len() != 1 { + return; + } + + let Some(arg) = call.arguments.find_argument("seconds", 0) else { + return; + }; + + match arg { + Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => { + let Some(int) = value.as_int() else { return }; + if *int != Int::ZERO { + return; + } + } + Expr::Name(ast::ExprName { id, .. }) => { + let scope = checker.semantic().current_scope(); + if let Some(binding_id) = scope.get(id) { + let binding = checker.semantic().binding(binding_id); + if binding.kind.is_assignment() || binding.kind.is_named_expr_assignment() { + if let Some(parent_id) = binding.source { + let parent = checker.semantic().statement(parent_id); + if let Stmt::Assign(ast::StmtAssign { value, .. }) + | Stmt::AnnAssign(ast::StmtAnnAssign { + value: Some(value), .. + }) + | Stmt::AugAssign(ast::StmtAugAssign { value, .. }) = parent + { + if let Expr::NumberLiteral(ast::ExprNumberLiteral { + value: num, .. + }) = value.as_ref() + { + let Some(int) = num.as_int() else { return }; + if *int != Int::ZERO { + return; + } + } + } + } + } + } + } + _ => return, + } + + let mut diagnostic = Diagnostic::new(TrioZeroSleepCall, call.range()); + diagnostic.try_set_fix(|| { + let (import_edit, binding) = checker.importer().get_or_import_symbol( + &ImportRequest::import("trio", "lowlevel.checkpoint"), + call.func.start(), + checker.semantic(), + )?; + let reference_edit = Edit::range_replacement(binding, call.func.range()); + let arg_edit = Edit::range_deletion(call.arguments.range); + Ok(Fix::safe_edits(import_edit, [reference_edit, arg_edit])) + }); + checker.diagnostics.push(diagnostic); +} diff --git a/crates/ruff_linter/src/rules/flake8_trio/snapshots/ruff_linter__rules__flake8_trio__tests__TRIO115_TRIO115.py.snap b/crates/ruff_linter/src/rules/flake8_trio/snapshots/ruff_linter__rules__flake8_trio__tests__TRIO115_TRIO115.py.snap new file mode 100644 index 0000000000000..b273e0333b8e4 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_trio/snapshots/ruff_linter__rules__flake8_trio__tests__TRIO115_TRIO115.py.snap @@ -0,0 +1,117 @@ +--- +source: crates/ruff_linter/src/rules/flake8_trio/mod.rs +--- +TRIO115.py:6:11: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)` + | +5 | async def func(): +6 | await trio.sleep(0) # TRIO115 + | ^^^^^^^^^^^^^ TRIO115 +7 | await trio.sleep(1) # OK +8 | await trio.sleep(0, 1) # OK + | + = help: Replace with `trio.lowlevel.checkpoint()` + +ℹ Fix +3 3 | +4 4 | +5 5 | async def func(): +6 |- await trio.sleep(0) # TRIO115 + 6 |+ await trio.lowlevel.checkpoint # TRIO115 +7 7 | await trio.sleep(1) # OK +8 8 | await trio.sleep(0, 1) # OK +9 9 | await trio.sleep(...) # OK + +TRIO115.py:12:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)` + | +10 | await trio.sleep() # OK +11 | +12 | trio.sleep(0) # TRIO115 + | ^^^^^^^^^^^^^ TRIO115 +13 | foo = 0 +14 | trio.sleep(foo) # TRIO115 + | + = help: Replace with `trio.lowlevel.checkpoint()` + +ℹ Fix +9 9 | await trio.sleep(...) # OK +10 10 | await trio.sleep() # OK +11 11 | +12 |- trio.sleep(0) # TRIO115 + 12 |+ trio.lowlevel.checkpoint # TRIO115 +13 13 | foo = 0 +14 14 | trio.sleep(foo) # TRIO115 +15 15 | trio.sleep(1) # OK + +TRIO115.py:14:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)` + | +12 | trio.sleep(0) # TRIO115 +13 | foo = 0 +14 | trio.sleep(foo) # TRIO115 + | ^^^^^^^^^^^^^^^ TRIO115 +15 | trio.sleep(1) # OK +16 | time.sleep(0) # OK + | + = help: Replace with `trio.lowlevel.checkpoint()` + +ℹ Fix +11 11 | +12 12 | trio.sleep(0) # TRIO115 +13 13 | foo = 0 +14 |- trio.sleep(foo) # TRIO115 + 14 |+ trio.lowlevel.checkpoint # TRIO115 +15 15 | trio.sleep(1) # OK +16 16 | time.sleep(0) # OK +17 17 | + +TRIO115.py:18:5: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)` + | +16 | time.sleep(0) # OK +17 | +18 | sleep(0) # TRIO115 + | ^^^^^^^^ TRIO115 + | + = help: Replace with `trio.lowlevel.checkpoint()` + +ℹ Fix +15 15 | trio.sleep(1) # OK +16 16 | time.sleep(0) # OK +17 17 | +18 |- sleep(0) # TRIO115 + 18 |+ trio.lowlevel.checkpoint # TRIO115 +19 19 | +20 20 | +21 21 | trio.sleep(0) # TRIO115 + +TRIO115.py:21:1: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)` + | +21 | trio.sleep(0) # TRIO115 + | ^^^^^^^^^^^^^ TRIO115 + | + = help: Replace with `trio.lowlevel.checkpoint()` + +ℹ Fix +18 18 | sleep(0) # TRIO115 +19 19 | +20 20 | +21 |-trio.sleep(0) # TRIO115 + 21 |+trio.lowlevel.checkpoint # TRIO115 +22 22 | +23 23 | +24 24 | def func(): + +TRIO115.py:25:14: TRIO115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)` + | +24 | def func(): +25 | trio.run(trio.sleep(0)) # TRIO115 + | ^^^^^^^^^^^^^ TRIO115 + | + = help: Replace with `trio.lowlevel.checkpoint()` + +ℹ Fix +22 22 | +23 23 | +24 24 | def func(): +25 |- trio.run(trio.sleep(0)) # TRIO115 + 25 |+ trio.run(trio.lowlevel.checkpoint) # TRIO115 + + diff --git a/ruff.schema.json b/ruff.schema.json index adad852f9cede..2945460f377e4 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3473,6 +3473,8 @@ "TRIO10", "TRIO100", "TRIO105", + "TRIO11", + "TRIO115", "TRY", "TRY0", "TRY00",