[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`.
This commit is contained in:
Amethyst Reese 2025-08-28 11:04:24 -07:00 committed by GitHub
parent 166b63ad4d
commit ca1f66a657
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 111 additions and 0 deletions

View file

@ -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

View file

@ -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);
}

View file

@ -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

View file

@ -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());

View file

@ -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());
}
}
}

View file

@ -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;

View file

@ -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
|