mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
[flake8-async
] Implement blocking-http-call-httpx
(ASYNC212
) (#20091)
## Summary Adds new rule to find and report use of `httpx.Client` in synchronous functions. See issue #8451 ## Test Plan New snapshots for `ASYNC212.py` with `cargo insta test`.
This commit is contained in:
parent
d75ef3823c
commit
af259faed5
8 changed files with 396 additions and 0 deletions
75
crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC212.py
vendored
Normal file
75
crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC212.py
vendored
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
def foo():
|
||||||
|
client = httpx.Client()
|
||||||
|
client.close() # Ok
|
||||||
|
client.delete() # Ok
|
||||||
|
client.get() # Ok
|
||||||
|
client.head() # Ok
|
||||||
|
client.options() # Ok
|
||||||
|
client.patch() # Ok
|
||||||
|
client.post() # Ok
|
||||||
|
client.put() # Ok
|
||||||
|
client.request() # Ok
|
||||||
|
client.send() # Ok
|
||||||
|
client.stream() # Ok
|
||||||
|
|
||||||
|
client.anything() # Ok
|
||||||
|
client.build_request() # Ok
|
||||||
|
client.is_closed # Ok
|
||||||
|
|
||||||
|
|
||||||
|
async def foo():
|
||||||
|
client = httpx.Client()
|
||||||
|
client.close() # ASYNC212
|
||||||
|
client.delete() # ASYNC212
|
||||||
|
client.get() # ASYNC212
|
||||||
|
client.head() # ASYNC212
|
||||||
|
client.options() # ASYNC212
|
||||||
|
client.patch() # ASYNC212
|
||||||
|
client.post() # ASYNC212
|
||||||
|
client.put() # ASYNC212
|
||||||
|
client.request() # ASYNC212
|
||||||
|
client.send() # ASYNC212
|
||||||
|
client.stream() # ASYNC212
|
||||||
|
|
||||||
|
client.anything() # Ok
|
||||||
|
client.build_request() # Ok
|
||||||
|
client.is_closed # Ok
|
||||||
|
|
||||||
|
|
||||||
|
async def foo(client: httpx.Client):
|
||||||
|
client.request() # ASYNC212
|
||||||
|
client.anything() # Ok
|
||||||
|
|
||||||
|
|
||||||
|
async def foo(client: httpx.Client | None):
|
||||||
|
client.request() # ASYNC212
|
||||||
|
client.anything() # Ok
|
||||||
|
|
||||||
|
|
||||||
|
async def foo(client: Optional[httpx.Client]):
|
||||||
|
client.request() # ASYNC212
|
||||||
|
client.anything() # Ok
|
||||||
|
|
||||||
|
|
||||||
|
async def foo():
|
||||||
|
client: httpx.Client = ...
|
||||||
|
client.request() # ASYNC212
|
||||||
|
client.anything() # Ok
|
||||||
|
|
||||||
|
|
||||||
|
global_client = httpx.Client()
|
||||||
|
|
||||||
|
|
||||||
|
async def foo():
|
||||||
|
global_client.request() # ASYNC212
|
||||||
|
global_client.anything() # Ok
|
||||||
|
|
||||||
|
|
||||||
|
async def foo():
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
await client.get() # Ok
|
|
@ -660,6 +660,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||||
if checker.is_rule_enabled(Rule::BlockingHttpCallInAsyncFunction) {
|
if checker.is_rule_enabled(Rule::BlockingHttpCallInAsyncFunction) {
|
||||||
flake8_async::rules::blocking_http_call(checker, call);
|
flake8_async::rules::blocking_http_call(checker, call);
|
||||||
}
|
}
|
||||||
|
if checker.is_rule_enabled(Rule::BlockingHttpCallHttpxInAsyncFunction) {
|
||||||
|
flake8_async::rules::blocking_http_call_httpx(checker, call);
|
||||||
|
}
|
||||||
if checker.is_rule_enabled(Rule::BlockingOpenCallInAsyncFunction) {
|
if checker.is_rule_enabled(Rule::BlockingOpenCallInAsyncFunction) {
|
||||||
flake8_async::rules::blocking_open_call(checker, call);
|
flake8_async::rules::blocking_open_call(checker, call);
|
||||||
}
|
}
|
||||||
|
|
|
@ -336,6 +336,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||||
(Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncZeroSleep),
|
(Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncZeroSleep),
|
||||||
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::LongSleepNotForever),
|
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::LongSleepNotForever),
|
||||||
(Flake8Async, "210") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
|
(Flake8Async, "210") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
|
||||||
|
(Flake8Async, "212") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingHttpCallHttpxInAsyncFunction),
|
||||||
(Flake8Async, "220") => (RuleGroup::Stable, rules::flake8_async::rules::CreateSubprocessInAsyncFunction),
|
(Flake8Async, "220") => (RuleGroup::Stable, rules::flake8_async::rules::CreateSubprocessInAsyncFunction),
|
||||||
(Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction),
|
(Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction),
|
||||||
(Flake8Async, "222") => (RuleGroup::Stable, rules::flake8_async::rules::WaitForProcessInAsyncFunction),
|
(Flake8Async, "222") => (RuleGroup::Stable, rules::flake8_async::rules::WaitForProcessInAsyncFunction),
|
||||||
|
|
|
@ -23,6 +23,7 @@ mod tests {
|
||||||
#[test_case(Rule::AsyncZeroSleep, Path::new("ASYNC115.py"))]
|
#[test_case(Rule::AsyncZeroSleep, Path::new("ASYNC115.py"))]
|
||||||
#[test_case(Rule::LongSleepNotForever, Path::new("ASYNC116.py"))]
|
#[test_case(Rule::LongSleepNotForever, Path::new("ASYNC116.py"))]
|
||||||
#[test_case(Rule::BlockingHttpCallInAsyncFunction, Path::new("ASYNC210.py"))]
|
#[test_case(Rule::BlockingHttpCallInAsyncFunction, Path::new("ASYNC210.py"))]
|
||||||
|
#[test_case(Rule::BlockingHttpCallHttpxInAsyncFunction, Path::new("ASYNC212.py"))]
|
||||||
#[test_case(Rule::CreateSubprocessInAsyncFunction, Path::new("ASYNC22x.py"))]
|
#[test_case(Rule::CreateSubprocessInAsyncFunction, Path::new("ASYNC22x.py"))]
|
||||||
#[test_case(Rule::RunProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
|
#[test_case(Rule::RunProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
|
||||||
#[test_case(Rule::WaitForProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
|
#[test_case(Rule::WaitForProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
use ruff_python_ast::{self as ast, Expr, ExprCall};
|
||||||
|
|
||||||
|
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||||
|
use ruff_python_semantic::analyze::typing::{TypeChecker, check_type, traverse_union_and_optional};
|
||||||
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
|
use crate::Violation;
|
||||||
|
use crate::checkers::ast::Checker;
|
||||||
|
|
||||||
|
/// ## What it does
|
||||||
|
/// Checks that async functions do not use blocking httpx clients.
|
||||||
|
///
|
||||||
|
/// ## 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 using the blocking `httpx` client, use the asynchronous client.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```python
|
||||||
|
/// import httpx
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// async def fetch():
|
||||||
|
/// client = httpx.Client()
|
||||||
|
/// response = client.get(...)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Use instead:
|
||||||
|
/// ```python
|
||||||
|
/// import httpx
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// async def fetch():
|
||||||
|
/// async with httpx.AsyncClient() as client:
|
||||||
|
/// response = await client.get(...)
|
||||||
|
/// ```
|
||||||
|
#[derive(ViolationMetadata)]
|
||||||
|
pub(crate) struct BlockingHttpCallHttpxInAsyncFunction {
|
||||||
|
name: String,
|
||||||
|
call: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Violation for BlockingHttpCallHttpxInAsyncFunction {
|
||||||
|
#[derive_message_formats]
|
||||||
|
fn message(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"Blocking httpx method {name}.{call}() in async context, use httpx.AsyncClient",
|
||||||
|
name = self.name,
|
||||||
|
call = self.call,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HttpxClientChecker;
|
||||||
|
|
||||||
|
impl TypeChecker for HttpxClientChecker {
|
||||||
|
fn match_annotation(
|
||||||
|
annotation: &ruff_python_ast::Expr,
|
||||||
|
semantic: &ruff_python_semantic::SemanticModel,
|
||||||
|
) -> bool {
|
||||||
|
// match base annotation directly
|
||||||
|
if semantic
|
||||||
|
.resolve_qualified_name(annotation)
|
||||||
|
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["httpx", "Client"]))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise traverse any union or optional annotation
|
||||||
|
let mut found = false;
|
||||||
|
traverse_union_and_optional(
|
||||||
|
&mut |inner_expr, _| {
|
||||||
|
if semantic
|
||||||
|
.resolve_qualified_name(inner_expr)
|
||||||
|
.is_some_and(|qualified_name| {
|
||||||
|
matches!(qualified_name.segments(), ["httpx", "Client"])
|
||||||
|
})
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
semantic,
|
||||||
|
annotation,
|
||||||
|
);
|
||||||
|
found
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_initializer(
|
||||||
|
initializer: &ruff_python_ast::Expr,
|
||||||
|
semantic: &ruff_python_semantic::SemanticModel,
|
||||||
|
) -> bool {
|
||||||
|
let Expr::Call(ExprCall { func, .. }) = initializer else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
semantic
|
||||||
|
.resolve_qualified_name(func)
|
||||||
|
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["httpx", "Client"]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ASYNC212
|
||||||
|
pub(crate) fn blocking_http_call_httpx(checker: &Checker, call: &ExprCall) {
|
||||||
|
let semantic = checker.semantic();
|
||||||
|
if !semantic.in_async_context() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(ast::ExprAttribute { value, attr, .. }) = call.func.as_attribute_expr() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(name) = value.as_name_expr() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if check_type::<HttpxClientChecker>(binding, semantic) {
|
||||||
|
if matches!(
|
||||||
|
attr.id.as_str(),
|
||||||
|
"close"
|
||||||
|
| "delete"
|
||||||
|
| "get"
|
||||||
|
| "head"
|
||||||
|
| "options"
|
||||||
|
| "patch"
|
||||||
|
| "post"
|
||||||
|
| "put"
|
||||||
|
| "request"
|
||||||
|
| "send"
|
||||||
|
| "stream"
|
||||||
|
) {
|
||||||
|
checker.report_diagnostic(
|
||||||
|
BlockingHttpCallHttpxInAsyncFunction {
|
||||||
|
name: name.id.to_string(),
|
||||||
|
call: attr.id.to_string(),
|
||||||
|
},
|
||||||
|
call.func.range(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ pub(crate) use async_busy_wait::*;
|
||||||
pub(crate) use async_function_with_timeout::*;
|
pub(crate) use async_function_with_timeout::*;
|
||||||
pub(crate) use async_zero_sleep::*;
|
pub(crate) use async_zero_sleep::*;
|
||||||
pub(crate) use blocking_http_call::*;
|
pub(crate) use blocking_http_call::*;
|
||||||
|
pub(crate) use blocking_http_call_httpx::*;
|
||||||
pub(crate) use blocking_open_call::*;
|
pub(crate) use blocking_open_call::*;
|
||||||
pub(crate) use blocking_process_invocation::*;
|
pub(crate) use blocking_process_invocation::*;
|
||||||
pub(crate) use blocking_sleep::*;
|
pub(crate) use blocking_sleep::*;
|
||||||
|
@ -13,6 +14,7 @@ mod async_busy_wait;
|
||||||
mod async_function_with_timeout;
|
mod async_function_with_timeout;
|
||||||
mod async_zero_sleep;
|
mod async_zero_sleep;
|
||||||
mod blocking_http_call;
|
mod blocking_http_call;
|
||||||
|
mod blocking_http_call_httpx;
|
||||||
mod blocking_open_call;
|
mod blocking_open_call;
|
||||||
mod blocking_process_invocation;
|
mod blocking_process_invocation;
|
||||||
mod blocking_sleep;
|
mod blocking_sleep;
|
||||||
|
|
|
@ -0,0 +1,168 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||||
|
---
|
||||||
|
ASYNC212 Blocking httpx method client.close() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:27:5
|
||||||
|
|
|
||||||
|
25 | async def foo():
|
||||||
|
26 | client = httpx.Client()
|
||||||
|
27 | client.close() # ASYNC212
|
||||||
|
| ^^^^^^^^^^^^
|
||||||
|
28 | client.delete() # ASYNC212
|
||||||
|
29 | client.get() # ASYNC212
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method client.delete() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:28:5
|
||||||
|
|
|
||||||
|
26 | client = httpx.Client()
|
||||||
|
27 | client.close() # ASYNC212
|
||||||
|
28 | client.delete() # ASYNC212
|
||||||
|
| ^^^^^^^^^^^^^
|
||||||
|
29 | client.get() # ASYNC212
|
||||||
|
30 | client.head() # ASYNC212
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method client.get() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:29:5
|
||||||
|
|
|
||||||
|
27 | client.close() # ASYNC212
|
||||||
|
28 | client.delete() # ASYNC212
|
||||||
|
29 | client.get() # ASYNC212
|
||||||
|
| ^^^^^^^^^^
|
||||||
|
30 | client.head() # ASYNC212
|
||||||
|
31 | client.options() # ASYNC212
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method client.head() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:30:5
|
||||||
|
|
|
||||||
|
28 | client.delete() # ASYNC212
|
||||||
|
29 | client.get() # ASYNC212
|
||||||
|
30 | client.head() # ASYNC212
|
||||||
|
| ^^^^^^^^^^^
|
||||||
|
31 | client.options() # ASYNC212
|
||||||
|
32 | client.patch() # ASYNC212
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method client.options() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:31:5
|
||||||
|
|
|
||||||
|
29 | client.get() # ASYNC212
|
||||||
|
30 | client.head() # ASYNC212
|
||||||
|
31 | client.options() # ASYNC212
|
||||||
|
| ^^^^^^^^^^^^^^
|
||||||
|
32 | client.patch() # ASYNC212
|
||||||
|
33 | client.post() # ASYNC212
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method client.patch() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:32:5
|
||||||
|
|
|
||||||
|
30 | client.head() # ASYNC212
|
||||||
|
31 | client.options() # ASYNC212
|
||||||
|
32 | client.patch() # ASYNC212
|
||||||
|
| ^^^^^^^^^^^^
|
||||||
|
33 | client.post() # ASYNC212
|
||||||
|
34 | client.put() # ASYNC212
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method client.post() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:33:5
|
||||||
|
|
|
||||||
|
31 | client.options() # ASYNC212
|
||||||
|
32 | client.patch() # ASYNC212
|
||||||
|
33 | client.post() # ASYNC212
|
||||||
|
| ^^^^^^^^^^^
|
||||||
|
34 | client.put() # ASYNC212
|
||||||
|
35 | client.request() # ASYNC212
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method client.put() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:34:5
|
||||||
|
|
|
||||||
|
32 | client.patch() # ASYNC212
|
||||||
|
33 | client.post() # ASYNC212
|
||||||
|
34 | client.put() # ASYNC212
|
||||||
|
| ^^^^^^^^^^
|
||||||
|
35 | client.request() # ASYNC212
|
||||||
|
36 | client.send() # ASYNC212
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:35:5
|
||||||
|
|
|
||||||
|
33 | client.post() # ASYNC212
|
||||||
|
34 | client.put() # ASYNC212
|
||||||
|
35 | client.request() # ASYNC212
|
||||||
|
| ^^^^^^^^^^^^^^
|
||||||
|
36 | client.send() # ASYNC212
|
||||||
|
37 | client.stream() # ASYNC212
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method client.send() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:36:5
|
||||||
|
|
|
||||||
|
34 | client.put() # ASYNC212
|
||||||
|
35 | client.request() # ASYNC212
|
||||||
|
36 | client.send() # ASYNC212
|
||||||
|
| ^^^^^^^^^^^
|
||||||
|
37 | client.stream() # ASYNC212
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method client.stream() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:37:5
|
||||||
|
|
|
||||||
|
35 | client.request() # ASYNC212
|
||||||
|
36 | client.send() # ASYNC212
|
||||||
|
37 | client.stream() # ASYNC212
|
||||||
|
| ^^^^^^^^^^^^^
|
||||||
|
38 |
|
||||||
|
39 | client.anything() # Ok
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:45:5
|
||||||
|
|
|
||||||
|
44 | async def foo(client: httpx.Client):
|
||||||
|
45 | client.request() # ASYNC212
|
||||||
|
| ^^^^^^^^^^^^^^
|
||||||
|
46 | client.anything() # Ok
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:50:5
|
||||||
|
|
|
||||||
|
49 | async def foo(client: httpx.Client | None):
|
||||||
|
50 | client.request() # ASYNC212
|
||||||
|
| ^^^^^^^^^^^^^^
|
||||||
|
51 | client.anything() # Ok
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:55:5
|
||||||
|
|
|
||||||
|
54 | async def foo(client: Optional[httpx.Client]):
|
||||||
|
55 | client.request() # ASYNC212
|
||||||
|
| ^^^^^^^^^^^^^^
|
||||||
|
56 | client.anything() # Ok
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:61:5
|
||||||
|
|
|
||||||
|
59 | async def foo():
|
||||||
|
60 | client: httpx.Client = ...
|
||||||
|
61 | client.request() # ASYNC212
|
||||||
|
| ^^^^^^^^^^^^^^
|
||||||
|
62 | client.anything() # Ok
|
||||||
|
|
|
||||||
|
|
||||||
|
ASYNC212 Blocking httpx method global_client.request() in async context, use httpx.AsyncClient
|
||||||
|
--> ASYNC212.py:69:5
|
||||||
|
|
|
||||||
|
68 | async def foo():
|
||||||
|
69 | global_client.request() # ASYNC212
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
70 | global_client.anything() # Ok
|
||||||
|
|
|
1
ruff.schema.json
generated
1
ruff.schema.json
generated
|
@ -3013,6 +3013,7 @@
|
||||||
"ASYNC2",
|
"ASYNC2",
|
||||||
"ASYNC21",
|
"ASYNC21",
|
||||||
"ASYNC210",
|
"ASYNC210",
|
||||||
|
"ASYNC212",
|
||||||
"ASYNC22",
|
"ASYNC22",
|
||||||
"ASYNC220",
|
"ASYNC220",
|
||||||
"ASYNC221",
|
"ASYNC221",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue