From ca1f66a65712ba66e5a22f2104d33464af6f3ab7 Mon Sep 17 00:00:00 2001 From: Amethyst Reese Date: Thu, 28 Aug 2025 11:04:24 -0700 Subject: [PATCH] [`flake8-async`] Implement `blocking-input` rule (`ASYNC250`) (#20122) ## Summary Adds new rule to catch use of builtins `input()` in async functions. Issue #8451 ## Test Plan New snapshosts in `ASYNC250.py` with `cargo insta test`. --- .../test/fixtures/flake8_async/ASYNC250.py | 22 ++++++++ .../src/checkers/ast/analyze/expression.rs | 3 ++ crates/ruff_linter/src/codes.rs | 1 + .../ruff_linter/src/rules/flake8_async/mod.rs | 1 + .../flake8_async/rules/blocking_input.rs | 52 +++++++++++++++++++ .../src/rules/flake8_async/rules/mod.rs | 2 + ...e8_async__tests__ASYNC250_ASYNC250.py.snap | 29 +++++++++++ ruff.schema.json | 1 + 8 files changed, 111 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC250.py create mode 100644 crates/ruff_linter/src/rules/flake8_async/rules/blocking_input.rs create mode 100644 crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC250_ASYNC250.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC250.py b/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC250.py new file mode 100644 index 0000000000..c5b3e1fd61 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC250.py @@ -0,0 +1,22 @@ +def foo(): + k = input() # Ok + input("hello world") # Ok + + +async def foo(): + k = input() # ASYNC250 + input("hello world") # ASYNC250 + + +import builtins + +import fake + + +def foo(): + builtins.input("testing") # Ok + + +async def foo(): + builtins.input("testing") # ASYNC250 + fake.input("whatever") # Ok diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index c2f1afe584..39c1b460ee 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -673,6 +673,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { ]) { flake8_async::rules::blocking_process_invocation(checker, call); } + if checker.is_rule_enabled(Rule::BlockingInputInAsyncFunction) { + flake8_async::rules::blocking_input(checker, call); + } if checker.is_rule_enabled(Rule::BlockingSleepInAsyncFunction) { flake8_async::rules::blocking_sleep(checker, call); } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index ebf0d7babb..e8f923289e 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -341,6 +341,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction), (Flake8Async, "222") => (RuleGroup::Stable, rules::flake8_async::rules::WaitForProcessInAsyncFunction), (Flake8Async, "230") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingOpenCallInAsyncFunction), + (Flake8Async, "250") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingInputInAsyncFunction), (Flake8Async, "251") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingSleepInAsyncFunction), // flake8-builtins diff --git a/crates/ruff_linter/src/rules/flake8_async/mod.rs b/crates/ruff_linter/src/rules/flake8_async/mod.rs index 7389abf603..bdfee91469 100644 --- a/crates/ruff_linter/src/rules/flake8_async/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_async/mod.rs @@ -28,6 +28,7 @@ mod tests { #[test_case(Rule::RunProcessInAsyncFunction, Path::new("ASYNC22x.py"))] #[test_case(Rule::WaitForProcessInAsyncFunction, Path::new("ASYNC22x.py"))] #[test_case(Rule::BlockingOpenCallInAsyncFunction, Path::new("ASYNC230.py"))] + #[test_case(Rule::BlockingInputInAsyncFunction, Path::new("ASYNC250.py"))] #[test_case(Rule::BlockingSleepInAsyncFunction, Path::new("ASYNC251.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_input.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_input.rs new file mode 100644 index 0000000000..3c5a70f92e --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_input.rs @@ -0,0 +1,52 @@ +use ruff_python_ast::ExprCall; + +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_text_size::Ranged; + +use crate::Violation; +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks that async functions do not contain blocking usage of input from user. +/// +/// ## Why is this bad? +/// Blocking an async function via a blocking input call will block the entire +/// event loop, preventing it from executing other tasks while waiting for user +/// input, negating the benefits of asynchronous programming. +/// +/// Instead of making a blocking input call directly, wrap the input call in +/// an executor to execute the blocking call on another thread. +/// +/// ## Example +/// ```python +/// async def foo(): +/// username = input("Username:") +/// ``` +/// +/// Use instead: +/// ```python +/// import asyncio +/// +/// +/// async def foo(): +/// loop = asyncio.get_running_loop() +/// username = await loop.run_in_executor(None, input, "Username:") +/// ``` +#[derive(ViolationMetadata)] +pub(crate) struct BlockingInputInAsyncFunction; + +impl Violation for BlockingInputInAsyncFunction { + #[derive_message_formats] + fn message(&self) -> String { + "Blocking call to input() in async context".to_string() + } +} + +/// ASYNC250 +pub(crate) fn blocking_input(checker: &Checker, call: &ExprCall) { + if checker.semantic().in_async_context() { + if checker.semantic().match_builtin_expr(&call.func, "input") { + checker.report_diagnostic(BlockingInputInAsyncFunction, call.func.range()); + } + } +} diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs index 32b2fa8e36..86103c5980 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs @@ -3,6 +3,7 @@ pub(crate) use async_function_with_timeout::*; pub(crate) use async_zero_sleep::*; pub(crate) use blocking_http_call::*; pub(crate) use blocking_http_call_httpx::*; +pub(crate) use blocking_input::*; pub(crate) use blocking_open_call::*; pub(crate) use blocking_process_invocation::*; pub(crate) use blocking_sleep::*; @@ -15,6 +16,7 @@ mod async_function_with_timeout; mod async_zero_sleep; mod blocking_http_call; mod blocking_http_call_httpx; +mod blocking_input; mod blocking_open_call; mod blocking_process_invocation; mod blocking_sleep; diff --git a/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC250_ASYNC250.py.snap b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC250_ASYNC250.py.snap new file mode 100644 index 0000000000..d65458efbd --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC250_ASYNC250.py.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff_linter/src/rules/flake8_async/mod.rs +--- +ASYNC250 Blocking call to input() in async context + --> ASYNC250.py:7:9 + | +6 | async def foo(): +7 | k = input() # ASYNC250 + | ^^^^^ +8 | input("hello world") # ASYNC250 + | + +ASYNC250 Blocking call to input() in async context + --> ASYNC250.py:8:5 + | +6 | async def foo(): +7 | k = input() # ASYNC250 +8 | input("hello world") # ASYNC250 + | ^^^^^ + | + +ASYNC250 Blocking call to input() in async context + --> ASYNC250.py:21:5 + | +20 | async def foo(): +21 | builtins.input("testing") # ASYNC250 + | ^^^^^^^^^^^^^^ +22 | fake.input("whatever") # Ok + | diff --git a/ruff.schema.json b/ruff.schema.json index 1b4d6ac27f..aacae163b5 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3021,6 +3021,7 @@ "ASYNC23", "ASYNC230", "ASYNC25", + "ASYNC250", "ASYNC251", "B", "B0",