mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:47 +00:00
302 lines
9.9 KiB
Rust
302 lines
9.9 KiB
Rust
//! Analysis rules for the `typing` module.
|
|
|
|
use num_traits::identities::Zero;
|
|
use ruff_python_ast::{self as ast, Constant, Expr, Operator};
|
|
|
|
use ruff_python_ast::call_path::{from_qualified_name, from_unqualified_name, CallPath};
|
|
use ruff_python_ast::helpers::is_const_false;
|
|
use ruff_python_stdlib::typing::{
|
|
as_pep_585_generic, has_pep_585_generic, is_immutable_generic_type,
|
|
is_immutable_non_generic_type, is_immutable_return_type, is_literal_member,
|
|
is_mutable_return_type, is_pep_593_generic_member, is_pep_593_generic_type,
|
|
is_standard_library_generic, is_standard_library_generic_member, is_standard_library_literal,
|
|
};
|
|
|
|
use crate::model::SemanticModel;
|
|
|
|
#[derive(Copy, Clone)]
|
|
pub enum Callable {
|
|
Bool,
|
|
Cast,
|
|
NewType,
|
|
TypeVar,
|
|
NamedTuple,
|
|
TypedDict,
|
|
MypyExtension,
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
pub enum SubscriptKind {
|
|
/// A subscript of the form `typing.Literal["foo", "bar"]`, i.e., a literal.
|
|
Literal,
|
|
/// A subscript of the form `typing.List[int]`, i.e., a generic.
|
|
Generic,
|
|
/// A subscript of the form `typing.Annotated[int, "foo"]`, i.e., a PEP 593 annotation.
|
|
PEP593Annotation,
|
|
}
|
|
|
|
pub fn match_annotated_subscript<'a>(
|
|
expr: &Expr,
|
|
semantic: &SemanticModel,
|
|
typing_modules: impl Iterator<Item = &'a str>,
|
|
extend_generics: &[String],
|
|
) -> Option<SubscriptKind> {
|
|
semantic.resolve_call_path(expr).and_then(|call_path| {
|
|
if is_standard_library_literal(call_path.as_slice()) {
|
|
return Some(SubscriptKind::Literal);
|
|
}
|
|
|
|
if is_standard_library_generic(call_path.as_slice())
|
|
|| extend_generics
|
|
.iter()
|
|
.map(|target| from_qualified_name(target))
|
|
.any(|target| call_path == target)
|
|
{
|
|
return Some(SubscriptKind::Generic);
|
|
}
|
|
|
|
if is_pep_593_generic_type(call_path.as_slice()) {
|
|
return Some(SubscriptKind::PEP593Annotation);
|
|
}
|
|
|
|
for module in typing_modules {
|
|
let module_call_path: CallPath = from_unqualified_name(module);
|
|
if call_path.starts_with(&module_call_path) {
|
|
if let Some(member) = call_path.last() {
|
|
if is_literal_member(member) {
|
|
return Some(SubscriptKind::Literal);
|
|
}
|
|
if is_standard_library_generic_member(member) {
|
|
return Some(SubscriptKind::Generic);
|
|
}
|
|
if is_pep_593_generic_member(member) {
|
|
return Some(SubscriptKind::PEP593Annotation);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
})
|
|
}
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
pub enum ModuleMember {
|
|
/// A builtin symbol, like `"list"`.
|
|
BuiltIn(&'static str),
|
|
/// A module member, like `("collections", "deque")`.
|
|
Member(&'static str, &'static str),
|
|
}
|
|
|
|
impl std::fmt::Display for ModuleMember {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
match self {
|
|
ModuleMember::BuiltIn(name) => std::write!(f, "{name}"),
|
|
ModuleMember::Member(module, member) => std::write!(f, "{module}.{member}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the PEP 585 standard library generic variant for a `typing` module reference, if such
|
|
/// a variant exists.
|
|
pub fn to_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> Option<ModuleMember> {
|
|
semantic.resolve_call_path(expr).and_then(|call_path| {
|
|
let [module, member] = call_path.as_slice() else {
|
|
return None;
|
|
};
|
|
as_pep_585_generic(module, member).map(|(module, member)| {
|
|
if module.is_empty() {
|
|
ModuleMember::BuiltIn(member)
|
|
} else {
|
|
ModuleMember::Member(module, member)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
/// Return whether a given expression uses a PEP 585 standard library generic.
|
|
pub fn is_pep585_generic(expr: &Expr, semantic: &SemanticModel) -> bool {
|
|
semantic.resolve_call_path(expr).is_some_and(|call_path| {
|
|
let [module, name] = call_path.as_slice() else {
|
|
return false;
|
|
};
|
|
has_pep_585_generic(module, name)
|
|
})
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub enum Pep604Operator {
|
|
/// The union operator, e.g., `Union[str, int]`, expressible as `str | int` after PEP 604.
|
|
Union,
|
|
/// The union operator, e.g., `Optional[str]`, expressible as `str | None` after PEP 604.
|
|
Optional,
|
|
}
|
|
|
|
/// Return the PEP 604 operator variant to which the given subscript [`Expr`] corresponds, if any.
|
|
pub fn to_pep604_operator(
|
|
value: &Expr,
|
|
slice: &Expr,
|
|
semantic: &SemanticModel,
|
|
) -> Option<Pep604Operator> {
|
|
/// Returns `true` if any argument in the slice is a quoted annotation).
|
|
fn quoted_annotation(slice: &Expr) -> bool {
|
|
match slice {
|
|
Expr::Constant(ast::ExprConstant {
|
|
value: Constant::Str(_),
|
|
..
|
|
}) => true,
|
|
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().any(quoted_annotation),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
// If the slice is a forward reference (e.g., `Optional["Foo"]`), it can only be rewritten
|
|
// if we're in a typing-only context.
|
|
//
|
|
// This, for example, is invalid, as Python will evaluate `"Foo" | None` at runtime in order to
|
|
// populate the function's `__annotations__`:
|
|
// ```python
|
|
// def f(x: "Foo" | None): ...
|
|
// ```
|
|
//
|
|
// This, however, is valid:
|
|
// ```python
|
|
// def f():
|
|
// x: "Foo" | None
|
|
// ```
|
|
if quoted_annotation(slice) {
|
|
if semantic.execution_context().is_runtime() {
|
|
return None;
|
|
}
|
|
}
|
|
|
|
semantic
|
|
.resolve_call_path(value)
|
|
.as_ref()
|
|
.and_then(|call_path| {
|
|
if semantic.match_typing_call_path(call_path, "Optional") {
|
|
Some(Pep604Operator::Optional)
|
|
} else if semantic.match_typing_call_path(call_path, "Union") {
|
|
Some(Pep604Operator::Union)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Return `true` if `Expr` represents a reference to a type annotation that resolves to an
|
|
/// immutable type.
|
|
pub fn is_immutable_annotation(expr: &Expr, semantic: &SemanticModel) -> bool {
|
|
match expr {
|
|
Expr::Name(_) | Expr::Attribute(_) => {
|
|
semantic.resolve_call_path(expr).is_some_and(|call_path| {
|
|
is_immutable_non_generic_type(call_path.as_slice())
|
|
|| is_immutable_generic_type(call_path.as_slice())
|
|
})
|
|
}
|
|
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
|
semantic.resolve_call_path(value).is_some_and(|call_path| {
|
|
if is_immutable_generic_type(call_path.as_slice()) {
|
|
true
|
|
} else if matches!(call_path.as_slice(), ["typing", "Union"]) {
|
|
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
|
|
elts.iter()
|
|
.all(|elt| is_immutable_annotation(elt, semantic))
|
|
} else {
|
|
false
|
|
}
|
|
} else if matches!(call_path.as_slice(), ["typing", "Optional"]) {
|
|
is_immutable_annotation(slice, semantic)
|
|
} else if is_pep_593_generic_type(call_path.as_slice()) {
|
|
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
|
|
elts.first()
|
|
.is_some_and(|elt| is_immutable_annotation(elt, semantic))
|
|
} else {
|
|
false
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
})
|
|
}
|
|
Expr::BinOp(ast::ExprBinOp {
|
|
left,
|
|
op: Operator::BitOr,
|
|
right,
|
|
range: _,
|
|
}) => is_immutable_annotation(left, semantic) && is_immutable_annotation(right, semantic),
|
|
Expr::Constant(ast::ExprConstant {
|
|
value: Constant::None,
|
|
..
|
|
}) => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Return `true` if `func` is a function that returns an immutable value.
|
|
pub fn is_immutable_func(
|
|
func: &Expr,
|
|
semantic: &SemanticModel,
|
|
extend_immutable_calls: &[CallPath],
|
|
) -> bool {
|
|
semantic.resolve_call_path(func).is_some_and(|call_path| {
|
|
is_immutable_return_type(call_path.as_slice())
|
|
|| extend_immutable_calls
|
|
.iter()
|
|
.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)
|
|
.is_some_and(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.
|
|
pub fn is_type_checking_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> bool {
|
|
let ast::StmtIf { test, .. } = stmt;
|
|
|
|
// Ex) `if False:`
|
|
if is_const_false(test) {
|
|
return true;
|
|
}
|
|
|
|
// Ex) `if 0:`
|
|
if let Expr::Constant(ast::ExprConstant {
|
|
value: Constant::Int(value),
|
|
..
|
|
}) = test.as_ref()
|
|
{
|
|
if value.is_zero() {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Ex) `if typing.TYPE_CHECKING:`
|
|
if semantic
|
|
.resolve_call_path(test)
|
|
.is_some_and(|call_path| matches!(call_path.as_slice(), ["typing", "TYPE_CHECKING"]))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|