Replace static `CallPath` vectors with `matches!` macros (#5148)

## Summary

After #5140, I audited the codebase for similar patterns (defining a
list of `CallPath` entities in a static vector, then looping over them
to pattern-match). This PR migrates all other such cases to use `match`
and `matches!` where possible.

There are a few benefits to this:

1. It more clearly denotes the intended semantics (branches are
exclusive).
2. The compiler can help deduplicate the patterns and detect unreachable
branches.
3. Performance: in the benchmark below, the all-rules performance is
increased by nearly 10%...

## Benchmarks

I decided to benchmark against a large file in the Airflow repository
with a lot of type annotations
([`views.py`](https://raw.githubusercontent.com/apache/airflow/f03f73100e8a7d6019249889de567cb00e71e457/airflow/www/views.py)):

```
linter/default-rules/airflow/views.py
                        time:   [10.871 ms 10.882 ms 10.894 ms]
                        thrpt:  [19.739 MiB/s 19.761 MiB/s 19.781 MiB/s]
                 change:
                        time:   [-2.7182% -2.5687% -2.4204%] (p = 0.00 < 0.05)
                        thrpt:  [+2.4805% +2.6364% +2.7942%]
                        Performance has improved.

linter/all-rules/airflow/views.py
                        time:   [24.021 ms 24.038 ms 24.062 ms]
                        thrpt:  [8.9373 MiB/s 8.9461 MiB/s 8.9527 MiB/s]
                 change:
                        time:   [-8.9537% -8.8516% -8.7527%] (p = 0.00 < 0.05)
                        thrpt:  [+9.5923% +9.7112% +9.8342%]
                        Performance has improved.
Found 12 outliers among 100 measurements (12.00%)
  5 (5.00%) high mild
  7 (7.00%) high severe
```

The impact is dramatic -- nearly a 10% improvement for `all-rules`.
This commit is contained in:
Charlie Marsh 2023-06-16 13:34:42 -04:00 committed by GitHub
parent b3240dbfa2
commit d0ad1ed0af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 641 additions and 584 deletions

View File

@ -1,6 +1,6 @@
use rustpython_parser::ast::{Expr, Ranged, Stmt}; use rustpython_parser::ast::{Expr, Ranged, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Violation}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::cast; use ruff_python_ast::cast;
use ruff_python_ast::helpers::ReturnStatementVisitor; use ruff_python_ast::helpers::ReturnStatementVisitor;
@ -8,7 +8,7 @@ use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::statement_visitor::StatementVisitor; use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel}; use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel};
use ruff_python_stdlib::typing::SIMPLE_MAGIC_RETURN_TYPES; use ruff_python_stdlib::typing::simple_magic_return_type;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule}; use crate::registry::{AsRule, Rule};
@ -667,9 +667,9 @@ pub(crate) fn definition(
stmt.identifier(checker.locator), stmt.identifier(checker.locator),
); );
if checker.patch(diagnostic.kind.rule()) { if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)] diagnostic.try_set_fix(|| {
diagnostic.try_set_fix_from_edit(|| {
fixes::add_return_annotation(checker.locator, stmt, "None") fixes::add_return_annotation(checker.locator, stmt, "None")
.map(Fix::suggested)
}); });
} }
diagnostics.push(diagnostic); diagnostics.push(diagnostic);
@ -683,12 +683,11 @@ pub(crate) fn definition(
}, },
stmt.identifier(checker.locator), stmt.identifier(checker.locator),
); );
let return_type = SIMPLE_MAGIC_RETURN_TYPES.get(name); if checker.patch(diagnostic.kind.rule()) {
if let Some(return_type) = return_type { if let Some(return_type) = simple_magic_return_type(name) {
if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::add_return_annotation(checker.locator, stmt, return_type) fixes::add_return_annotation(checker.locator, stmt, return_type)
.map(Fix::suggested)
}); });
} }
} }

View File

