Implement flake8-async plugin (#4432)

This commit is contained in:
qdegraaf 2023-05-15 15:15:28 +02:00 committed by GitHub
parent 2c6efc2f5f
commit 8ba9eb83af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 495 additions and 3 deletions

View file

@ -0,0 +1,23 @@
import urllib.request
import requests
import httpx
async def foo():
urllib.request.urlopen("http://example.com/foo/bar").read()
async def foo():
requests.get()
async def foo():
httpx.get()
async def foo():
requests.post()
async def foo():
httpx.post()

View file

@ -0,0 +1,31 @@
import os
import subprocess
import time
async def foo():
open("foo")
async def foo():
time.sleep(1)
async def foo():
subprocess.run("foo")
async def foo():
subprocess.call("foo")
async def foo():
subprocess.foo(0)
async def foo():
os.wait4(10)
async def foo():
os.wait(12)

View file

@ -0,0 +1,13 @@
import os
async def foo():
os.popen()
async def foo():
os.spawnl()
async def foo():
os.fspath("foo")

View file

@ -42,9 +42,9 @@ use crate::importer::Importer;
use crate::noqa::NoqaMapping;
use crate::registry::{AsRule, Rule};
use crate::rules::{
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
flake8_django, flake8_errmsg, flake8_future_annotations, flake8_gettext,
flake8_2020, flake8_annotations, flake8_async, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez,
flake8_debugger, flake8_django, flake8_errmsg, flake8_future_annotations, flake8_gettext,
flake8_implicit_str_concat, flake8_import_conventions, flake8_logging_format, flake8_pie,
flake8_print, flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_self,
flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments,
@ -2584,6 +2584,29 @@ where
pyupgrade::rules::use_pep604_isinstance(self, expr, func, args);
}
// flake8-async
if self
.settings
.rules
.enabled(Rule::BlockingHttpCallInAsyncFunction)
{
flake8_async::rules::blocking_http_call(self, expr);
}
if self
.settings
.rules
.enabled(Rule::OpenSleepOrSubprocessInAsyncFunction)
{
flake8_async::rules::open_sleep_or_subprocess_call(self, expr);
}
if self
.settings
.rules
.enabled(Rule::BlockingOsCallInAsyncFunction)
{
flake8_async::rules::blocking_os_call(self, expr);
}
// flake8-print
if self
.settings

View file

@ -215,6 +215,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
(Pylint, "W3301") => Rule::NestedMinMax,
(Pylint, "E0241") => Rule::DuplicateBases,
// flake8-async
(Flake8Async, "100") => Rule::BlockingHttpCallInAsyncFunction,
(Flake8Async, "101") => Rule::OpenSleepOrSubprocessInAsyncFunction,
(Flake8Async, "102") => Rule::BlockingOsCallInAsyncFunction,
// flake8-builtins
(Flake8Builtins, "001") => Rule::BuiltinVariableShadowing,
(Flake8Builtins, "002") => Rule::BuiltinArgumentShadowing,

View file

@ -191,6 +191,10 @@ ruff_macros::register_rules!(
rules::pylint::rules::UnexpectedSpecialMethodSignature,
rules::pylint::rules::NestedMinMax,
rules::pylint::rules::DuplicateBases,
// flake8-async
rules::flake8_async::rules::BlockingHttpCallInAsyncFunction,
rules::flake8_async::rules::OpenSleepOrSubprocessInAsyncFunction,
rules::flake8_async::rules::BlockingOsCallInAsyncFunction,
// flake8-builtins
rules::flake8_builtins::rules::BuiltinVariableShadowing,
rules::flake8_builtins::rules::BuiltinArgumentShadowing,
@ -735,6 +739,9 @@ pub enum Linter {
/// [flake8-annotations](https://pypi.org/project/flake8-annotations/)
#[prefix = "ANN"]
Flake8Annotations,
/// [flake8-async](https://pypi.org/project/flake8-async/)
#[prefix = "ASYNC"]
Flake8Async,
/// [flake8-bandit](https://pypi.org/project/flake8-bandit/)
#[prefix = "S"]
Flake8Bandit,

View file

@ -0,0 +1,28 @@
//! Rules from [flake8-async](https://pypi.org/project/flake8-async/).
pub(crate) mod rules;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::assert_messages;
use crate::registry::Rule;
use crate::settings::Settings;
use crate::test::test_path;
#[test_case(Rule::BlockingHttpCallInAsyncFunction, Path::new("ASYNC100.py"); "ASYNC100")]
#[test_case(Rule::OpenSleepOrSubprocessInAsyncFunction, Path::new("ASYNC101.py"); "ASYNC101")]
#[test_case(Rule::BlockingOsCallInAsyncFunction, Path::new("ASYNC102.py"); "ASYNC102")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_async").join(path).as_path(),
&Settings::for_rule(rule_code),
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}

View file

@ -0,0 +1,224 @@
use rustpython_parser::ast;
use rustpython_parser::ast::{Expr, ExprKind};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::context::Context;
use ruff_python_semantic::scope::{FunctionDef, ScopeKind};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks that async functions do not contain blocking HTTP calls.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking HTTP call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// HTTP response, negating the benefits of asynchronous programming.
///
/// Instead of making a blocking HTTP call, use an asynchronous HTTP client
/// library such as `aiohttp` or `httpx`.
///
/// ## Example
/// ```python
/// async def fetch():
/// urllib.request.urlopen("https://example.com/foo/bar").read()
/// ```
///
/// Use instead:
/// ```python
/// async def fetch():
/// async with aiohttp.ClientSession() as session:
/// async with session.get("https://example.com/foo/bar") as resp:
/// ...
/// ```
#[violation]
pub struct BlockingHttpCallInAsyncFunction;
impl Violation for BlockingHttpCallInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not call blocking HTTP methods")
}
}
const BLOCKING_HTTP_CALLS: &[&[&str]] = &[
&["urllib", "request", "urlopen"],
&["httpx", "get"],
&["httpx", "post"],
&["httpx", "delete"],
&["httpx", "patch"],
&["httpx", "put"],
&["httpx", "head"],
&["httpx", "connect"],
&["httpx", "options"],
&["httpx", "trace"],
&["requests", "get"],
&["requests", "post"],
&["requests", "delete"],
&["requests", "patch"],
&["requests", "put"],
&["requests", "head"],
&["requests", "connect"],
&["requests", "options"],
&["requests", "trace"],
];
/// ASYNC100
pub(crate) fn blocking_http_call(checker: &mut Checker, expr: &Expr) {
if in_async_function(&checker.ctx) {
if let ExprKind::Call(ast::ExprCall { func, .. }) = &expr.node {
if let Some(call_path) = checker.ctx.resolve_call_path(func) {
if BLOCKING_HTTP_CALLS.contains(&call_path.as_slice()) {
checker
.diagnostics
.push(Diagnostic::new(BlockingHttpCallInAsyncFunction, func.range));
}
}
}
}
}
/// ## What it does
/// Checks that async functions do not contain calls to `open`, `time.sleep`,
/// or `subprocess` methods.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// call to complete, negating the benefits of asynchronous programming.
///
/// Instead of making a blocking call, use an equivalent asynchronous library
/// or function.
///
/// ## Example
/// ```python
/// async def foo():
/// time.sleep(1000)
/// ```
///
/// Use instead:
/// ```python
/// async def foo():
/// await asyncio.sleep(1000)
/// ```
#[violation]
pub struct OpenSleepOrSubprocessInAsyncFunction;
impl Violation for OpenSleepOrSubprocessInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not call `open`, `time.sleep`, or `subprocess` methods")
}
}
const OPEN_SLEEP_OR_SUBPROCESS_CALL: &[&[&str]] = &[
&["", "open"],
&["time", "sleep"],
&["subprocess", "run"],
&["subprocess", "Popen"],
// Deprecated subprocess calls:
&["subprocess", "call"],
&["subprocess", "check_call"],
&["subprocess", "check_output"],
&["subprocess", "getoutput"],
&["subprocess", "getstatusoutput"],
&["os", "wait"],
&["os", "wait3"],
&["os", "wait4"],
&["os", "waitid"],
&["os", "waitpid"],
];
/// ASYNC101
pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr) {
if in_async_function(&checker.ctx) {
if let ExprKind::Call(ast::ExprCall { func, .. }) = &expr.node {
if let Some(call_path) = checker.ctx.resolve_call_path(func) {
if OPEN_SLEEP_OR_SUBPROCESS_CALL.contains(&call_path.as_slice()) {
checker.diagnostics.push(Diagnostic::new(
OpenSleepOrSubprocessInAsyncFunction,
func.range,
));
}
}
}
}
}
/// ## What it does
/// Checks that async functions do not contain calls to blocking synchronous
/// process calls via the `os` module.
///
/// ## Why is this bad?
/// Blocking an async function via a blocking call will block the entire
/// event loop, preventing it from executing other tasks while waiting for the
/// call to complete, negating the benefits of asynchronous programming.
///
/// Instead of making a blocking call, use an equivalent asynchronous library
/// or function.
///
/// ## Example
/// ```python
/// async def foo():
/// os.popen()
/// ```
///
/// Use instead:
/// ```python
/// def foo():
/// os.popen()
/// ```
#[violation]
pub struct BlockingOsCallInAsyncFunction;
impl Violation for BlockingOsCallInAsyncFunction {
#[derive_message_formats]
fn message(&self) -> String {
format!("Async functions should not call synchronous `os` methods")
}
}
const UNSAFE_OS_METHODS: &[&[&str]] = &[
&["os", "popen"],
&["os", "posix_spawn"],
&["os", "posix_spawnp"],
&["os", "spawnl"],
&["os", "spawnle"],
&["os", "spawnlp"],
&["os", "spawnlpe"],
&["os", "spawnv"],
&["os", "spawnve"],
&["os", "spawnvp"],
&["os", "spawnvpe"],
&["os", "system"],
];
/// ASYNC102
pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) {
if in_async_function(&checker.ctx) {
if let ExprKind::Call(ast::ExprCall { func, .. }) = &expr.node {
if let Some(call_path) = checker.ctx.resolve_call_path(func) {
if UNSAFE_OS_METHODS.contains(&call_path.as_slice()) {
checker
.diagnostics
.push(Diagnostic::new(BlockingOsCallInAsyncFunction, func.range));
}
}
}
}
}
/// Return `true` if the [`Context`] is inside an async function definition.
fn in_async_function(context: &Context) -> bool {
context
.scopes()
.find_map(|scope| {
if let ScopeKind::Function(FunctionDef { async_, .. }) = &scope.kind {
Some(*async_)
} else {
None
}
})
.unwrap_or(false)
}

