diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_trio/TRIO105.py b/crates/ruff_linter/resources/test/fixtures/flake8_trio/TRIO105.py new file mode 100644 index 0000000000..4668d114c9 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_trio/TRIO105.py @@ -0,0 +1,64 @@ +import trio + + +async def func() -> None: + trio.run(foo) # OK, not async + + # OK + await trio.aclose_forcefully(foo) + await trio.open_file(foo) + await trio.open_ssl_over_tcp_listeners(foo, foo) + await trio.open_ssl_over_tcp_stream(foo, foo) + await trio.open_tcp_listeners(foo) + await trio.open_tcp_stream(foo, foo) + await trio.open_unix_socket(foo) + await trio.run_process(foo) + await trio.sleep(5) + await trio.sleep_until(5) + await trio.lowlevel.cancel_shielded_checkpoint() + await trio.lowlevel.checkpoint() + await trio.lowlevel.checkpoint_if_cancelled() + await trio.lowlevel.open_process(foo) + await trio.lowlevel.permanently_detach_coroutine_object(foo) + await trio.lowlevel.reattach_detached_coroutine_object(foo, foo) + await trio.lowlevel.temporarily_detach_coroutine_object(foo) + await trio.lowlevel.wait_readable(foo) + await trio.lowlevel.wait_task_rescheduled(foo) + await trio.lowlevel.wait_writable(foo) + + # TRIO105 + trio.aclose_forcefully(foo) + trio.open_file(foo) + trio.open_ssl_over_tcp_listeners(foo, foo) + trio.open_ssl_over_tcp_stream(foo, foo) + trio.open_tcp_listeners(foo) + trio.open_tcp_stream(foo, foo) + trio.open_unix_socket(foo) + trio.run_process(foo) + trio.serve_listeners(foo, foo) + trio.serve_ssl_over_tcp(foo, foo, foo) + trio.serve_tcp(foo, foo) + trio.sleep(foo) + trio.sleep_forever() + trio.sleep_until(foo) + trio.lowlevel.cancel_shielded_checkpoint() + trio.lowlevel.checkpoint() + trio.lowlevel.checkpoint_if_cancelled() + trio.lowlevel.open_process() + trio.lowlevel.permanently_detach_coroutine_object(foo) + trio.lowlevel.reattach_detached_coroutine_object(foo, foo) + trio.lowlevel.temporarily_detach_coroutine_object(foo) + trio.lowlevel.wait_readable(foo) + trio.lowlevel.wait_task_rescheduled(foo) + trio.lowlevel.wait_writable(foo) + + async with await trio.open_file(foo): # Ok + pass + + async with trio.open_file(foo): # TRIO105 + pass + + +def func() -> None: + # TRIO105 (without fix) + trio.open_file(foo) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index aa000bcf5d..e8970a2fab 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -15,8 +15,8 @@ use crate::rules::{ flake8_comprehensions, flake8_datetimez, flake8_debugger, flake8_django, flake8_future_annotations, flake8_gettext, flake8_implicit_str_concat, flake8_logging, flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_self, - flake8_simplify, flake8_tidy_imports, flake8_use_pathlib, flynt, numpy, pandas_vet, - pep8_naming, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff, + flake8_simplify, flake8_tidy_imports, flake8_trio, flake8_use_pathlib, flynt, numpy, + pandas_vet, pep8_naming, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff, }; use crate::settings::types::PythonVersion; @@ -926,6 +926,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { if checker.enabled(Rule::ImplicitCwd) { refurb::rules::no_implicit_cwd(checker, call); } + if checker.enabled(Rule::TrioSyncCall) { + flake8_trio::rules::sync_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 9e0ceccfd7..5139a47358 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -292,6 +292,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), // flake8-builtins (Flake8Builtins, "001") => (RuleGroup::Stable, rules::flake8_builtins::rules::BuiltinVariableShadowing), diff --git a/crates/ruff_linter/src/rules/flake8_trio/method_name.rs b/crates/ruff_linter/src/rules/flake8_trio/method_name.rs new file mode 100644 index 0000000000..51a6475d7e --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_trio/method_name.rs @@ -0,0 +1,157 @@ +use ruff_python_ast::call_path::CallPath; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub(super) enum MethodName { + AcloseForcefully, + CancelScope, + CancelShieldedCheckpoint, + Checkpoint, + CheckpointIfCancelled, + FailAfter, + FailAt, + MoveOnAfter, + MoveOnAt, + OpenFile, + OpenProcess, + OpenSslOverTcpListeners, + OpenSslOverTcpStream, + OpenTcpListeners, + OpenTcpStream, + OpenUnixSocket, + PermanentlyDetachCoroutineObject, + ReattachDetachedCoroutineObject, + RunProcess, + ServeListeners, + ServeSslOverTcp, + ServeTcp, + Sleep, + SleepForever, + TemporarilyDetachCoroutineObject, + WaitReadable, + WaitTaskRescheduled, + WaitWritable, +} + +impl MethodName { + /// Returns `true` if the method is async, `false` if it is sync. + pub(super) fn is_async(self) -> bool { + match self { + MethodName::AcloseForcefully + | MethodName::CancelShieldedCheckpoint + | MethodName::Checkpoint + | MethodName::CheckpointIfCancelled + | MethodName::OpenFile + | MethodName::OpenProcess + | MethodName::OpenSslOverTcpListeners + | MethodName::OpenSslOverTcpStream + | MethodName::OpenTcpListeners + | MethodName::OpenTcpStream + | MethodName::OpenUnixSocket + | MethodName::PermanentlyDetachCoroutineObject + | MethodName::ReattachDetachedCoroutineObject + | MethodName::RunProcess + | MethodName::ServeListeners + | MethodName::ServeSslOverTcp + | MethodName::ServeTcp + | MethodName::Sleep + | MethodName::SleepForever + | MethodName::TemporarilyDetachCoroutineObject + | MethodName::WaitReadable + | MethodName::WaitTaskRescheduled + | MethodName::WaitWritable => true, + + MethodName::MoveOnAfter + | MethodName::MoveOnAt + | MethodName::FailAfter + | MethodName::FailAt + | MethodName::CancelScope => false, + } + } +} + +impl MethodName { + pub(super) fn try_from(call_path: &CallPath<'_>) -> Option { + match call_path.as_slice() { + ["trio", "CancelScope"] => Some(Self::CancelScope), + ["trio", "aclose_forcefully"] => Some(Self::AcloseForcefully), + ["trio", "fail_after"] => Some(Self::FailAfter), + ["trio", "fail_at"] => Some(Self::FailAt), + ["trio", "lowlevel", "cancel_shielded_checkpoint"] => { + Some(Self::CancelShieldedCheckpoint) + } + ["trio", "lowlevel", "checkpoint"] => Some(Self::Checkpoint), + ["trio", "lowlevel", "checkpoint_if_cancelled"] => Some(Self::CheckpointIfCancelled), + ["trio", "lowlevel", "open_process"] => Some(Self::OpenProcess), + ["trio", "lowlevel", "permanently_detach_coroutine_object"] => { + Some(Self::PermanentlyDetachCoroutineObject) + } + ["trio", "lowlevel", "reattach_detached_coroutine_object"] => { + Some(Self::ReattachDetachedCoroutineObject) + } + ["trio", "lowlevel", "temporarily_detach_coroutine_object"] => { + Some(Self::TemporarilyDetachCoroutineObject) + } + ["trio", "lowlevel", "wait_readable"] => Some(Self::WaitReadable), + ["trio", "lowlevel", "wait_task_rescheduled"] => Some(Self::WaitTaskRescheduled), + ["trio", "lowlevel", "wait_writable"] => Some(Self::WaitWritable), + ["trio", "move_on_after"] => Some(Self::MoveOnAfter), + ["trio", "move_on_at"] => Some(Self::MoveOnAt), + ["trio", "open_file"] => Some(Self::OpenFile), + ["trio", "open_ssl_over_tcp_listeners"] => Some(Self::OpenSslOverTcpListeners), + ["trio", "open_ssl_over_tcp_stream"] => Some(Self::OpenSslOverTcpStream), + ["trio", "open_tcp_listeners"] => Some(Self::OpenTcpListeners), + ["trio", "open_tcp_stream"] => Some(Self::OpenTcpStream), + ["trio", "open_unix_socket"] => Some(Self::OpenUnixSocket), + ["trio", "run_process"] => Some(Self::RunProcess), + ["trio", "serve_listeners"] => Some(Self::ServeListeners), + ["trio", "serve_ssl_over_tcp"] => Some(Self::ServeSslOverTcp), + ["trio", "serve_tcp"] => Some(Self::ServeTcp), + ["trio", "sleep"] => Some(Self::Sleep), + ["trio", "sleep_forever"] => Some(Self::SleepForever), + _ => None, + } + } +} + +impl std::fmt::Display for MethodName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MethodName::AcloseForcefully => write!(f, "trio.aclose_forcefully"), + MethodName::CancelScope => write!(f, "trio.CancelScope"), + MethodName::CancelShieldedCheckpoint => { + write!(f, "trio.lowlevel.cancel_shielded_checkpoint") + } + MethodName::Checkpoint => write!(f, "trio.lowlevel.checkpoint"), + MethodName::CheckpointIfCancelled => write!(f, "trio.lowlevel.checkpoint_if_cancelled"), + MethodName::FailAfter => write!(f, "trio.fail_after"), + MethodName::FailAt => write!(f, "trio.fail_at"), + MethodName::MoveOnAfter => write!(f, "trio.move_on_after"), + MethodName::MoveOnAt => write!(f, "trio.move_on_at"), + MethodName::OpenFile => write!(f, "trio.open_file"), + MethodName::OpenProcess => write!(f, "trio.lowlevel.open_process"), + MethodName::OpenSslOverTcpListeners => write!(f, "trio.open_ssl_over_tcp_listeners"), + MethodName::OpenSslOverTcpStream => write!(f, "trio.open_ssl_over_tcp_stream"), + MethodName::OpenTcpListeners => write!(f, "trio.open_tcp_listeners"), + MethodName::OpenTcpStream => write!(f, "trio.open_tcp_stream"), + MethodName::OpenUnixSocket => write!(f, "trio.open_unix_socket"), + MethodName::PermanentlyDetachCoroutineObject => { + write!(f, "trio.lowlevel.permanently_detach_coroutine_object") + } + MethodName::ReattachDetachedCoroutineObject => { + write!(f, "trio.lowlevel.reattach_detached_coroutine_object") + } + MethodName::RunProcess => write!(f, "trio.run_process"), + MethodName::ServeListeners => write!(f, "trio.serve_listeners"), + MethodName::ServeSslOverTcp => write!(f, "trio.serve_ssl_over_tcp"), + MethodName::ServeTcp => write!(f, "trio.serve_tcp"), + MethodName::Sleep => write!(f, "trio.sleep"), + MethodName::SleepForever => write!(f, "trio.sleep_forever"), + MethodName::TemporarilyDetachCoroutineObject => { + write!(f, "trio.lowlevel.temporarily_detach_coroutine_object") + } + MethodName::WaitReadable => write!(f, "trio.lowlevel.wait_readable"), + MethodName::WaitTaskRescheduled => write!(f, "trio.lowlevel.wait_task_rescheduled"), + MethodName::WaitWritable => write!(f, "trio.lowlevel.wait_writable"), + } + } +} diff --git a/crates/ruff_linter/src/rules/flake8_trio/mod.rs b/crates/ruff_linter/src/rules/flake8_trio/mod.rs index cde1aae100..a07b2794f7 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/mod.rs @@ -1,4 +1,5 @@ //! Rules from [flake8-trio](https://pypi.org/project/flake8-trio/). +pub(super) mod method_name; pub(crate) mod rules; #[cfg(test)] @@ -14,6 +15,7 @@ mod tests { use crate::test::test_path; #[test_case(Rule::TrioTimeoutWithoutAwait, Path::new("TRIO100.py"))] + #[test_case(Rule::TrioSyncCall, Path::new("TRIO105.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 73521d47fe..61875c489d 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/rules/mod.rs @@ -1,3 +1,5 @@ +pub(crate) use sync_call::*; pub(crate) use timeout_without_await::*; +mod sync_call; mod timeout_without_await; diff --git a/crates/ruff_linter/src/rules/flake8_trio/rules/sync_call.rs b/crates/ruff_linter/src/rules/flake8_trio/rules/sync_call.rs new file mode 100644 index 0000000000..43a2699fe3 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_trio/rules/sync_call.rs @@ -0,0 +1,87 @@ +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::{Expr, ExprCall}; +use ruff_text_size::{Ranged, TextRange}; + +use crate::checkers::ast::Checker; +use crate::fix::edits::pad; +use crate::rules::flake8_trio::method_name::MethodName; + +/// ## What it does +/// Checks for calls to trio functions that are not immediately awaited. +/// +/// ## Why is this bad? +/// Many of the functions exposed by trio are asynchronous, and must be awaited +/// to take effect. Calling a trio function without an `await` can lead to +/// `RuntimeWarning` diagnostics and unexpected behaviour. +/// +/// ## Fix safety +/// This rule's fix is marked as unsafe, as adding an `await` to a function +/// call changes its semantics and runtime behavior. +/// +/// ## Example +/// ```python +/// async def double_sleep(x): +/// trio.sleep(2 * x) +/// ``` +/// +/// Use instead: +/// ```python +/// async def double_sleep(x): +/// await trio.sleep(2 * x) +/// ``` +#[violation] +pub struct TrioSyncCall { + method_name: MethodName, +} + +impl Violation for TrioSyncCall { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let Self { method_name } = self; + format!("Call to `{method_name}` is not immediately awaited") + } + + fn fix_title(&self) -> Option { + Some(format!("Add `await`")) + } +} + +/// TRIO105 +pub(crate) fn sync_call(checker: &mut Checker, call: &ExprCall) { + let Some(method_name) = ({ + let Some(call_path) = checker.semantic().resolve_call_path(call.func.as_ref()) else { + return; + }; + MethodName::try_from(&call_path) + }) else { + return; + }; + + if !method_name.is_async() { + return; + } + + if checker + .semantic() + .current_expression_parent() + .is_some_and(Expr::is_await_expr) + { + return; + }; + + let mut diagnostic = Diagnostic::new(TrioSyncCall { method_name }, call.range); + if checker.semantic().in_async_context() { + diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion( + pad( + "await".to_string(), + TextRange::new(call.func.start(), call.func.start()), + checker.locator(), + ), + call.func.start(), + ))); + } + checker.diagnostics.push(diagnostic); +} diff --git a/crates/ruff_linter/src/rules/flake8_trio/rules/timeout_without_await.rs b/crates/ruff_linter/src/rules/flake8_trio/rules/timeout_without_await.rs index 8492933b35..6870d99f1a 100644 --- a/crates/ruff_linter/src/rules/flake8_trio/rules/timeout_without_await.rs +++ b/crates/ruff_linter/src/rules/flake8_trio/rules/timeout_without_await.rs @@ -1,10 +1,11 @@ use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; -use ruff_python_ast::visitor::{walk_expr, walk_stmt, Visitor}; -use ruff_python_ast::{Expr, ExprAwait, Stmt, StmtWith, WithItem}; +use ruff_python_ast::helpers::AwaitVisitor; +use ruff_python_ast::visitor::Visitor; +use ruff_python_ast::{StmtWith, WithItem}; use crate::checkers::ast::Checker; +use crate::rules::flake8_trio::method_name::MethodName; /// ## What it does /// Checks for trio functions that should contain await but don't. @@ -56,6 +57,17 @@ pub(crate) fn timeout_without_await( return; }; + if !matches!( + method_name, + MethodName::MoveOnAfter + | MethodName::MoveOnAt + | MethodName::FailAfter + | MethodName::FailAt + | MethodName::CancelScope + ) { + return; + } + let mut visitor = AwaitVisitor::default(); visitor.visit_body(&with_stmt.body); if visitor.seen_await { @@ -67,59 +79,3 @@ pub(crate) fn timeout_without_await( with_stmt.range, )); } - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum MethodName { - MoveOnAfter, - MoveOnAt, - FailAfter, - FailAt, - CancelScope, -} - -impl MethodName { - fn try_from(call_path: &CallPath<'_>) -> Option { - match call_path.as_slice() { - ["trio", "move_on_after"] => Some(Self::MoveOnAfter), - ["trio", "move_on_at"] => Some(Self::MoveOnAt), - ["trio", "fail_after"] => Some(Self::FailAfter), - ["trio", "fail_at"] => Some(Self::FailAt), - ["trio", "CancelScope"] => Some(Self::CancelScope), - _ => None, - } - } -} - -impl std::fmt::Display for MethodName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - MethodName::MoveOnAfter => write!(f, "trio.move_on_after"), - MethodName::MoveOnAt => write!(f, "trio.move_on_at"), - MethodName::FailAfter => write!(f, "trio.fail_after"), - MethodName::FailAt => write!(f, "trio.fail_at"), - MethodName::CancelScope => write!(f, "trio.CancelScope"), - } - } -} - -#[derive(Debug, Default)] -struct AwaitVisitor { - seen_await: bool, -} - -impl Visitor<'_> for AwaitVisitor { - fn visit_stmt(&mut self, stmt: &Stmt) { - match stmt { - Stmt::FunctionDef(_) | Stmt::ClassDef(_) => (), - _ => walk_stmt(self, stmt), - } - } - - fn visit_expr(&mut self, expr: &Expr) { - if let Expr::Await(ExprAwait { .. }) = expr { - self.seen_await = true; - } else { - walk_expr(self, expr); - } - } -} diff --git a/crates/ruff_linter/src/rules/flake8_trio/snapshots/ruff_linter__rules__flake8_trio__tests__TRIO105_TRIO105.py.snap b/crates/ruff_linter/src/rules/flake8_trio/snapshots/ruff_linter__rules__flake8_trio__tests__TRIO105_TRIO105.py.snap new file mode 100644 index 0000000000..ac9d346036 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_trio/snapshots/ruff_linter__rules__flake8_trio__tests__TRIO105_TRIO105.py.snap @@ -0,0 +1,514 @@ +--- +source: crates/ruff_linter/src/rules/flake8_trio/mod.rs +--- +TRIO105.py:30:5: TRIO105 [*] Call to `trio.aclose_forcefully` is not immediately awaited + | +29 | # TRIO105 +30 | trio.aclose_forcefully(foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +31 | trio.open_file(foo) +32 | trio.open_ssl_over_tcp_listeners(foo, foo) + | + = help: Add `await` + +ℹ Suggested fix +27 27 | await trio.lowlevel.wait_writable(foo) +28 28 | +29 29 | # TRIO105 +30 |- trio.aclose_forcefully(foo) + 30 |+ await trio.aclose_forcefully(foo) +31 31 | trio.open_file(foo) +32 32 | trio.open_ssl_over_tcp_listeners(foo, foo) +33 33 | trio.open_ssl_over_tcp_stream(foo, foo) + +TRIO105.py:31:5: TRIO105 [*] Call to `trio.open_file` is not immediately awaited + | +29 | # TRIO105 +30 | trio.aclose_forcefully(foo) +31 | trio.open_file(foo) + | ^^^^^^^^^^^^^^^^^^^ TRIO105 +32 | trio.open_ssl_over_tcp_listeners(foo, foo) +33 | trio.open_ssl_over_tcp_stream(foo, foo) + | + = help: Add `await` + +ℹ Suggested fix +28 28 | +29 29 | # TRIO105 +30 30 | trio.aclose_forcefully(foo) +31 |- trio.open_file(foo) + 31 |+ await trio.open_file(foo) +32 32 | trio.open_ssl_over_tcp_listeners(foo, foo) +33 33 | trio.open_ssl_over_tcp_stream(foo, foo) +34 34 | trio.open_tcp_listeners(foo) + +TRIO105.py:32:5: TRIO105 [*] Call to `trio.open_ssl_over_tcp_listeners` is not immediately awaited + | +30 | trio.aclose_forcefully(foo) +31 | trio.open_file(foo) +32 | trio.open_ssl_over_tcp_listeners(foo, foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +33 | trio.open_ssl_over_tcp_stream(foo, foo) +34 | trio.open_tcp_listeners(foo) + | + = help: Add `await` + +ℹ Suggested fix +29 29 | # TRIO105 +30 30 | trio.aclose_forcefully(foo) +31 31 | trio.open_file(foo) +32 |- trio.open_ssl_over_tcp_listeners(foo, foo) + 32 |+ await trio.open_ssl_over_tcp_listeners(foo, foo) +33 33 | trio.open_ssl_over_tcp_stream(foo, foo) +34 34 | trio.open_tcp_listeners(foo) +35 35 | trio.open_tcp_stream(foo, foo) + +TRIO105.py:33:5: TRIO105 [*] Call to `trio.open_ssl_over_tcp_stream` is not immediately awaited + | +31 | trio.open_file(foo) +32 | trio.open_ssl_over_tcp_listeners(foo, foo) +33 | trio.open_ssl_over_tcp_stream(foo, foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +34 | trio.open_tcp_listeners(foo) +35 | trio.open_tcp_stream(foo, foo) + | + = help: Add `await` + +ℹ Suggested fix +30 30 | trio.aclose_forcefully(foo) +31 31 | trio.open_file(foo) +32 32 | trio.open_ssl_over_tcp_listeners(foo, foo) +33 |- trio.open_ssl_over_tcp_stream(foo, foo) + 33 |+ await trio.open_ssl_over_tcp_stream(foo, foo) +34 34 | trio.open_tcp_listeners(foo) +35 35 | trio.open_tcp_stream(foo, foo) +36 36 | trio.open_unix_socket(foo) + +TRIO105.py:34:5: TRIO105 [*] Call to `trio.open_tcp_listeners` is not immediately awaited + | +32 | trio.open_ssl_over_tcp_listeners(foo, foo) +33 | trio.open_ssl_over_tcp_stream(foo, foo) +34 | trio.open_tcp_listeners(foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +35 | trio.open_tcp_stream(foo, foo) +36 | trio.open_unix_socket(foo) + | + = help: Add `await` + +ℹ Suggested fix +31 31 | trio.open_file(foo) +32 32 | trio.open_ssl_over_tcp_listeners(foo, foo) +33 33 | trio.open_ssl_over_tcp_stream(foo, foo) +34 |- trio.open_tcp_listeners(foo) + 34 |+ await trio.open_tcp_listeners(foo) +35 35 | trio.open_tcp_stream(foo, foo) +36 36 | trio.open_unix_socket(foo) +37 37 | trio.run_process(foo) + +TRIO105.py:35:5: TRIO105 [*] Call to `trio.open_tcp_stream` is not immediately awaited + | +33 | trio.open_ssl_over_tcp_stream(foo, foo) +34 | trio.open_tcp_listeners(foo) +35 | trio.open_tcp_stream(foo, foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +36 | trio.open_unix_socket(foo) +37 | trio.run_process(foo) + | + = help: Add `await` + +ℹ Suggested fix +32 32 | trio.open_ssl_over_tcp_listeners(foo, foo) +33 33 | trio.open_ssl_over_tcp_stream(foo, foo) +34 34 | trio.open_tcp_listeners(foo) +35 |- trio.open_tcp_stream(foo, foo) + 35 |+ await trio.open_tcp_stream(foo, foo) +36 36 | trio.open_unix_socket(foo) +37 37 | trio.run_process(foo) +38 38 | trio.serve_listeners(foo, foo) + +TRIO105.py:36:5: TRIO105 [*] Call to `trio.open_unix_socket` is not immediately awaited + | +34 | trio.open_tcp_listeners(foo) +35 | trio.open_tcp_stream(foo, foo) +36 | trio.open_unix_socket(foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +37 | trio.run_process(foo) +38 | trio.serve_listeners(foo, foo) + | + = help: Add `await` + +ℹ Suggested fix +33 33 | trio.open_ssl_over_tcp_stream(foo, foo) +34 34 | trio.open_tcp_listeners(foo) +35 35 | trio.open_tcp_stream(foo, foo) +36 |- trio.open_unix_socket(foo) + 36 |+ await trio.open_unix_socket(foo) +37 37 | trio.run_process(foo) +38 38 | trio.serve_listeners(foo, foo) +39 39 | trio.serve_ssl_over_tcp(foo, foo, foo) + +TRIO105.py:37:5: TRIO105 [*] Call to `trio.run_process` is not immediately awaited + | +35 | trio.open_tcp_stream(foo, foo) +36 | trio.open_unix_socket(foo) +37 | trio.run_process(foo) + | ^^^^^^^^^^^^^^^^^^^^^ TRIO105 +38 | trio.serve_listeners(foo, foo) +39 | trio.serve_ssl_over_tcp(foo, foo, foo) + | + = help: Add `await` + +ℹ Suggested fix +34 34 | trio.open_tcp_listeners(foo) +35 35 | trio.open_tcp_stream(foo, foo) +36 36 | trio.open_unix_socket(foo) +37 |- trio.run_process(foo) + 37 |+ await trio.run_process(foo) +38 38 | trio.serve_listeners(foo, foo) +39 39 | trio.serve_ssl_over_tcp(foo, foo, foo) +40 40 | trio.serve_tcp(foo, foo) + +TRIO105.py:38:5: TRIO105 [*] Call to `trio.serve_listeners` is not immediately awaited + | +36 | trio.open_unix_socket(foo) +37 | trio.run_process(foo) +38 | trio.serve_listeners(foo, foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +39 | trio.serve_ssl_over_tcp(foo, foo, foo) +40 | trio.serve_tcp(foo, foo) + | + = help: Add `await` + +ℹ Suggested fix +35 35 | trio.open_tcp_stream(foo, foo) +36 36 | trio.open_unix_socket(foo) +37 37 | trio.run_process(foo) +38 |- trio.serve_listeners(foo, foo) + 38 |+ await trio.serve_listeners(foo, foo) +39 39 | trio.serve_ssl_over_tcp(foo, foo, foo) +40 40 | trio.serve_tcp(foo, foo) +41 41 | trio.sleep(foo) + +TRIO105.py:39:5: TRIO105 [*] Call to `trio.serve_ssl_over_tcp` is not immediately awaited + | +37 | trio.run_process(foo) +38 | trio.serve_listeners(foo, foo) +39 | trio.serve_ssl_over_tcp(foo, foo, foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +40 | trio.serve_tcp(foo, foo) +41 | trio.sleep(foo) + | + = help: Add `await` + +ℹ Suggested fix +36 36 | trio.open_unix_socket(foo) +37 37 | trio.run_process(foo) +38 38 | trio.serve_listeners(foo, foo) +39 |- trio.serve_ssl_over_tcp(foo, foo, foo) + 39 |+ await trio.serve_ssl_over_tcp(foo, foo, foo) +40 40 | trio.serve_tcp(foo, foo) +41 41 | trio.sleep(foo) +42 42 | trio.sleep_forever() + +TRIO105.py:40:5: TRIO105 [*] Call to `trio.serve_tcp` is not immediately awaited + | +38 | trio.serve_listeners(foo, foo) +39 | trio.serve_ssl_over_tcp(foo, foo, foo) +40 | trio.serve_tcp(foo, foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +41 | trio.sleep(foo) +42 | trio.sleep_forever() + | + = help: Add `await` + +ℹ Suggested fix +37 37 | trio.run_process(foo) +38 38 | trio.serve_listeners(foo, foo) +39 39 | trio.serve_ssl_over_tcp(foo, foo, foo) +40 |- trio.serve_tcp(foo, foo) + 40 |+ await trio.serve_tcp(foo, foo) +41 41 | trio.sleep(foo) +42 42 | trio.sleep_forever() +43 43 | trio.sleep_until(foo) + +TRIO105.py:41:5: TRIO105 [*] Call to `trio.sleep` is not immediately awaited + | +39 | trio.serve_ssl_over_tcp(foo, foo, foo) +40 | trio.serve_tcp(foo, foo) +41 | trio.sleep(foo) + | ^^^^^^^^^^^^^^^ TRIO105 +42 | trio.sleep_forever() +43 | trio.sleep_until(foo) + | + = help: Add `await` + +ℹ Suggested fix +38 38 | trio.serve_listeners(foo, foo) +39 39 | trio.serve_ssl_over_tcp(foo, foo, foo) +40 40 | trio.serve_tcp(foo, foo) +41 |- trio.sleep(foo) + 41 |+ await trio.sleep(foo) +42 42 | trio.sleep_forever() +43 43 | trio.sleep_until(foo) +44 44 | trio.lowlevel.cancel_shielded_checkpoint() + +TRIO105.py:42:5: TRIO105 [*] Call to `trio.sleep_forever` is not immediately awaited + | +40 | trio.serve_tcp(foo, foo) +41 | trio.sleep(foo) +42 | trio.sleep_forever() + | ^^^^^^^^^^^^^^^^^^^^ TRIO105 +43 | trio.sleep_until(foo) +44 | trio.lowlevel.cancel_shielded_checkpoint() + | + = help: Add `await` + +ℹ Suggested fix +39 39 | trio.serve_ssl_over_tcp(foo, foo, foo) +40 40 | trio.serve_tcp(foo, foo) +41 41 | trio.sleep(foo) +42 |- trio.sleep_forever() + 42 |+ await trio.sleep_forever() +43 43 | trio.sleep_until(foo) +44 44 | trio.lowlevel.cancel_shielded_checkpoint() +45 45 | trio.lowlevel.checkpoint() + +TRIO105.py:44:5: TRIO105 [*] Call to `trio.lowlevel.cancel_shielded_checkpoint` is not immediately awaited + | +42 | trio.sleep_forever() +43 | trio.sleep_until(foo) +44 | trio.lowlevel.cancel_shielded_checkpoint() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +45 | trio.lowlevel.checkpoint() +46 | trio.lowlevel.checkpoint_if_cancelled() + | + = help: Add `await` + +ℹ Suggested fix +41 41 | trio.sleep(foo) +42 42 | trio.sleep_forever() +43 43 | trio.sleep_until(foo) +44 |- trio.lowlevel.cancel_shielded_checkpoint() + 44 |+ await trio.lowlevel.cancel_shielded_checkpoint() +45 45 | trio.lowlevel.checkpoint() +46 46 | trio.lowlevel.checkpoint_if_cancelled() +47 47 | trio.lowlevel.open_process() + +TRIO105.py:45:5: TRIO105 [*] Call to `trio.lowlevel.checkpoint` is not immediately awaited + | +43 | trio.sleep_until(foo) +44 | trio.lowlevel.cancel_shielded_checkpoint() +45 | trio.lowlevel.checkpoint() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +46 | trio.lowlevel.checkpoint_if_cancelled() +47 | trio.lowlevel.open_process() + | + = help: Add `await` + +ℹ Suggested fix +42 42 | trio.sleep_forever() +43 43 | trio.sleep_until(foo) +44 44 | trio.lowlevel.cancel_shielded_checkpoint() +45 |- trio.lowlevel.checkpoint() + 45 |+ await trio.lowlevel.checkpoint() +46 46 | trio.lowlevel.checkpoint_if_cancelled() +47 47 | trio.lowlevel.open_process() +48 48 | trio.lowlevel.permanently_detach_coroutine_object(foo) + +TRIO105.py:46:5: TRIO105 [*] Call to `trio.lowlevel.checkpoint_if_cancelled` is not immediately awaited + | +44 | trio.lowlevel.cancel_shielded_checkpoint() +45 | trio.lowlevel.checkpoint() +46 | trio.lowlevel.checkpoint_if_cancelled() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +47 | trio.lowlevel.open_process() +48 | trio.lowlevel.permanently_detach_coroutine_object(foo) + | + = help: Add `await` + +ℹ Suggested fix +43 43 | trio.sleep_until(foo) +44 44 | trio.lowlevel.cancel_shielded_checkpoint() +45 45 | trio.lowlevel.checkpoint() +46 |- trio.lowlevel.checkpoint_if_cancelled() + 46 |+ await trio.lowlevel.checkpoint_if_cancelled() +47 47 | trio.lowlevel.open_process() +48 48 | trio.lowlevel.permanently_detach_coroutine_object(foo) +49 49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo) + +TRIO105.py:47:5: TRIO105 [*] Call to `trio.lowlevel.open_process` is not immediately awaited + | +45 | trio.lowlevel.checkpoint() +46 | trio.lowlevel.checkpoint_if_cancelled() +47 | trio.lowlevel.open_process() + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +48 | trio.lowlevel.permanently_detach_coroutine_object(foo) +49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo) + | + = help: Add `await` + +ℹ Suggested fix +44 44 | trio.lowlevel.cancel_shielded_checkpoint() +45 45 | trio.lowlevel.checkpoint() +46 46 | trio.lowlevel.checkpoint_if_cancelled() +47 |- trio.lowlevel.open_process() + 47 |+ await trio.lowlevel.open_process() +48 48 | trio.lowlevel.permanently_detach_coroutine_object(foo) +49 49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo) +50 50 | trio.lowlevel.temporarily_detach_coroutine_object(foo) + +TRIO105.py:48:5: TRIO105 [*] Call to `trio.lowlevel.permanently_detach_coroutine_object` is not immediately awaited + | +46 | trio.lowlevel.checkpoint_if_cancelled() +47 | trio.lowlevel.open_process() +48 | trio.lowlevel.permanently_detach_coroutine_object(foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo) +50 | trio.lowlevel.temporarily_detach_coroutine_object(foo) + | + = help: Add `await` + +ℹ Suggested fix +45 45 | trio.lowlevel.checkpoint() +46 46 | trio.lowlevel.checkpoint_if_cancelled() +47 47 | trio.lowlevel.open_process() +48 |- trio.lowlevel.permanently_detach_coroutine_object(foo) + 48 |+ await trio.lowlevel.permanently_detach_coroutine_object(foo) +49 49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo) +50 50 | trio.lowlevel.temporarily_detach_coroutine_object(foo) +51 51 | trio.lowlevel.wait_readable(foo) + +TRIO105.py:49:5: TRIO105 [*] Call to `trio.lowlevel.reattach_detached_coroutine_object` is not immediately awaited + | +47 | trio.lowlevel.open_process() +48 | trio.lowlevel.permanently_detach_coroutine_object(foo) +49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +50 | trio.lowlevel.temporarily_detach_coroutine_object(foo) +51 | trio.lowlevel.wait_readable(foo) + | + = help: Add `await` + +ℹ Suggested fix +46 46 | trio.lowlevel.checkpoint_if_cancelled() +47 47 | trio.lowlevel.open_process() +48 48 | trio.lowlevel.permanently_detach_coroutine_object(foo) +49 |- trio.lowlevel.reattach_detached_coroutine_object(foo, foo) + 49 |+ await trio.lowlevel.reattach_detached_coroutine_object(foo, foo) +50 50 | trio.lowlevel.temporarily_detach_coroutine_object(foo) +51 51 | trio.lowlevel.wait_readable(foo) +52 52 | trio.lowlevel.wait_task_rescheduled(foo) + +TRIO105.py:50:5: TRIO105 [*] Call to `trio.lowlevel.temporarily_detach_coroutine_object` is not immediately awaited + | +48 | trio.lowlevel.permanently_detach_coroutine_object(foo) +49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo) +50 | trio.lowlevel.temporarily_detach_coroutine_object(foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +51 | trio.lowlevel.wait_readable(foo) +52 | trio.lowlevel.wait_task_rescheduled(foo) + | + = help: Add `await` + +ℹ Suggested fix +47 47 | trio.lowlevel.open_process() +48 48 | trio.lowlevel.permanently_detach_coroutine_object(foo) +49 49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo) +50 |- trio.lowlevel.temporarily_detach_coroutine_object(foo) + 50 |+ await trio.lowlevel.temporarily_detach_coroutine_object(foo) +51 51 | trio.lowlevel.wait_readable(foo) +52 52 | trio.lowlevel.wait_task_rescheduled(foo) +53 53 | trio.lowlevel.wait_writable(foo) + +TRIO105.py:51:5: TRIO105 [*] Call to `trio.lowlevel.wait_readable` is not immediately awaited + | +49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo) +50 | trio.lowlevel.temporarily_detach_coroutine_object(foo) +51 | trio.lowlevel.wait_readable(foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +52 | trio.lowlevel.wait_task_rescheduled(foo) +53 | trio.lowlevel.wait_writable(foo) + | + = help: Add `await` + +ℹ Suggested fix +48 48 | trio.lowlevel.permanently_detach_coroutine_object(foo) +49 49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo) +50 50 | trio.lowlevel.temporarily_detach_coroutine_object(foo) +51 |- trio.lowlevel.wait_readable(foo) + 51 |+ await trio.lowlevel.wait_readable(foo) +52 52 | trio.lowlevel.wait_task_rescheduled(foo) +53 53 | trio.lowlevel.wait_writable(foo) +54 54 | + +TRIO105.py:52:5: TRIO105 [*] Call to `trio.lowlevel.wait_task_rescheduled` is not immediately awaited + | +50 | trio.lowlevel.temporarily_detach_coroutine_object(foo) +51 | trio.lowlevel.wait_readable(foo) +52 | trio.lowlevel.wait_task_rescheduled(foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +53 | trio.lowlevel.wait_writable(foo) + | + = help: Add `await` + +ℹ Suggested fix +49 49 | trio.lowlevel.reattach_detached_coroutine_object(foo, foo) +50 50 | trio.lowlevel.temporarily_detach_coroutine_object(foo) +51 51 | trio.lowlevel.wait_readable(foo) +52 |- trio.lowlevel.wait_task_rescheduled(foo) + 52 |+ await trio.lowlevel.wait_task_rescheduled(foo) +53 53 | trio.lowlevel.wait_writable(foo) +54 54 | +55 55 | async with await trio.open_file(foo): # Ok + +TRIO105.py:53:5: TRIO105 [*] Call to `trio.lowlevel.wait_writable` is not immediately awaited + | +51 | trio.lowlevel.wait_readable(foo) +52 | trio.lowlevel.wait_task_rescheduled(foo) +53 | trio.lowlevel.wait_writable(foo) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TRIO105 +54 | +55 | async with await trio.open_file(foo): # Ok + | + = help: Add `await` + +ℹ Suggested fix +50 50 | trio.lowlevel.temporarily_detach_coroutine_object(foo) +51 51 | trio.lowlevel.wait_readable(foo) +52 52 | trio.lowlevel.wait_task_rescheduled(foo) +53 |- trio.lowlevel.wait_writable(foo) + 53 |+ await trio.lowlevel.wait_writable(foo) +54 54 | +55 55 | async with await trio.open_file(foo): # Ok +56 56 | pass + +TRIO105.py:58:16: TRIO105 [*] Call to `trio.open_file` is not immediately awaited + | +56 | pass +57 | +58 | async with trio.open_file(foo): # TRIO105 + | ^^^^^^^^^^^^^^^^^^^ TRIO105 +59 | pass + | + = help: Add `await` + +ℹ Suggested fix +55 55 | async with await trio.open_file(foo): # Ok +56 56 | pass +57 57 | +58 |- async with trio.open_file(foo): # TRIO105 + 58 |+ async with await trio.open_file(foo): # TRIO105 +59 59 | pass +60 60 | +61 61 | + +TRIO105.py:64:5: TRIO105 Call to `trio.open_file` is not immediately awaited + | +62 | def func() -> None: +63 | # TRIO105 (without fix) +64 | trio.open_file(foo) + | ^^^^^^^^^^^^^^^^^^^ TRIO105 + | + = help: Add `await` + + diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index c3c2b71254..4fd349316c 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -10,6 +10,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::call_path::CallPath; use crate::parenthesize::parenthesized_range; use crate::statement_visitor::{walk_body, walk_stmt, StatementVisitor}; +use crate::visitor::Visitor; use crate::AnyNodeRef; use crate::{ self as ast, Arguments, CmpOp, ExceptHandler, Expr, MatchCase, Pattern, Stmt, TypeParam, @@ -931,6 +932,29 @@ where } } +/// A [`Visitor`] that detects the presence of `await` expressions in the current scope. +#[derive(Debug, Default)] +pub struct AwaitVisitor { + pub seen_await: bool, +} + +impl Visitor<'_> for AwaitVisitor { + fn visit_stmt(&mut self, stmt: &Stmt) { + match stmt { + Stmt::FunctionDef(_) | Stmt::ClassDef(_) => (), + _ => crate::visitor::walk_stmt(self, stmt), + } + } + + fn visit_expr(&mut self, expr: &Expr) { + if let Expr::Await(ast::ExprAwait { .. }) = expr { + self.seen_await = true; + } else { + crate::visitor::walk_expr(self, expr); + } + } +} + /// Return `true` if a `Stmt` is a docstring. pub fn is_docstring_stmt(stmt: &Stmt) -> bool { if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt { diff --git a/ruff.schema.json b/ruff.schema.json index 1886b55e83..adad852f9c 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3472,6 +3472,7 @@ "TRIO1", "TRIO10", "TRIO100", + "TRIO105", "TRY", "TRY0", "TRY00",