@ -3,6 +3,7 @@ use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -40,37 +41,35 @@ impl Violation for BlockingHttpCallInAsyncFunction {
} }
} }
const BLOCKING_HTTP_CALLS: &[&[&str]] = &[ fn is_blocking_http_call(call_path: &CallPath) -> bool {
&["urllib", "request", "urlopen"], matches!(
&["httpx", "get"], call_path.as_slice(),
&["httpx", "post"], ["urllib", "request", "urlopen"]
&["httpx", "delete"], | [
&["httpx", "patch"], "httpx" | "requests",
&["httpx", "put"], "get"
&["httpx", "head"], | "post"
&["httpx", "connect"], | "delete"
&["httpx", "options"], | "patch"
&["httpx", "trace"], | "put"
&["requests", "get"], | "head"
&["requests", "post"], | "connect"
&["requests", "delete"], | "options"
&["requests", "patch"], | "trace"
&["requests", "put"], ]
&["requests", "head"], )
&["requests", "connect"], }
&["requests", "options"],
&["requests", "trace"],
];
/// ASYNC100 /// ASYNC100
pub(crate) fn blocking_http_call(checker: &mut Checker, expr: &Expr) { pub(crate) fn blocking_http_call(checker: &mut Checker, expr: &Expr) {
if checker.semantic().in_async_context() { if checker.semantic().in_async_context() {
if let Expr::Call(ast::ExprCall { func, .. }) = expr { if let Expr::Call(ast::ExprCall { func, .. }) = expr {
let call_path = checker.semantic().resolve_call_path(func); if checker
let is_blocking = .semantic()
call_path.map_or(false, |path| BLOCKING_HTTP_CALLS.contains(&path.as_slice())); .resolve_call_path(func)
.as_ref()
if is_blocking { .map_or(false, is_blocking_http_call)
{
checker.diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
BlockingHttpCallInAsyncFunction, BlockingHttpCallInAsyncFunction,
func.range(), func.range(),

View File

@ -3,6 +3,7 @@ use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -39,31 +40,16 @@ impl Violation for BlockingOsCallInAsyncFunction {
} }
} }
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 /// ASYNC102
pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) { pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) {
if checker.semantic().in_async_context() { if checker.semantic().in_async_context() {
if let Expr::Call(ast::ExprCall { func, .. }) = expr { if let Expr::Call(ast::ExprCall { func, .. }) = expr {
let is_unsafe_os_method = checker if checker
.semantic() .semantic()
.resolve_call_path(func) .resolve_call_path(func)
.map_or(false, |path| UNSAFE_OS_METHODS.contains(&path.as_slice())); .as_ref()
.map_or(false, is_unsafe_os_method)
if is_unsafe_os_method { {
checker checker
.diagnostics .diagnostics
.push(Diagnostic::new(BlockingOsCallInAsyncFunction, func.range())); .push(Diagnostic::new(BlockingOsCallInAsyncFunction, func.range()));
@ -71,3 +57,24 @@ pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) {
} }
} }
} }
fn is_unsafe_os_method(call_path: &CallPath) -> bool {
matches!(
call_path.as_slice(),
[
"os",
"popen"
| "posix_spawn"
| "posix_spawnp"
| "spawnl"
| "spawnle"
| "spawnlp"
| "spawnlpe"
| "spawnv"
| "spawnve"
| "spawnvp"
| "spawnvpe"
| "system"
]
)
}

View File

@ -3,6 +3,7 @@ use rustpython_parser::ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -39,36 +40,16 @@ impl Violation for OpenSleepOrSubprocessInAsyncFunction {
} }
} }
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 /// ASYNC101
pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr) { pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr) {
if checker.semantic().in_async_context() { if checker.semantic().in_async_context() {
if let Expr::Call(ast::ExprCall { func, .. }) = expr { if let Expr::Call(ast::ExprCall { func, .. }) = expr {
let is_open_sleep_or_subprocess_call = checker if checker
.semantic() .semantic()
.resolve_call_path(func) .resolve_call_path(func)
.map_or(false, |path| { .as_ref()
OPEN_SLEEP_OR_SUBPROCESS_CALL.contains(&path.as_slice()) .map_or(false, is_open_sleep_or_subprocess_call)
}); {
if is_open_sleep_or_subprocess_call {
checker.diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
OpenSleepOrSubprocessInAsyncFunction, OpenSleepOrSubprocessInAsyncFunction,
func.range(), func.range(),
@ -77,3 +58,22 @@ pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr)
} }
} }
} }
fn is_open_sleep_or_subprocess_call(call_path: &CallPath) -> bool {
matches!(
call_path.as_slice(),
["", "open"]
| ["time", "sleep"]
| [
"subprocess",
"run"
| "Popen"
| "call"
| "check_call"
| "check_output"
| "getoutput"
| "getstatusoutput"
]
| ["os", "wait" | "wait3" | "wait4" | "waitid" | "waitpid"]
)
}

View File

@ -7,11 +7,10 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::{compose_call_path, from_qualified_name, CallPath}; use ruff_python_ast::call_path::{compose_call_path, from_qualified_name, CallPath};
use ruff_python_ast::visitor; use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor; use ruff_python_ast::visitor::Visitor;
use ruff_python_semantic::analyze::typing::is_immutable_func; use ruff_python_semantic::analyze::typing::{is_immutable_func, is_mutable_func};
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::rules::flake8_bugbear::rules::mutable_argument_default::is_mutable_func;
/// ## What it does /// ## What it does
/// Checks for function calls in default function arguments. /// Checks for function calls in default function arguments.

View File

@ -1,9 +1,8 @@
use rustpython_parser::ast::{self, Arguments, Expr, Ranged}; use rustpython_parser::ast::{Arguments, Ranged};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::analyze::typing::is_immutable_annotation; use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr};
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
@ -16,36 +15,6 @@ impl Violation for MutableArgumentDefault {
format!("Do not use mutable data structures for argument defaults") format!("Do not use mutable data structures for argument defaults")
} }
} }
const MUTABLE_FUNCS: &[&[&str]] = &[
&["", "dict"],
&["", "list"],
&["", "set"],
&["collections", "Counter"],
&["collections", "OrderedDict"],
&["collections", "defaultdict"],
&["collections", "deque"],
];
pub(crate) fn is_mutable_func(func: &Expr, semantic: &SemanticModel) -> bool {
semantic.resolve_call_path(func).map_or(false, |call_path| {
MUTABLE_FUNCS
.iter()
.any(|target| call_path.as_slice() == *target)
})
}
fn is_mutable_expr(expr: &Expr, semantic: &SemanticModel) -> bool {
match expr {
Expr::List(_)
| Expr::Dict(_)
| Expr::Set(_)
| Expr::ListComp(_)
| Expr::DictComp(_)
| Expr::SetComp(_) => true,
Expr::Call(ast::ExprCall { func, .. }) => is_mutable_func(func, semantic),
_ => false,
}
}
/// B006 /// B006
pub(crate) fn mutable_argument_default(checker: &mut Checker, arguments: &Arguments) { pub(crate) fn mutable_argument_default(checker: &mut Checker, arguments: &Arguments) {

View File

@ -23,59 +23,32 @@ impl Violation for Debugger {
} }
} }
const DEBUGGERS: &[&[&str]] = &[
&["pdb", "set_trace"],
&["pudb", "set_trace"],
&["ipdb", "set_trace"],
&["ipdb", "sset_trace"],
&["IPython", "terminal", "embed", "InteractiveShellEmbed"],
&[
"IPython",
"frontend",
"terminal",
"embed",
"InteractiveShellEmbed",
],
&["celery", "contrib", "rdb", "set_trace"],
&["builtins", "breakpoint"],
&["", "breakpoint"],
];
/// Checks for the presence of a debugger call. /// Checks for the presence of a debugger call.
pub(crate) fn debugger_call(checker: &mut Checker, expr: &Expr, func: &Expr) { pub(crate) fn debugger_call(checker: &mut Checker, expr: &Expr, func: &Expr) {
if let Some(target) = checker if let Some(using_type) = checker
.semantic() .semantic()
.resolve_call_path(func) .resolve_call_path(func)
.and_then(|call_path| { .and_then(|call_path| {
DEBUGGERS if is_debugger_call(&call_path) {
.iter() Some(DebuggerUsingType::Call(format_call_path(&call_path)))
.find(|target| call_path.as_slice() == **target) } else {
None
}
}) })
{ {
checker.diagnostics.push(Diagnostic::new( checker
Debugger { .diagnostics
using_type: DebuggerUsingType::Call(format_call_path(target)), .push(Diagnostic::new(Debugger { using_type }, expr.range()));
},
expr.range(),
));
} }
} }
/// Checks for the presence of a debugger import. /// Checks for the presence of a debugger import.
pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> Option<Diagnostic> { pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> Option<Diagnostic> {
// Special-case: allow `import builtins`, which is far more general than (e.g.)
// `import celery.contrib.rdb`).
if module.is_none() && name == "builtins" {
return None;
}
if let Some(module) = module { if let Some(module) = module {
let mut call_path: CallPath = from_unqualified_name(module); let mut call_path: CallPath = from_unqualified_name(module);
call_path.push(name); call_path.push(name);
if DEBUGGERS
.iter() if is_debugger_call(&call_path) {
.any(|target| call_path.as_slice() == *target)
{
return Some(Diagnostic::new( return Some(Diagnostic::new(
Debugger { Debugger {
using_type: DebuggerUsingType::Import(format_call_path(&call_path)), using_type: DebuggerUsingType::Import(format_call_path(&call_path)),
@ -84,11 +57,9 @@ pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) ->
)); ));
} }
} else { } else {
let parts: CallPath = from_unqualified_name(name); let call_path: CallPath = from_unqualified_name(name);
if DEBUGGERS
.iter() if is_debugger_import(&call_path) {
.any(|call_path| &call_path[..call_path.len() - 1] == parts.as_slice())
{
return Some(Diagnostic::new( return Some(Diagnostic::new(
Debugger { Debugger {
using_type: DebuggerUsingType::Import(name.to_string()), using_type: DebuggerUsingType::Import(name.to_string()),
@ -99,3 +70,35 @@ pub(crate) fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) ->
} }
None None
} }
fn is_debugger_call(call_path: &CallPath) -> bool {
matches!(
call_path.as_slice(),
["pdb" | "pudb" | "ipdb", "set_trace"]
| ["ipdb", "sset_trace"]
| ["IPython", "terminal", "embed", "InteractiveShellEmbed"]
| [
"IPython",
"frontend",
"terminal",
"embed",
"InteractiveShellEmbed"
]
| ["celery", "contrib", "rdb", "set_trace"]
| ["builtins" | "", "breakpoint"]
)
}
fn is_debugger_import(call_path: &CallPath) -> bool {
// Constructed by taking every pattern in `is_debugger_call`, removing the last element in
// each pattern, and de-duplicating the values.
// As a special-case, we omit `builtins` to allow `import builtins`, which is far more general
// than (e.g.) `import celery.contrib.rdb`.
matches!(
call_path.as_slice(),
["pdb" | "pudb" | "ipdb"]
| ["IPython", "terminal", "embed"]
| ["IPython", "frontend", "terminal", "embed",]
| ["celery", "contrib", "rdb"]
)
}

View File

@ -2,6 +2,7 @@ use rustpython_parser::ast::{self, Arguments, Constant, Expr, Operator, Ranged,
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::CallPath;
use ruff_python_ast::source_code::Locator; use ruff_python_ast::source_code::Locator;
use ruff_python_semantic::{ScopeKind, SemanticModel}; use ruff_python_semantic::{ScopeKind, SemanticModel};
@ -94,30 +95,33 @@ impl Violation for UnassignedSpecialVariableInStub {
} }
} }
const ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS: &[&[&str]] = &[ fn is_allowed_negated_math_attribute(call_path: &CallPath) -> bool {
&["math", "inf"], matches!(call_path.as_slice(), ["math", "inf" | "e" | "pi" | "tau"])
&["math", "nan"], }
&["math", "e"],
&["math", "pi"],
&["math", "tau"],
];
const ALLOWED_ATTRIBUTES_IN_DEFAULTS: &[&[&str]] = &[ fn is_allowed_math_attribute(call_path: &CallPath) -> bool {
&["sys", "stdin"], matches!(
&["sys", "stdout"], call_path.as_slice(),
&["sys", "stderr"], ["math", "inf" | "nan" | "e" | "pi" | "tau"]
&["sys", "version"], | [
&["sys", "version_info"], "sys",
&["sys", "platform"], "stdin"
&["sys", "executable"], | "stdout"
&["sys", "prefix"], | "stderr"
&["sys", "exec_prefix"], | "version"
&["sys", "base_prefix"], | "version_info"
&["sys", "byteorder"], | "platform"
&["sys", "maxsize"], | "executable"
&["sys", "hexversion"], | "prefix"
&["sys", "winver"], | "exec_prefix"
]; | "base_prefix"
| "byteorder"
| "maxsize"
| "hexversion"
| "winver"
]
)
}
fn is_valid_default_value_with_annotation( fn is_valid_default_value_with_annotation(
default: &Expr, default: &Expr,
@ -166,12 +170,8 @@ fn is_valid_default_value_with_annotation(
Expr::Attribute(_) => { Expr::Attribute(_) => {
if semantic if semantic
.resolve_call_path(operand) .resolve_call_path(operand)
.map_or(false, |call_path| { .as_ref()
ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS.iter().any(|target| { .map_or(false, is_allowed_negated_math_attribute)
// reject `-math.nan`
call_path.as_slice() == *target && *target != ["math", "nan"]
})
})
{ {
return true; return true;
} }
@ -219,12 +219,8 @@ fn is_valid_default_value_with_annotation(
Expr::Attribute(_) => { Expr::Attribute(_) => {
if semantic if semantic
.resolve_call_path(default) .resolve_call_path(default)
.map_or(false, |call_path| { .as_ref()
ALLOWED_MATH_ATTRIBUTES_IN_DEFAULTS .map_or(false, is_allowed_math_attribute)
.iter()
.chain(ALLOWED_ATTRIBUTES_IN_DEFAULTS.iter())
.any(|target| call_path.as_slice() == *target)
})
{ {
return true; return true;
} }

View File

@ -370,34 +370,17 @@ fn implicit_return_value(checker: &mut Checker, stack: &Stack) {
} }
} }
const NORETURN_FUNCS: &[&[&str]] = &[
// builtins
&["", "exit"],
&["", "quit"],
// stdlib
&["builtins", "exit"],
&["builtins", "quit"],
&["os", "_exit"],
&["os", "abort"],
&["posix", "_exit"],
&["posix", "abort"],
&["sys", "exit"],
&["_thread", "exit"],
&["_winapi", "ExitProcess"],
// third-party modules
&["pytest", "exit"],
&["pytest", "fail"],
&["pytest", "skip"],
&["pytest", "xfail"],
];
/// Return `true` if the `func` is a known function that never returns. /// Return `true` if the `func` is a known function that never returns.
fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool { fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool {
semantic.resolve_call_path(func).map_or(false, |call_path| { semantic.resolve_call_path(func).map_or(false, |call_path| {
NORETURN_FUNCS matches!(
.iter() call_path.as_slice(),
.any(|target| call_path.as_slice() == *target) ["" | "builtins" | "sys" | "_thread" | "pytest", "exit"]
|| semantic.match_typing_call_path(&call_path, "assert_never") | ["" | "builtins", "quit"]
| ["os" | "posix", "_exit" | "abort"]
| ["_winapi", "ExitProcess"]
| ["pytest", "fail" | "skip" | "xfail"]
) || semantic.match_typing_call_path(&call_path, "assert_never")
}) })
} }

View File

@ -8,7 +8,7 @@ use ruff_python_semantic::analyze::typing::is_immutable_func;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::rules::ruff::rules::helpers::{ use crate::rules::ruff::rules::helpers::{
is_allowed_dataclass_function, is_class_var_annotation, is_dataclass, is_class_var_annotation, is_dataclass, is_dataclass_field,
}; };
/// ## What it does /// ## What it does
@ -97,7 +97,7 @@ pub(crate) fn function_call_in_dataclass_default(
if let Expr::Call(ast::ExprCall { func, .. }) = expr.as_ref() { if let Expr::Call(ast::ExprCall { func, .. }) = expr.as_ref() {
if !is_class_var_annotation(annotation, checker.semantic()) if !is_class_var_annotation(annotation, checker.semantic())
&& !is_immutable_func(func, checker.semantic(), &extend_immutable_calls) && !is_immutable_func(func, checker.semantic(), &extend_immutable_calls)
&& !is_allowed_dataclass_function(func, checker.semantic()) && !is_dataclass_field(func, checker.semantic())
{ {
checker.diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
FunctionCallInDataclassDefaultArgument { FunctionCallInDataclassDefaultArgument {

View File

@ -1,27 +1,12 @@
use ruff_python_ast::helpers::map_callable;
use rustpython_parser::ast::{self, Expr}; use rustpython_parser::ast::{self, Expr};
use ruff_python_ast::helpers::map_callable;
use ruff_python_semantic::SemanticModel; use ruff_python_semantic::SemanticModel;
pub(super) fn is_mutable_expr(expr: &Expr) -> bool { /// Returns `true` if the given [`Expr`] is a `dataclasses.field` call.
matches!( pub(super) fn is_dataclass_field(func: &Expr, semantic: &SemanticModel) -> bool {
expr,
Expr::List(_)
| Expr::Dict(_)
| Expr::Set(_)
| Expr::ListComp(_)
| Expr::DictComp(_)
| Expr::SetComp(_)
)
}
const ALLOWED_DATACLASS_SPECIFIC_FUNCTIONS: &[&[&str]] = &[&["dataclasses", "field"]];
pub(super) fn is_allowed_dataclass_function(func: &Expr, semantic: &SemanticModel) -> bool {
semantic.resolve_call_path(func).map_or(false, |call_path| { semantic.resolve_call_path(func).map_or(false, |call_path| {
ALLOWED_DATACLASS_SPECIFIC_FUNCTIONS matches!(call_path.as_slice(), ["dataclasses", "field"])
.iter()
.any(|target| call_path.as_slice() == *target)
}) })
} }

View File

@ -2,10 +2,10 @@ use rustpython_parser::ast::{self, Ranged, Stmt};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::analyze::typing::is_immutable_annotation; use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass, is_mutable_expr}; use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass};
/// ## What it does /// ## What it does
/// Checks for mutable default values in class attributes. /// Checks for mutable default values in class attributes.
@ -52,7 +52,7 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt
value: Some(value), value: Some(value),
.. ..
}) => { }) => {
if is_mutable_expr(value) if is_mutable_expr(value, checker.semantic())
&& !is_class_var_annotation(annotation, checker.semantic()) && !is_class_var_annotation(annotation, checker.semantic())
&& !is_immutable_annotation(annotation, checker.semantic()) && !is_immutable_annotation(annotation, checker.semantic())
&& !is_dataclass(class_def, checker.semantic()) && !is_dataclass(class_def, checker.semantic())
@ -63,7 +63,7 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt
} }
} }
Stmt::Assign(ast::StmtAssign { value, .. }) => { Stmt::Assign(ast::StmtAssign { value, .. }) => {
if is_mutable_expr(value) { if is_mutable_expr(value, checker.semantic()) {
checker checker
.diagnostics .diagnostics
.push(Diagnostic::new(MutableClassDefault, value.range())); .push(Diagnostic::new(MutableClassDefault, value.range()));

View File

@ -2,10 +2,10 @@ use rustpython_parser::ast::{self, Ranged, Stmt};
use ruff_diagnostics::{Diagnostic, Violation}; use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation}; use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::analyze::typing::is_immutable_annotation; use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass, is_mutable_expr}; use crate::rules::ruff::rules::helpers::{is_class_var_annotation, is_dataclass};
/// ## What it does /// ## What it does
/// Checks for mutable default values in dataclass attributes. /// Checks for mutable default values in dataclass attributes.
@ -74,7 +74,7 @@ pub(crate) fn mutable_dataclass_default(checker: &mut Checker, class_def: &ast::
.. ..
}) = statement }) = statement
{ {
if is_mutable_expr(value) if is_mutable_expr(value, checker.semantic())
&& !is_class_var_annotation(annotation, checker.semantic()) && !is_class_var_annotation(annotation, checker.semantic())
&& !is_immutable_annotation(annotation, checker.semantic()) && !is_immutable_annotation(annotation, checker.semantic())
{ {

View File

@ -6,7 +6,10 @@ use rustpython_parser::ast::{self, Constant, Expr, Operator};
use ruff_python_ast::call_path::{from_qualified_name, from_unqualified_name, CallPath}; use ruff_python_ast::call_path::{from_qualified_name, from_unqualified_name, CallPath};
use ruff_python_ast::helpers::is_const_false; use ruff_python_ast::helpers::is_const_false;
use ruff_python_stdlib::typing::{ use ruff_python_stdlib::typing::{
IMMUTABLE_GENERIC_TYPES, IMMUTABLE_TYPES, PEP_585_GENERICS, PEP_593_SUBSCRIPTS, SUBSCRIPTS, as_pep_585_generic, has_pep_585_generic, is_immutable_generic_type,
is_immutable_non_generic_type, is_immutable_return_type, is_mutable_return_type,
is_pep_593_generic_member, is_pep_593_generic_type, is_standard_library_generic,
is_standard_library_generic_member,
}; };
use crate::model::SemanticModel; use crate::model::SemanticModel;
@ -34,12 +37,8 @@ pub fn match_annotated_subscript<'a>(
typing_modules: impl Iterator<Item = &'a str>, typing_modules: impl Iterator<Item = &'a str>,
extend_generics: &[String], extend_generics: &[String],
) -> Option<SubscriptKind> { ) -> Option<SubscriptKind> {
if !matches!(expr, Expr::Name(_) | Expr::Attribute(_)) {
return None;
}
semantic.resolve_call_path(expr).and_then(|call_path| { semantic.resolve_call_path(expr).and_then(|call_path| {
if SUBSCRIPTS.contains(&call_path.as_slice()) if is_standard_library_generic(call_path.as_slice())
|| extend_generics || extend_generics
.iter() .iter()
.map(|target| from_qualified_name(target)) .map(|target| from_qualified_name(target))
@ -47,20 +46,19 @@ pub fn match_annotated_subscript<'a>(
{ {
return Some(SubscriptKind::AnnotatedSubscript); return Some(SubscriptKind::AnnotatedSubscript);
} }
if PEP_593_SUBSCRIPTS.contains(&call_path.as_slice()) {
if is_pep_593_generic_type(call_path.as_slice()) {
return Some(SubscriptKind::PEP593AnnotatedSubscript); return Some(SubscriptKind::PEP593AnnotatedSubscript);
} }
for module in typing_modules { for module in typing_modules {
let module_call_path: CallPath = from_unqualified_name(module); let module_call_path: CallPath = from_unqualified_name(module);
if call_path.starts_with(&module_call_path) { if call_path.starts_with(&module_call_path) {
for subscript in SUBSCRIPTS.iter() { if let Some(member) = call_path.last() {
if call_path.last() == subscript.last() { if is_standard_library_generic_member(member) {
return Some(SubscriptKind::AnnotatedSubscript); return Some(SubscriptKind::AnnotatedSubscript);
} }
} if is_pep_593_generic_member(member) {
for subscript in PEP_593_SUBSCRIPTS.iter() {
if call_path.last() == subscript.last() {
return Some(SubscriptKind::PEP593AnnotatedSubscript); return Some(SubscriptKind::PEP593AnnotatedSubscript);
} }
} }
@ -92,38 +90,27 @@ impl std::fmt::Display for ModuleMember {
/// a variant exists. /// a variant exists.
pub fn to_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> Option<ModuleMember> { pub fn to_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> Option<ModuleMember> {
semantic.resolve_call_path(expr).and_then(|call_path| { semantic.resolve_call_path(expr).and_then(|call_path| {
let [module, name] = call_path.as_slice() else { let [module, member] = call_path.as_slice() else {
return None; return None;
}; };
PEP_585_GENERICS as_pep_585_generic(module, member).map(|(module, member)| {
.iter() if module.is_empty() {
.find_map(|((from_module, from_member), (to_module, to_member))| { ModuleMember::BuiltIn(member)
if module == from_module && name == from_member { } else {
if to_module.is_empty() { ModuleMember::Member(module, member)
Some(ModuleMember::BuiltIn(to_member)) }
} else { })
Some(ModuleMember::Member(to_module, to_member))
}
} else {
None
}
})
}) })
} }
/// Return whether a given expression uses a PEP 585 standard library generic. /// Return whether a given expression uses a PEP 585 standard library generic.
pub fn is_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> bool { pub fn is_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> bool {
if let Some(call_path) = semantic.resolve_call_path(expr) { semantic.resolve_call_path(expr).map_or(false, |call_path| {
let [module, name] = call_path.as_slice() else { let [module, name] = call_path.as_slice() else {
return false; return false;
}; };
for (_, (to_module, to_member)) in PEP_585_GENERICS { has_pep_585_generic(module, name)
if module == to_module && name == to_member { })
return true;
}
}
}
false
} }
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
@ -178,19 +165,14 @@ pub fn is_immutable_annotation(expr: &Expr, semantic: &SemanticModel) -> bool {
match expr { match expr {
Expr::Name(_) | Expr::Attribute(_) => { Expr::Name(_) | Expr::Attribute(_) => {
semantic.resolve_call_path(expr).map_or(false, |call_path| { semantic.resolve_call_path(expr).map_or(false, |call_path| {
IMMUTABLE_TYPES is_immutable_non_generic_type(call_path.as_slice())
.iter() || is_immutable_generic_type(call_path.as_slice())
.chain(IMMUTABLE_GENERIC_TYPES)
.any(|target| call_path.as_slice() == *target)
}) })
} }
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => semantic Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => semantic
.resolve_call_path(value) .resolve_call_path(value)
.map_or(false, |call_path| { .map_or(false, |call_path| {
if IMMUTABLE_GENERIC_TYPES if is_immutable_generic_type(call_path.as_slice()) {
.iter()
.any(|target| call_path.as_slice() == *target)
{
true true
} else if matches!(call_path.as_slice(), ["typing", "Union"]) { } else if matches!(call_path.as_slice(), ["typing", "Union"]) {
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() { if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
@ -226,43 +208,43 @@ pub fn is_immutable_annotation(expr: &Expr, semantic: &SemanticModel) -> bool {
} }
} }
const IMMUTABLE_FUNCS: &[&[&str]] = &[ /// Return `true` if `func` is a function that returns an immutable value.
&["", "bool"],
&["", "complex"],
&["", "float"],
&["", "frozenset"],
&["", "int"],
&["", "str"],
&["", "tuple"],
&["datetime", "date"],
&["datetime", "datetime"],
&["datetime", "timedelta"],
&["decimal", "Decimal"],
&["fractions", "Fraction"],
&["operator", "attrgetter"],
&["operator", "itemgetter"],
&["operator", "methodcaller"],
&["pathlib", "Path"],
&["types", "MappingProxyType"],
&["re", "compile"],
];
/// Return `true` if `func` is a function that returns an immutable object.
pub fn is_immutable_func( pub fn is_immutable_func(
func: &Expr, func: &Expr,
semantic: &SemanticModel, semantic: &SemanticModel,
extend_immutable_calls: &[CallPath], extend_immutable_calls: &[CallPath],
) -> bool { ) -> bool {
semantic.resolve_call_path(func).map_or(false, |call_path| { semantic.resolve_call_path(func).map_or(false, |call_path| {
IMMUTABLE_FUNCS is_immutable_return_type(call_path.as_slice())
.iter()
.any(|target| call_path.as_slice() == *target)
|| extend_immutable_calls || extend_immutable_calls
.iter() .iter()
.any(|target| call_path == *target) .any(|target| call_path == *target)
}) })
} }
/// Return `true` if `func` is a function that returns a mutable value.
pub fn is_mutable_func(func: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_call_path(func)
.as_ref()
.map(CallPath::as_slice)
.map_or(false, is_mutable_return_type)
}
/// Return `true` if `expr` is an expression that resolves to a mutable value.
pub fn is_mutable_expr(expr: &Expr, semantic: &SemanticModel) -> bool {
match expr {
Expr::List(_)
| Expr::Dict(_)
| Expr::Set(_)
| Expr::ListComp(_)
| Expr::DictComp(_)
| Expr::SetComp(_) => true,
Expr::Call(ast::ExprCall { func, .. }) => is_mutable_func(func, semantic),
_ => false,
}
}
/// Return `true` if [`Expr`] is a guard for a type-checking block. /// Return `true` if [`Expr`] is a guard for a type-checking block.
pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> bool { pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> bool {
let ast::StmtIf { test, .. } = stmt; let ast::StmtIf { test, .. } = stmt;

View File

@ -10,7 +10,7 @@ use smallvec::smallvec;
use ruff_python_ast::call_path::{collect_call_path, from_unqualified_name, CallPath}; use ruff_python_ast::call_path::{collect_call_path, from_unqualified_name, CallPath};
use ruff_python_ast::helpers::from_relative_import; use ruff_python_ast::helpers::from_relative_import;
use ruff_python_stdlib::path::is_python_stub_file; use ruff_python_stdlib::path::is_python_stub_file;
use ruff_python_stdlib::typing::TYPING_EXTENSIONS; use ruff_python_stdlib::typing::is_typing_extension;
use crate::binding::{ use crate::binding::{
Binding, BindingFlags, BindingId, BindingKind, Bindings, Exceptions, FromImportation, Binding, BindingFlags, BindingId, BindingKind, Bindings, Exceptions, FromImportation,
@ -175,7 +175,7 @@ impl<'a> SemanticModel<'a> {
return true; return true;
} }
if TYPING_EXTENSIONS.contains(target) { if is_typing_extension(target) {
if call_path.as_slice() == ["typing_extensions", target] { if call_path.as_slice() == ["typing_extensions", target] {
return true; return true;
} }

View File

@ -1,279 +1,414 @@
use once_cell::sync::Lazy; /// Returns `true` if a name is a member of Python's `typing_extensions` module.
use rustc_hash::{FxHashMap, FxHashSet}; ///
/// See: <https://pypi.org/project/typing-extensions/>
pub fn is_typing_extension(member: &str) -> bool {
matches!(
member,
"Annotated"
| "Any"
| "AsyncContextManager"
| "AsyncGenerator"
| "AsyncIterable"
| "AsyncIterator"
| "Awaitable"
| "ChainMap"
| "ClassVar"
| "Concatenate"
| "ContextManager"
| "Coroutine"
| "Counter"
| "DefaultDict"
| "Deque"
| "Final"
| "Literal"
| "LiteralString"
| "NamedTuple"
| "Never"
| "NewType"
| "NotRequired"
| "OrderedDict"
| "ParamSpec"
| "ParamSpecArgs"
| "ParamSpecKwargs"
| "Protocol"
| "Required"
| "Self"
| "TYPE_CHECKING"
| "Text"
| "Type"
| "TypeAlias"
| "TypeGuard"
| "TypeVar"
| "TypeVarTuple"
| "TypedDict"
| "Unpack"
| "assert_never"
| "assert_type"
| "clear_overloads"
| "final"
| "get_type_hints"
| "get_args"
| "get_origin"
| "get_overloads"
| "is_typeddict"
| "overload"
| "override"
| "reveal_type"
| "runtime_checkable"
)
}
// See: https://pypi.org/project/typing-extensions/ /// Returns `true` if a call path is a generic from the Python standard library (e.g. `list`, which
pub static TYPING_EXTENSIONS: Lazy<FxHashSet<&'static str>> = Lazy::new(|| { /// can be used as `list[int]`).
FxHashSet::from_iter([ ///
"Annotated", /// See: <https://docs.python.org/3/library/typing.html>
"Any", pub fn is_standard_library_generic(call_path: &[&str]) -> bool {
"AsyncContextManager", matches!(
"AsyncGenerator", call_path,
"AsyncIterable", ["", "dict" | "frozenset" | "list" | "set" | "tuple" | "type"]
"AsyncIterator", | [
"Awaitable", "collections" | "typing" | "typing_extensions",
"ChainMap", "ChainMap" | "Counter"
"ClassVar", ]
"Concatenate", | ["collections" | "typing", "OrderedDict"]
"ContextManager", | ["collections", "defaultdict" | "deque"]
"Coroutine", | [
"Counter", "collections",
"DefaultDict", "abc",
"Deque", "AsyncGenerator"
"Final", | "AsyncIterable"
"Literal", | "AsyncIterator"
"LiteralString", | "Awaitable"
"NamedTuple", | "ByteString"
"Never", | "Callable"
"NewType", | "Collection"
"NotRequired", | "Container"
"OrderedDict", | "Coroutine"
"ParamSpec", | "Generator"
"ParamSpecArgs", | "ItemsView"
"ParamSpecKwargs", | "Iterable"
"Protocol", | "Iterator"
"Required", | "KeysView"
"Self", | "Mapping"
"TYPE_CHECKING", | "MappingView"
"Text", | "MutableMapping"
"Type", | "MutableSequence"
"TypeAlias", | "MutableSet"
"TypeGuard", | "Reversible"
"TypeVar", | "Sequence"
"TypeVarTuple", | "Set"
"TypedDict", | "ValuesView"
"Unpack", ]
"assert_never", | [
"assert_type", "contextlib",
"clear_overloads", "AbstractAsyncContextManager" | "AbstractContextManager"
"final", ]
"get_type_hints", | ["re" | "typing", "Match" | "Pattern"]
"get_args", | [
"get_origin", "typing",
"get_overloads", "AbstractSet"
"is_typeddict", | "AsyncContextManager"
"overload", | "AsyncGenerator"
"override", | "AsyncIterator"
"reveal_type", | "Awaitable"
"runtime_checkable", | "BinaryIO"
]) | "ByteString"
}); | "Callable"
| "ClassVar"
| "Collection"
| "Concatenate"
| "Container"
| "ContextManager"
| "Coroutine"
| "DefaultDict"
| "Deque"
| "Dict"
| "Final"
| "FrozenSet"
| "Generator"
| "Generic"
| "IO"
| "ItemsView"
| "Iterable"
| "Iterator"
| "KeysView"
| "List"
| "Mapping"
| "MutableMapping"
| "MutableSequence"
| "MutableSet"
| "Optional"
| "Reversible"
| "Sequence"
| "Set"
| "TextIO"
| "Tuple"
| "Type"
| "TypeGuard"
| "Union"
| "Unpack"
| "ValuesView"
]
| ["typing", "io", "BinaryIO" | "IO" | "TextIO"]
| ["typing", "re", "Match" | "Pattern"]
| [
"typing_extensions",
"AsyncContextManager"
| "AsyncGenerator"
| "AsyncIterable"
| "AsyncIterator"
| "Awaitable"
| "ClassVar"
| "Concatenate"
| "ContextManager"
| "Coroutine"
| "DefaultDict"
| "Deque"
| "Type"
]
| [
"weakref",
"WeakKeyDictionary" | "WeakSet" | "WeakValueDictionary"
]
)
}
// See: https://docs.python.org/3/library/typing.html /// Returns `true` if a call path is a [PEP 593] generic (e.g. `Annotated`).
pub const SUBSCRIPTS: &[&[&str]] = &[ ///
// builtins /// See: <https://docs.python.org/3/library/typing.html>
&["", "dict"], ///
&["", "frozenset"], /// [PEP 593]: https://peps.python.org/pep-0593/
&["", "list"], pub fn is_pep_593_generic_type(call_path: &[&str]) -> bool {
&["", "set"], matches!(call_path, ["typing" | "typing_extensions", "Annotated"])
&["", "tuple"], }
&["", "type"],
// `collections`
&["collections", "ChainMap"],
&["collections", "Counter"],
&["collections", "OrderedDict"],
&["collections", "defaultdict"],
&["collections", "deque"],
// `collections.abc`
&["collections", "abc", "AsyncGenerator"],
&["collections", "abc", "AsyncIterable"],
&["collections", "abc", "AsyncIterator"],
&["collections", "abc", "Awaitable"],
&["collections", "abc", "ByteString"],
&["collections", "abc", "Callable"],
&["collections", "abc", "Collection"],
&["collections", "abc", "Container"],
&["collections", "abc", "Coroutine"],
&["collections", "abc", "Generator"],
&["collections", "abc", "ItemsView"],
&["collections", "abc", "Iterable"],
&["collections", "abc", "Iterator"],
&["collections", "abc", "KeysView"],
&["collections", "abc", "Mapping"],
&["collections", "abc", "MappingView"],
&["collections", "abc", "MutableMapping"],
&["collections", "abc", "MutableSequence"],
&["collections", "abc", "MutableSet"],
&["collections", "abc", "Reversible"],
&["collections", "abc", "Sequence"],
&["collections", "abc", "Set"],
&["collections", "abc", "ValuesView"],
// `contextlib`
&["contextlib", "AbstractAsyncContextManager"],
&["contextlib", "AbstractContextManager"],
// `re`
&["re", "Match"],
&["re", "Pattern"],
// `typing`
&["typing", "AbstractSet"],
&["typing", "AsyncContextManager"],
&["typing", "AsyncGenerator"],
&["typing", "AsyncIterator"],
&["typing", "Awaitable"],
&["typing", "BinaryIO"],
&["typing", "ByteString"],
&["typing", "Callable"],
&["typing", "ChainMap"],
&["typing", "ClassVar"],
&["typing", "Collection"],
&["typing", "Concatenate"],
&["typing", "Container"],
&["typing", "ContextManager"],
&["typing", "Coroutine"],
&["typing", "Counter"],
&["typing", "DefaultDict"],
&["typing", "Deque"],
&["typing", "Dict"],
&["typing", "Final"],
&["typing", "FrozenSet"],
&["typing", "Generator"],
&["typing", "Generic"],
&["typing", "IO"],
&["typing", "ItemsView"],
&["typing", "Iterable"],
&["typing", "Iterator"],
&["typing", "KeysView"],
&["typing", "List"],
&["typing", "Mapping"],
&["typing", "Match"],
&["typing", "MutableMapping"],
&["typing", "MutableSequence"],
&["typing", "MutableSet"],
&["typing", "Optional"],
&["typing", "OrderedDict"],
&["typing", "Pattern"],
&["typing", "Reversible"],
&["typing", "Sequence"],
&["typing", "Set"],
&["typing", "TextIO"],
&["typing", "Tuple"],
&["typing", "Type"],
&["typing", "TypeGuard"],
&["typing", "Union"],
&["typing", "Unpack"],
&["typing", "ValuesView"],
// `typing.io`
&["typing", "io", "BinaryIO"],
&["typing", "io", "IO"],
&["typing", "io", "TextIO"],
// `typing.re`
&["typing", "re", "Match"],
&["typing", "re", "Pattern"],
// `typing_extensions`
&["typing_extensions", "AsyncContextManager"],
&["typing_extensions", "AsyncGenerator"],
&["typing_extensions", "AsyncIterable"],
&["typing_extensions", "AsyncIterator"],
&["typing_extensions", "Awaitable"],
&["typing_extensions", "ChainMap"],
&["typing_extensions", "ClassVar"],
&["typing_extensions", "Concatenate"],
&["typing_extensions", "ContextManager"],
&["typing_extensions", "Coroutine"],
&["typing_extensions", "Counter"],
&["typing_extensions", "DefaultDict"],
&["typing_extensions", "Deque"],
&["typing_extensions", "Type"],
// `weakref`
&["weakref", "WeakKeyDictionary"],
&["weakref", "WeakSet"],
&["weakref", "WeakValueDictionary"],
];
// See: https://docs.python.org/3/library/typing.html /// Returns `true` if a name matches that of a generic from the Python standard library (e.g.
pub const PEP_593_SUBSCRIPTS: &[&[&str]] = &[ /// `list` or `Set`).
// `typing` ///
&["typing", "Annotated"], /// See: <https://docs.python.org/3/library/typing.html>
// `typing_extensions` pub fn is_standard_library_generic_member(member: &str) -> bool {
&["typing_extensions", "Annotated"], // Constructed by taking every pattern from `is_standard_library_generic`, removing all but
]; // the last element in each pattern, and de-duplicating the values.
matches!(
member,
"dict"
| "AbstractAsyncContextManager"
| "AbstractContextManager"
| "AbstractSet"
| "AsyncContextManager"
| "AsyncGenerator"
| "AsyncIterable"
| "AsyncIterator"
| "Awaitable"
| "BinaryIO"
| "ByteString"
| "Callable"
| "ChainMap"
| "ClassVar"
| "Collection"
| "Concatenate"
| "Container"
| "ContextManager"
| "Coroutine"
| "Counter"
| "DefaultDict"
| "Deque"
| "Dict"
| "Final"
| "FrozenSet"
| "Generator"
| "Generic"
| "IO"
| "ItemsView"
| "Iterable"
| "Iterator"
| "KeysView"
| "List"
| "Mapping"
| "MappingView"
| "Match"
| "MutableMapping"
| "MutableSequence"
| "MutableSet"
| "Optional"
| "OrderedDict"
| "Pattern"
| "Reversible"
| "Sequence"
| "Set"
| "TextIO"
| "Tuple"
| "Type"
| "TypeGuard"
| "Union"
| "Unpack"
| "ValuesView"
| "WeakKeyDictionary"
| "WeakSet"
| "WeakValueDictionary"
| "defaultdict"
| "deque"
| "frozenset"
| "list"
| "set"
| "tuple"
| "type"
)
}
/// Returns `true` if a name matches that of a generic from [PEP 593] (e.g. `Annotated`).
///
/// See: <https://docs.python.org/3/library/typing.html>
///
/// [PEP 593]: https://peps.python.org/pep-0593/
pub fn is_pep_593_generic_member(member: &str) -> bool {
// Constructed by taking every pattern from `is_pep_593_generic`, removing all but
// the last element in each pattern, and de-duplicating the values.
matches!(member, "Annotated")
}
/// Returns `true` if a call path represents that of an immutable, non-generic type from the Python
/// standard library (e.g. `int` or `str`).
pub fn is_immutable_non_generic_type(call_path: &[&str]) -> bool {
matches!(
call_path,
["collections", "abc", "Sized"]
| ["typing", "LiteralString" | "Sized"]
| [
"",
"bool"
| "bytes"
| "complex"
| "float"
| "frozenset"
| "int"
| "object"
| "range"
| "str"
]
)
}
/// Returns `true` if a call path represents that of an immutable, generic type from the Python
/// standard library (e.g. `tuple`).
pub fn is_immutable_generic_type(call_path: &[&str]) -> bool {
matches!(
call_path,
["", "tuple"]
| [
"collections",
"abc",
"ByteString"
| "Collection"
| "Container"
| "Iterable"
| "Mapping"
| "Reversible"
| "Sequence"
| "Set"
]
| [
"typing",
"AbstractSet"
| "ByteString"
| "Callable"
| "Collection"
| "Container"
| "FrozenSet"
| "Iterable"
| "Literal"
| "Mapping"
| "Never"
| "NoReturn"
| "Reversible"
| "Sequence"
| "Tuple"
]
)
}
/// Returns `true` if a call path represents a function from the Python standard library that
/// returns a mutable value (e.g., `dict`).
pub fn is_mutable_return_type(call_path: &[&str]) -> bool {
matches!(
call_path,
["", "dict" | "list" | "set"]
| [
"collections",
"Counter" | "OrderedDict" | "defaultdict" | "deque"
]
)
}
/// Returns `true` if a call path represents a function from the Python standard library that
/// returns a immutable value (e.g., `bool`).
pub fn is_immutable_return_type(call_path: &[&str]) -> bool {
matches!(
call_path,
["datetime", "date" | "datetime" | "timedelta"]
| ["decimal", "Decimal"]
| ["fractions", "Fraction"]
| ["operator", "attrgetter" | "itemgetter" | "methodcaller"]
| ["pathlib", "Path"]
| ["types", "MappingProxyType"]
| ["re", "compile"]
| [
"",
"bool" | "complex" | "float" | "frozenset" | "int" | "str" | "tuple"
]
)
}
type ModuleMember = (&'static str, &'static str); type ModuleMember = (&'static str, &'static str);
type SymbolReplacement = (ModuleMember, ModuleMember); /// Given a typing member, returns the module and member name for a generic from the Python standard
/// library (e.g., `list` for `typing.List`), if such a generic was introduced by [PEP 585].
///
/// [PEP 585]: https://peps.python.org/pep-0585/
pub fn as_pep_585_generic(module: &str, member: &str) -> Option<ModuleMember> {
match (module, member) {
("typing", "Dict") => Some(("", "dict")),
("typing", "FrozenSet") => Some(("", "frozenset")),
("typing", "List") => Some(("", "list")),
("typing", "Set") => Some(("", "set")),
("typing", "Tuple") => Some(("", "tuple")),
("typing", "Type") => Some(("", "type")),
("typing_extensions", "Type") => Some(("", "type")),
("typing", "Deque") => Some(("collections", "deque")),
("typing_extensions", "Deque") => Some(("collections", "deque")),
("typing", "DefaultDict") => Some(("collections", "defaultdict")),
("typing_extensions", "DefaultDict") => Some(("collections", "defaultdict")),
_ => None,
}
}
// See: https://peps.python.org/pep-0585/ /// Given a typing member, returns `true` if a generic equivalent exists in the Python standard
pub const PEP_585_GENERICS: &[SymbolReplacement] = &[ /// library (e.g., `list` for `typing.List`), as introduced by [PEP 585].
(("typing", "Dict"), ("", "dict")), ///
(("typing", "FrozenSet"), ("", "frozenset")), /// [PEP 585]: https://peps.python.org/pep-0585/
(("typing", "List"), ("", "list")), pub fn has_pep_585_generic(module: &str, member: &str) -> bool {
(("typing", "Set"), ("", "set")), // Constructed by taking every pattern from `as_pep_585_generic`, removing all but
(("typing", "Tuple"), ("", "tuple")), // the last element in each pattern, and de-duplicating the values.
(("typing", "Type"), ("", "type")), matches!(
(("typing_extensions", "Type"), ("", "type")), (module, member),
(("typing", "Deque"), ("collections", "deque")), ("", "dict" | "frozenset" | "list" | "set" | "tuple" | "type")
(("typing_extensions", "Deque"), ("collections", "deque")), | ("collections", "deque" | "defaultdict")
(("typing", "DefaultDict"), ("collections", "defaultdict")), )
( }
("typing_extensions", "DefaultDict"),
("collections", "defaultdict"),
),
];
// See: https://github.com/JelleZijlstra/autotyping/blob/0adba5ba0eee33c1de4ad9d0c79acfd737321dd9/autotyping/autotyping.py#L69-L91 /// Returns the expected return type for a magic method.
pub static SIMPLE_MAGIC_RETURN_TYPES: Lazy<FxHashMap<&'static str, &'static str>> = ///
Lazy::new(|| { /// See: <https://github.com/JelleZijlstra/autotyping/blob/0adba5ba0eee33c1de4ad9d0c79acfd737321dd9/autotyping/autotyping.py#L69-L91>
FxHashMap::from_iter([ pub fn simple_magic_return_type(method: &str) -> Option<&'static str> {
("__str__", "str"), match method {
("__repr__", "str"), "__str__" | "__repr__" | "__format__" => Some("str"),
("__len__", "int"), "__bytes__" => Some("bytes"),
("__length_hint__", "int"), "__len__" | "__length_hint__" | "__int__" | "__index__" => Some("int"),
("__init__", "None"), "__float__" => Some("float"),
("__del__", "None"), "__complex__" => Some("complex"),
("__bool__", "bool"), "__bool__" | "__contains__" | "__instancecheck__" | "__subclasscheck__" => Some("bool"),
("__bytes__", "bytes"), "__init__" | "__del__" | "__setattr__" | "__delattr__" | "__setitem__" | "__delitem__"
("__format__", "str"), | "__set__" => Some("None"),
("__contains__", "bool"), _ => None,
("__complex__", "complex"), }
("__int__", "int"), }
("__float__", "float"),
("__index__", "int"),
("__setattr__", "None"),
("__delattr__", "None"),
("__setitem__", "None"),
("__delitem__", "None"),
("__set__", "None"),
("__instancecheck__", "bool"),
("__subclasscheck__", "bool"),
])
});
pub const IMMUTABLE_TYPES: &[&[&str]] = &[
&["", "bool"],
&["", "bytes"],
&["", "complex"],
&["", "float"],
&["", "frozenset"],
&["", "int"],
&["", "object"],
&["", "range"],
&["", "str"],
&["collections", "abc", "Sized"],
&["typing", "LiteralString"],
&["typing", "Sized"],
];
pub const IMMUTABLE_GENERIC_TYPES: &[&[&str]] = &[
&["", "tuple"],
&["collections", "abc", "ByteString"],
&["collections", "abc", "Collection"],
&["collections", "abc", "Container"],
&["collections", "abc", "Iterable"],
&["collections", "abc", "Mapping"],
&["collections", "abc", "Reversible"],
&["collections", "abc", "Sequence"],
&["collections", "abc", "Set"],
&["typing", "AbstractSet"],
&["typing", "ByteString"],
&["typing", "Callable"],
&["typing", "Collection"],
&["typing", "Container"],
&["typing", "FrozenSet"],
&["typing", "Iterable"],
&["typing", "Literal"],
&["typing", "Mapping"],
&["typing", "Never"],
&["typing", "NoReturn"],
&["typing", "Reversible"],
&["typing", "Sequence"],
&["typing", "Tuple"],
];