View file

@ -0,0 +1,39 @@
---
source: crates/ruff/src/rules/flake8_async/mod.rs
---
ASYNC100.py:7:5: ASYNC100 Async functions should not call blocking HTTP methods
|
7 | async def foo():
8 | urllib.request.urlopen("http://example.com/foo/bar").read()
| ^^^^^^^^^^^^^^^^^^^^^^ ASYNC100
|
ASYNC100.py:11:5: ASYNC100 Async functions should not call blocking HTTP methods
|
11 | async def foo():
12 | requests.get()
| ^^^^^^^^^^^^ ASYNC100
|
ASYNC100.py:15:5: ASYNC100 Async functions should not call blocking HTTP methods
|
15 | async def foo():
16 | httpx.get()
| ^^^^^^^^^ ASYNC100
|
ASYNC100.py:19:5: ASYNC100 Async functions should not call blocking HTTP methods
|
19 | async def foo():
20 | requests.post()
| ^^^^^^^^^^^^^ ASYNC100
|
ASYNC100.py:23:5: ASYNC100 Async functions should not call blocking HTTP methods
|
23 | async def foo():
24 | httpx.post()
| ^^^^^^^^^^ ASYNC100
|

View file

@ -0,0 +1,46 @@
---
source: crates/ruff/src/rules/flake8_async/mod.rs
---
ASYNC101.py:7:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
7 | async def foo():
8 | open("foo")
| ^^^^ ASYNC101
|
ASYNC101.py:11:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
11 | async def foo():
12 | time.sleep(1)
| ^^^^^^^^^^ ASYNC101
|
ASYNC101.py:15:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
15 | async def foo():
16 | subprocess.run("foo")
| ^^^^^^^^^^^^^^ ASYNC101
|
ASYNC101.py:19:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
19 | async def foo():
20 | subprocess.call("foo")
| ^^^^^^^^^^^^^^^ ASYNC101
|
ASYNC101.py:27:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
27 | async def foo():
28 | os.wait4(10)
| ^^^^^^^^ ASYNC101
|
ASYNC101.py:31:5: ASYNC101 Async functions should not call `open`, `time.sleep`, or `subprocess` methods
|
31 | async def foo():
32 | os.wait(12)
| ^^^^^^^ ASYNC101
|

View file

@ -0,0 +1,18 @@
---
source: crates/ruff/src/rules/flake8_async/mod.rs
---
ASYNC102.py:5:5: ASYNC102 Async functions should not call synchronous `os` methods
|
5 | async def foo():
6 | os.popen()
| ^^^^^^^^ ASYNC102
|
ASYNC102.py:9:5: ASYNC102 Async functions should not call synchronous `os` methods
|
9 | async def foo():
10 | os.spawnl()
| ^^^^^^^^^ ASYNC102
|

View file

@ -2,6 +2,7 @@
pub mod eradicate;
pub mod flake8_2020;
pub mod flake8_annotations;
pub mod flake8_async;
pub mod flake8_bandit;
pub mod flake8_blind_except;
pub mod flake8_boolean_trap;