mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:45:24 +00:00

## Summary Includes minor changes to the semantic type inference to help detect the return type of function call. Fixes #17691 ## Test Plan Snapshot tests
1257 lines
43 KiB
Rust
1257 lines
43 KiB
Rust
//! Analysis rules for the `typing` module.
|
|
|
|
use ruff_python_ast::helpers::{any_over_expr, map_subscript};
|
|
use ruff_python_ast::identifier::Identifier;
|
|
use ruff_python_ast::name::QualifiedName;
|
|
use ruff_python_ast::{
|
|
self as ast, Expr, ExprCall, ExprName, Operator, ParameterWithDefault, Parameters, Stmt,
|
|
StmtAssign,
|
|
};
|
|
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,
|
|
is_typed_dict, is_typed_dict_member,
|
|
};
|
|
use ruff_text_size::Ranged;
|
|
use smallvec::{smallvec, SmallVec};
|
|
|
|
use crate::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
|
|
use crate::model::SemanticModel;
|
|
use crate::{Binding, BindingKind, Modules};
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub enum Callable {
|
|
Bool,
|
|
Cast,
|
|
NewType,
|
|
TypeVar,
|
|
NamedTuple,
|
|
TypedDict,
|
|
MypyExtension,
|
|
TypeAliasType,
|
|
}
|
|
|
|
#[derive(Debug, 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,
|
|
/// A subscript of the form `typing.TypedDict[{"key": Type}]`, i.e., a [PEP 764] annotation.
|
|
///
|
|
/// [PEP 764]: https://github.com/python/peps/pull/4082
|
|
TypedDict,
|
|
}
|
|
|
|
pub fn is_known_to_be_of_type_dict(semantic: &SemanticModel, expr: &ExprName) -> bool {
|
|
let Some(binding) = semantic.only_binding(expr).map(|id| semantic.binding(id)) else {
|
|
return false;
|
|
};
|
|
|
|
is_dict(binding, semantic)
|
|
}
|
|
|
|
pub fn match_annotated_subscript<'a>(
|
|
expr: &Expr,
|
|
semantic: &SemanticModel,
|
|
typing_modules: impl Iterator<Item = &'a str>,
|
|
extend_generics: &[String],
|
|
) -> Option<SubscriptKind> {
|
|
semantic
|
|
.resolve_qualified_name(expr)
|
|
.and_then(|qualified_name| {
|
|
if is_standard_library_literal(qualified_name.segments()) {
|
|
return Some(SubscriptKind::Literal);
|
|
}
|
|
|
|
if is_standard_library_generic(qualified_name.segments())
|
|
|| extend_generics
|
|
.iter()
|
|
.map(|target| QualifiedName::from_dotted_name(target))
|
|
.any(|target| qualified_name == target)
|
|
{
|
|
return Some(SubscriptKind::Generic);
|
|
}
|
|
|
|
if is_pep_593_generic_type(qualified_name.segments()) {
|
|
return Some(SubscriptKind::PEP593Annotation);
|
|
}
|
|
|
|
if is_typed_dict(qualified_name.segments()) {
|
|
return Some(SubscriptKind::TypedDict);
|
|
}
|
|
|
|
for module in typing_modules {
|
|
let module_qualified_name = QualifiedName::user_defined(module);
|
|
if qualified_name.starts_with(&module_qualified_name) {
|
|
if let Some(member) = qualified_name.segments().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);
|
|
}
|
|
if is_typed_dict_member(member) {
|
|
return Some(SubscriptKind::TypedDict);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
.seen_module(Modules::TYPING | Modules::TYPING_EXTENSIONS)
|
|
.then(|| semantic.resolve_qualified_name(expr))
|
|
.flatten()
|
|
.and_then(|qualified_name| {
|
|
let [module, member] = qualified_name.segments() 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_qualified_name(expr)
|
|
.is_some_and(|qualified_name| {
|
|
let [module, name] = qualified_name.segments() 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::StringLiteral(_) => true,
|
|
Expr::Tuple(tuple) => tuple.iter().any(quoted_annotation),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if any argument in the slice is a starred expression.
|
|
fn starred_annotation(slice: &Expr) -> bool {
|
|
match slice {
|
|
Expr::Starred(_) => true,
|
|
Expr::Tuple(tuple) => tuple.iter().any(starred_annotation),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
// If the typing modules were never imported, we'll never match below.
|
|
if !semantic.seen_typing() {
|
|
return None;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// If any of the elements are starred expressions, we can't rewrite the subscript:
|
|
// ```python
|
|
// def f(x: Union[*int, str]): ...
|
|
// ```
|
|
if starred_annotation(slice) {
|
|
return None;
|
|
}
|
|
|
|
semantic
|
|
.resolve_qualified_name(value)
|
|
.as_ref()
|
|
.and_then(|qualified_name| {
|
|
if semantic.match_typing_qualified_name(qualified_name, "Optional") {
|
|
Some(Pep604Operator::Optional)
|
|
} else if semantic.match_typing_qualified_name(qualified_name, "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,
|
|
extend_immutable_calls: &[QualifiedName],
|
|
) -> bool {
|
|
match expr {
|
|
Expr::Name(_) | Expr::Attribute(_) => {
|
|
semantic
|
|
.resolve_qualified_name(expr)
|
|
.is_some_and(|qualified_name| {
|
|
is_immutable_non_generic_type(qualified_name.segments())
|
|
|| is_immutable_generic_type(qualified_name.segments())
|
|
|| extend_immutable_calls
|
|
.iter()
|
|
.any(|target| qualified_name == *target)
|
|
})
|
|
}
|
|
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => semantic
|
|
.resolve_qualified_name(value)
|
|
.is_some_and(|qualified_name| {
|
|
if is_immutable_generic_type(qualified_name.segments()) {
|
|
true
|
|
} else if matches!(qualified_name.segments(), ["typing", "Union"]) {
|
|
if let Expr::Tuple(tuple) = &**slice {
|
|
tuple.iter().all(|element| {
|
|
is_immutable_annotation(element, semantic, extend_immutable_calls)
|
|
})
|
|
} else {
|
|
false
|
|
}
|
|
} else if matches!(qualified_name.segments(), ["typing", "Optional"]) {
|
|
is_immutable_annotation(slice, semantic, extend_immutable_calls)
|
|
} else if is_pep_593_generic_type(qualified_name.segments()) {
|
|
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
|
|
elts.first().is_some_and(|elt| {
|
|
is_immutable_annotation(elt, semantic, extend_immutable_calls)
|
|
})
|
|
} else {
|
|
false
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
}),
|
|
Expr::BinOp(ast::ExprBinOp {
|
|
left,
|
|
op: Operator::BitOr,
|
|
right,
|
|
range: _,
|
|
}) => {
|
|
is_immutable_annotation(left, semantic, extend_immutable_calls)
|
|
&& is_immutable_annotation(right, semantic, extend_immutable_calls)
|
|
}
|
|
Expr::NoneLiteral(_) => 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: &[QualifiedName],
|
|
) -> bool {
|
|
semantic
|
|
.resolve_qualified_name(map_subscript(func))
|
|
.is_some_and(|qualified_name| {
|
|
is_immutable_return_type(qualified_name.segments())
|
|
|| extend_immutable_calls
|
|
.iter()
|
|
.any(|target| qualified_name == *target)
|
|
})
|
|
}
|
|
|
|
/// Return `true` if `name` is bound to the `typing.NewType` call where the original type is
|
|
/// immutable.
|
|
///
|
|
/// For example:
|
|
/// ```python
|
|
/// from typing import NewType
|
|
///
|
|
/// UserId = NewType("UserId", int)
|
|
/// ```
|
|
///
|
|
/// Here, `name` would be `UserId`.
|
|
pub fn is_immutable_newtype_call(
|
|
name: &ast::ExprName,
|
|
semantic: &SemanticModel,
|
|
extend_immutable_calls: &[QualifiedName],
|
|
) -> bool {
|
|
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
|
|
return false;
|
|
};
|
|
|
|
if !binding.kind.is_assignment() {
|
|
return false;
|
|
}
|
|
|
|
let Some(Stmt::Assign(StmtAssign { value, .. })) = binding.statement(semantic) else {
|
|
return false;
|
|
};
|
|
|
|
let Expr::Call(ExprCall {
|
|
func, arguments, ..
|
|
}) = value.as_ref()
|
|
else {
|
|
return false;
|
|
};
|
|
|
|
if !semantic.match_typing_expr(func, "NewType") {
|
|
return false;
|
|
}
|
|
|
|
if arguments.len() != 2 {
|
|
return false;
|
|
}
|
|
|
|
let Some(original_type) = arguments.find_argument_value("tp", 1) else {
|
|
return false;
|
|
};
|
|
|
|
is_immutable_annotation(original_type, semantic, extend_immutable_calls)
|
|
}
|
|
|
|
/// Return `true` if `func` is a function that returns a mutable value.
|
|
pub fn is_mutable_func(func: &Expr, semantic: &SemanticModel) -> bool {
|
|
semantic
|
|
.resolve_qualified_name(func)
|
|
.as_ref()
|
|
.map(QualifiedName::segments)
|
|
.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(map_subscript(func), semantic),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Return `true` if [`ast::StmtIf`] 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;
|
|
|
|
match test.as_ref() {
|
|
// As long as the symbol's name is "TYPE_CHECKING" we will treat it like `typing.TYPE_CHECKING`
|
|
// for this specific check even if it's defined somewhere else, like the current module.
|
|
// Ex) `if TYPE_CHECKING:`
|
|
Expr::Name(ast::ExprName { id, .. }) => {
|
|
id == "TYPE_CHECKING"
|
|
// Ex) `if TC:` with `from typing import TYPE_CHECKING as TC`
|
|
|| semantic.match_typing_expr(test, "TYPE_CHECKING")
|
|
}
|
|
// Ex) `if typing.TYPE_CHECKING:`
|
|
Expr::Attribute(ast::ExprAttribute { attr, .. }) => attr == "TYPE_CHECKING",
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the [`ast::StmtIf`] is a version-checking block (e.g., `if sys.version_info >= ...:`).
|
|
pub fn is_sys_version_block(stmt: &ast::StmtIf, semantic: &SemanticModel) -> bool {
|
|
let ast::StmtIf { test, .. } = stmt;
|
|
|
|
any_over_expr(test, &|expr| {
|
|
semantic
|
|
.resolve_qualified_name(expr)
|
|
.is_some_and(|qualified_name| {
|
|
matches!(
|
|
qualified_name.segments(),
|
|
["sys", "version_info" | "platform"]
|
|
)
|
|
})
|
|
})
|
|
}
|
|
|
|
/// Traverse a "union" type annotation, applying `func` to each union member.
|
|
///
|
|
/// Supports traversal of `Union` and `|` union expressions.
|
|
///
|
|
/// The function is called with each expression in the union (excluding declarations of nested
|
|
/// unions) and the parent expression.
|
|
pub fn traverse_union<'a, F>(func: &mut F, semantic: &SemanticModel, expr: &'a Expr)
|
|
where
|
|
F: FnMut(&'a Expr, &'a Expr),
|
|
{
|
|
fn inner<'a, F>(
|
|
func: &mut F,
|
|
semantic: &SemanticModel,
|
|
expr: &'a Expr,
|
|
parent: Option<&'a Expr>,
|
|
) where
|
|
F: FnMut(&'a Expr, &'a Expr),
|
|
{
|
|
// Ex) `x | y`
|
|
if let Expr::BinOp(ast::ExprBinOp {
|
|
op: Operator::BitOr,
|
|
left,
|
|
right,
|
|
range: _,
|
|
}) = expr
|
|
{
|
|
// The union data structure usually looks like this:
|
|
// a | b | c -> (a | b) | c
|
|
//
|
|
// However, parenthesized expressions can coerce it into any structure:
|
|
// a | (b | c)
|
|
//
|
|
// So we have to traverse both branches in order (left, then right), to report members
|
|
// in the order they appear in the source code.
|
|
|
|
// Traverse the left then right arms
|
|
inner(func, semantic, left, Some(expr));
|
|
inner(func, semantic, right, Some(expr));
|
|
return;
|
|
}
|
|
|
|
// Ex) `Union[x, y]`
|
|
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
|
|
if semantic.match_typing_expr(value, "Union") {
|
|
if let Expr::Tuple(tuple) = &**slice {
|
|
// Traverse each element of the tuple within the union recursively to handle cases
|
|
// such as `Union[..., Union[...]]`
|
|
tuple
|
|
.iter()
|
|
.for_each(|elem| inner(func, semantic, elem, Some(expr)));
|
|
return;
|
|
}
|
|
|
|
// Ex) `Union[Union[a, b]]` and `Union[a | b | c]`
|
|
inner(func, semantic, slice, Some(expr));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Otherwise, call the function on expression, if it's not the top-level expression.
|
|
if let Some(parent) = parent {
|
|
func(expr, parent);
|
|
}
|
|
}
|
|
|
|
inner(func, semantic, expr, None);
|
|
}
|
|
|
|
/// Traverse a "literal" type annotation, applying `func` to each literal member.
|
|
///
|
|
/// The function is called with each expression in the literal (excluding declarations of nested
|
|
/// literals) and the parent expression.
|
|
pub fn traverse_literal<'a, F>(func: &mut F, semantic: &SemanticModel, expr: &'a Expr)
|
|
where
|
|
F: FnMut(&'a Expr, &'a Expr),
|
|
{
|
|
fn inner<'a, F>(
|
|
func: &mut F,
|
|
semantic: &SemanticModel,
|
|
expr: &'a Expr,
|
|
parent: Option<&'a Expr>,
|
|
) where
|
|
F: FnMut(&'a Expr, &'a Expr),
|
|
{
|
|
// Ex) `Literal[x, y]`
|
|
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
|
|
if semantic.match_typing_expr(value, "Literal") {
|
|
match &**slice {
|
|
Expr::Tuple(tuple) => {
|
|
// Traverse each element of the tuple within the literal recursively to handle cases
|
|
// such as `Literal[..., Literal[...]]`
|
|
for element in tuple {
|
|
inner(func, semantic, element, Some(expr));
|
|
}
|
|
}
|
|
other => {
|
|
// Ex) `Literal[Literal[...]]`
|
|
inner(func, semantic, other, Some(expr));
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Otherwise, call the function on expression, if it's not the top-level expression.
|
|
if let Some(parent) = parent {
|
|
func(expr, parent);
|
|
}
|
|
}
|
|
}
|
|
|
|
inner(func, semantic, expr, None);
|
|
}
|
|
|
|
/// Abstraction for a type checker, conservatively checks for the intended type(s).
|
|
pub trait TypeChecker {
|
|
/// Check annotation expression to match the intended type(s).
|
|
fn match_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool;
|
|
/// Check initializer expression to match the intended type(s).
|
|
fn match_initializer(initializer: &Expr, semantic: &SemanticModel) -> bool;
|
|
}
|
|
|
|
/// Check if the type checker accepts the given binding with the given name.
|
|
///
|
|
/// NOTE: this function doesn't perform more serious type inference, so it won't be able
|
|
/// to understand if the value gets initialized from a call to a function always returning
|
|
/// lists. This also implies no interfile analysis.
|
|
pub fn check_type<T: TypeChecker>(binding: &Binding, semantic: &SemanticModel) -> bool {
|
|
match binding.kind {
|
|
BindingKind::Assignment => match binding.statement(semantic) {
|
|
// Given:
|
|
//
|
|
// ```python
|
|
// x = init_expr
|
|
// ```
|
|
//
|
|
// The type checker might know how to infer the type based on `init_expr`.
|
|
Some(Stmt::Assign(ast::StmtAssign { targets, value, .. })) => targets
|
|
.iter()
|
|
.find_map(|target| match_value(binding, target, value))
|
|
.is_some_and(|value| T::match_initializer(value, semantic)),
|
|
|
|
// ```python
|
|
// x: annotation = some_expr
|
|
// ```
|
|
//
|
|
// In this situation, we check only the annotation.
|
|
Some(Stmt::AnnAssign(ast::StmtAnnAssign { annotation, .. })) => {
|
|
T::match_annotation(annotation, semantic)
|
|
}
|
|
|
|
_ => false,
|
|
},
|
|
|
|
BindingKind::NamedExprAssignment => {
|
|
// ```python
|
|
// if (x := some_expr) is not None:
|
|
// ...
|
|
// ```
|
|
binding.source.is_some_and(|source| {
|
|
semantic
|
|
.expressions(source)
|
|
.find_map(|expr| expr.as_named_expr())
|
|
.and_then(|ast::ExprNamed { target, value, .. }| {
|
|
match_value(binding, target, value)
|
|
})
|
|
.is_some_and(|value| T::match_initializer(value, semantic))
|
|
})
|
|
}
|
|
|
|
BindingKind::WithItemVar => match binding.statement(semantic) {
|
|
// ```python
|
|
// with open("file.txt") as x:
|
|
// ...
|
|
// ```
|
|
Some(Stmt::With(ast::StmtWith { items, .. })) => items
|
|
.iter()
|
|
.find_map(|item| {
|
|
let target = item.optional_vars.as_ref()?;
|
|
let value = &item.context_expr;
|
|
match_value(binding, target, value)
|
|
})
|
|
.is_some_and(|value| T::match_initializer(value, semantic)),
|
|
|
|
_ => false,
|
|
},
|
|
|
|
BindingKind::Argument => match binding.statement(semantic) {
|
|
// ```python
|
|
// def foo(x: annotation):
|
|
// ...
|
|
// ```
|
|
//
|
|
// We trust the annotation and see if the type checker matches the annotation.
|
|
Some(Stmt::FunctionDef(ast::StmtFunctionDef { parameters, .. })) => {
|
|
let Some(parameter) = find_parameter(parameters, binding) else {
|
|
return false;
|
|
};
|
|
let Some(annotation) = parameter.annotation() else {
|
|
return false;
|
|
};
|
|
T::match_annotation(annotation, semantic)
|
|
}
|
|
|
|
_ => false,
|
|
},
|
|
|
|
BindingKind::Annotation => match binding.statement(semantic) {
|
|
// ```python
|
|
// x: annotation
|
|
// ```
|
|
//
|
|
// It's a typed declaration, type annotation is the only source of information.
|
|
Some(Stmt::AnnAssign(ast::StmtAnnAssign { annotation, .. })) => {
|
|
T::match_annotation(annotation, semantic)
|
|
}
|
|
_ => false,
|
|
},
|
|
|
|
BindingKind::FunctionDefinition(_) => match binding.statement(semantic) {
|
|
// ```python
|
|
// def foo() -> int:
|
|
// ...
|
|
// ```
|
|
Some(Stmt::FunctionDef(ast::StmtFunctionDef { returns, .. })) => returns
|
|
.as_ref()
|
|
.is_some_and(|return_ann| T::match_annotation(return_ann, semantic)),
|
|
|
|
_ => false,
|
|
},
|
|
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Type checker for builtin types.
|
|
trait BuiltinTypeChecker {
|
|
/// Builtin type name.
|
|
const BUILTIN_TYPE_NAME: &'static str;
|
|
/// Type name as found in the `Typing` module.
|
|
const TYPING_NAME: Option<&'static str>;
|
|
/// [`PythonType`] associated with the intended type.
|
|
const EXPR_TYPE: PythonType;
|
|
|
|
/// Check annotation expression to match the intended type.
|
|
fn match_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
|
|
let value = map_subscript(annotation);
|
|
semantic.match_builtin_expr(value, Self::BUILTIN_TYPE_NAME)
|
|
|| Self::TYPING_NAME.is_some_and(|name| semantic.match_typing_expr(value, name))
|
|
}
|
|
|
|
/// Check initializer expression to match the intended type.
|
|
fn match_initializer(initializer: &Expr, semantic: &SemanticModel) -> bool {
|
|
Self::match_expr_type(initializer) || Self::match_builtin_constructor(initializer, semantic)
|
|
}
|
|
|
|
/// Check if the type can be inferred from the given expression.
|
|
fn match_expr_type(initializer: &Expr) -> bool {
|
|
let init_type: ResolvedPythonType = initializer.into();
|
|
match init_type {
|
|
ResolvedPythonType::Atom(atom) => atom == Self::EXPR_TYPE,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Check if the given expression corresponds to a constructor call of the builtin type.
|
|
fn match_builtin_constructor(initializer: &Expr, semantic: &SemanticModel) -> bool {
|
|
let Expr::Call(ast::ExprCall { func, .. }) = initializer else {
|
|
return false;
|
|
};
|
|
semantic.match_builtin_expr(func, Self::BUILTIN_TYPE_NAME)
|
|
}
|
|
}
|
|
|
|
impl<T: BuiltinTypeChecker> TypeChecker for T {
|
|
fn match_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
|
|
<Self as BuiltinTypeChecker>::match_annotation(annotation, semantic)
|
|
}
|
|
|
|
fn match_initializer(initializer: &Expr, semantic: &SemanticModel) -> bool {
|
|
<Self as BuiltinTypeChecker>::match_initializer(initializer, semantic)
|
|
}
|
|
}
|
|
|
|
struct ListChecker;
|
|
|
|
impl BuiltinTypeChecker for ListChecker {
|
|
const BUILTIN_TYPE_NAME: &'static str = "list";
|
|
const TYPING_NAME: Option<&'static str> = Some("List");
|
|
const EXPR_TYPE: PythonType = PythonType::List;
|
|
}
|
|
|
|
struct DictChecker;
|
|
|
|
impl BuiltinTypeChecker for DictChecker {
|
|
const BUILTIN_TYPE_NAME: &'static str = "dict";
|
|
const TYPING_NAME: Option<&'static str> = Some("Dict");
|
|
const EXPR_TYPE: PythonType = PythonType::Dict;
|
|
}
|
|
|
|
struct SetChecker;
|
|
|
|
impl BuiltinTypeChecker for SetChecker {
|
|
const BUILTIN_TYPE_NAME: &'static str = "set";
|
|
const TYPING_NAME: Option<&'static str> = Some("Set");
|
|
const EXPR_TYPE: PythonType = PythonType::Set;
|
|
}
|
|
|
|
struct StringChecker;
|
|
|
|
impl BuiltinTypeChecker for StringChecker {
|
|
const BUILTIN_TYPE_NAME: &'static str = "str";
|
|
const TYPING_NAME: Option<&'static str> = None;
|
|
const EXPR_TYPE: PythonType = PythonType::String;
|
|
}
|
|
|
|
struct BytesChecker;
|
|
|
|
impl BuiltinTypeChecker for BytesChecker {
|
|
const BUILTIN_TYPE_NAME: &'static str = "bytes";
|
|
const TYPING_NAME: Option<&'static str> = None;
|
|
const EXPR_TYPE: PythonType = PythonType::Bytes;
|
|
}
|
|
|
|
struct TupleChecker;
|
|
|
|
impl BuiltinTypeChecker for TupleChecker {
|
|
const BUILTIN_TYPE_NAME: &'static str = "tuple";
|
|
const TYPING_NAME: Option<&'static str> = Some("Tuple");
|
|
const EXPR_TYPE: PythonType = PythonType::Tuple;
|
|
}
|
|
|
|
struct IntChecker;
|
|
|
|
impl BuiltinTypeChecker for IntChecker {
|
|
const BUILTIN_TYPE_NAME: &'static str = "int";
|
|
const TYPING_NAME: Option<&'static str> = None;
|
|
const EXPR_TYPE: PythonType = PythonType::Number(NumberLike::Integer);
|
|
}
|
|
|
|
struct FloatChecker;
|
|
|
|
impl BuiltinTypeChecker for FloatChecker {
|
|
const BUILTIN_TYPE_NAME: &'static str = "float";
|
|
const TYPING_NAME: Option<&'static str> = None;
|
|
const EXPR_TYPE: PythonType = PythonType::Number(NumberLike::Float);
|
|
}
|
|
|
|
pub struct IoBaseChecker;
|
|
|
|
impl TypeChecker for IoBaseChecker {
|
|
fn match_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
|
|
semantic
|
|
.resolve_qualified_name(annotation)
|
|
.is_some_and(|qualified_name| {
|
|
if semantic.match_typing_qualified_name(&qualified_name, "IO") {
|
|
return true;
|
|
}
|
|
if semantic.match_typing_qualified_name(&qualified_name, "BinaryIO") {
|
|
return true;
|
|
}
|
|
if semantic.match_typing_qualified_name(&qualified_name, "TextIO") {
|
|
return true;
|
|
}
|
|
matches!(
|
|
qualified_name.segments(),
|
|
[
|
|
"io",
|
|
"IOBase"
|
|
| "RawIOBase"
|
|
| "BufferedIOBase"
|
|
| "TextIOBase"
|
|
| "BytesIO"
|
|
| "StringIO"
|
|
| "BufferedReader"
|
|
| "BufferedWriter"
|
|
| "BufferedRandom"
|
|
| "BufferedRWPair"
|
|
| "TextIOWrapper"
|
|
] | ["os", "Path" | "PathLike"]
|
|
| [
|
|
"pathlib",
|
|
"Path" | "PurePath" | "PurePosixPath" | "PureWindowsPath"
|
|
]
|
|
)
|
|
})
|
|
}
|
|
|
|
fn match_initializer(initializer: &Expr, semantic: &SemanticModel) -> bool {
|
|
let Expr::Call(ast::ExprCall { func, .. }) = initializer else {
|
|
return false;
|
|
};
|
|
|
|
// Ex) `pathlib.Path("file.txt")`
|
|
if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() {
|
|
if attr.as_str() == "open" {
|
|
if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
|
|
return semantic
|
|
.resolve_qualified_name(func)
|
|
.is_some_and(|qualified_name| {
|
|
matches!(
|
|
qualified_name.segments(),
|
|
[
|
|
"pathlib",
|
|
"Path" | "PurePath" | "PurePosixPath" | "PureWindowsPath"
|
|
]
|
|
)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ex) `open("file.txt")`
|
|
semantic
|
|
.resolve_qualified_name(func)
|
|
.is_some_and(|qualified_name| {
|
|
matches!(
|
|
qualified_name.segments(),
|
|
["io", "open" | "open_code"] | ["os" | "" | "builtins", "open"]
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct PathlibPathChecker;
|
|
|
|
impl PathlibPathChecker {
|
|
fn is_pathlib_path_constructor(semantic: &SemanticModel, expr: &Expr) -> bool {
|
|
let Some(qualified_name) = semantic.resolve_qualified_name(expr) else {
|
|
return false;
|
|
};
|
|
|
|
matches!(
|
|
qualified_name.segments(),
|
|
[
|
|
"pathlib",
|
|
"Path"
|
|
| "PosixPath"
|
|
| "PurePath"
|
|
| "PurePosixPath"
|
|
| "PureWindowsPath"
|
|
| "WindowsPath"
|
|
]
|
|
)
|
|
}
|
|
}
|
|
|
|
impl TypeChecker for PathlibPathChecker {
|
|
fn match_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
|
|
Self::is_pathlib_path_constructor(semantic, annotation)
|
|
}
|
|
|
|
fn match_initializer(initializer: &Expr, semantic: &SemanticModel) -> bool {
|
|
let Expr::Call(ast::ExprCall { func, .. }) = initializer else {
|
|
return false;
|
|
};
|
|
|
|
Self::is_pathlib_path_constructor(semantic, func)
|
|
}
|
|
}
|
|
|
|
pub struct FastApiRouteChecker;
|
|
|
|
impl FastApiRouteChecker {
|
|
fn is_fastapi_route_constructor(semantic: &SemanticModel, expr: &Expr) -> bool {
|
|
let Some(qualified_name) = semantic.resolve_qualified_name(expr) else {
|
|
return false;
|
|
};
|
|
|
|
matches!(
|
|
qualified_name.segments(),
|
|
["fastapi", "FastAPI" | "APIRouter"]
|
|
)
|
|
}
|
|
}
|
|
|
|
impl TypeChecker for FastApiRouteChecker {
|
|
fn match_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
|
|
Self::is_fastapi_route_constructor(semantic, annotation)
|
|
}
|
|
|
|
fn match_initializer(initializer: &Expr, semantic: &SemanticModel) -> bool {
|
|
let Expr::Call(ast::ExprCall { func, .. }) = initializer else {
|
|
return false;
|
|
};
|
|
|
|
Self::is_fastapi_route_constructor(semantic, func)
|
|
}
|
|
}
|
|
|
|
pub struct TypeVarLikeChecker;
|
|
|
|
impl TypeVarLikeChecker {
|
|
/// Returns `true` if an [`Expr`] is a `TypeVar`, `TypeVarTuple`, or `ParamSpec` call.
|
|
///
|
|
/// See also [`ruff_linter::rules::flake8_pyi::rules::simple_defaults::is_type_var_like_call`].
|
|
fn is_type_var_like_call(expr: &Expr, semantic: &SemanticModel) -> bool {
|
|
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
|
|
return false;
|
|
};
|
|
let Some(qualified_name) = semantic.resolve_qualified_name(func) else {
|
|
return false;
|
|
};
|
|
|
|
matches!(
|
|
qualified_name.segments(),
|
|
[
|
|
"typing" | "typing_extensions",
|
|
"TypeVar" | "TypeVarTuple" | "ParamSpec"
|
|
]
|
|
)
|
|
}
|
|
}
|
|
|
|
impl TypeChecker for TypeVarLikeChecker {
|
|
fn match_annotation(_annotation: &Expr, _semantic: &SemanticModel) -> bool {
|
|
false
|
|
}
|
|
|
|
fn match_initializer(initializer: &Expr, semantic: &SemanticModel) -> bool {
|
|
Self::is_type_var_like_call(initializer, semantic)
|
|
}
|
|
}
|
|
|
|
/// Test whether the given binding can be considered a list.
|
|
///
|
|
/// For this, we check what value might be associated with it through it's initialization and
|
|
/// what annotation it has (we consider `list` and `typing.List`)
|
|
pub fn is_list(binding: &Binding, semantic: &SemanticModel) -> bool {
|
|
check_type::<ListChecker>(binding, semantic)
|
|
}
|
|
|
|
/// Test whether the given binding can be considered a dictionary.
|
|
///
|
|
/// For this, we check what value might be associated with it through it's initialization,
|
|
/// what annotation it has (we consider `dict` and `typing.Dict`), and if it is a variadic keyword
|
|
/// argument parameter.
|
|
pub fn is_dict(binding: &Binding, semantic: &SemanticModel) -> bool {
|
|
// ```python
|
|
// def foo(**kwargs):
|
|
// ...
|
|
// ```
|
|
if matches!(binding.kind, BindingKind::Argument) {
|
|
if let Some(Stmt::FunctionDef(ast::StmtFunctionDef { parameters, .. })) =
|
|
binding.statement(semantic)
|
|
{
|
|
if let Some(kwarg_parameter) = parameters.kwarg.as_deref() {
|
|
if kwarg_parameter.name.range() == binding.range() {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
check_type::<DictChecker>(binding, semantic)
|
|
}
|
|
|
|
/// Test whether the given binding can be considered an integer.
|
|
pub fn is_int(binding: &Binding, semantic: &SemanticModel) -> bool {
|
|
check_type::<IntChecker>(binding, semantic)
|
|
}
|
|
|
|
/// Test whether the given binding can be considered an instance of `float`.
|
|
pub fn is_float(binding: &Binding, semantic: &SemanticModel) -> bool {
|
|
check_type::<FloatChecker>(binding, semantic)
|
|
}
|
|
|
|
/// Test whether the given binding can be considered an instance of `str`.
|
|
pub fn is_string(binding: &Binding, semantic: &SemanticModel) -> bool {
|
|
check_type::<StringChecker>(binding, semantic)
|
|
}
|
|
|
|
/// Test whether the given binding can be considered an instance of `bytes`.
|
|
pub fn is_bytes(binding: &Binding, semantic: &SemanticModel) -> bool {
|
|
check_type::<BytesChecker>(binding, semantic)
|
|
}
|
|
|
|
/// Test whether the given binding can be considered a set.
|
|
///
|
|
/// For this, we check what value might be associated with it through it's initialization and
|
|
/// what annotation it has (we consider `set` and `typing.Set`).
|
|
pub fn is_set(binding: &Binding, semantic: &SemanticModel) -> bool {
|
|
check_type::<SetChecker>(binding, semantic)
|
|
}
|
|
|
|
/// Test whether the given binding can be considered a tuple.
|
|
///
|
|
/// For this, we check what value might be associated with it through it's initialization, what
|
|
/// annotation it has (we consider `tuple` and `typing.Tuple`), and if it is a variadic positional
|
|
/// argument.
|
|
pub fn is_tuple(binding: &Binding, semantic: &SemanticModel) -> bool {
|
|
// ```python
|
|
// def foo(*args):
|
|
// ...
|
|
// ```
|
|
if matches!(binding.kind, BindingKind::Argument) {
|
|
if let Some(Stmt::FunctionDef(ast::StmtFunctionDef { parameters, .. })) =
|
|
binding.statement(semantic)
|
|
{
|
|
if let Some(arg_parameter) = parameters.vararg.as_deref() {
|
|
if arg_parameter.name.range() == binding.range() {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
check_type::<TupleChecker>(binding, semantic)
|
|
}
|
|
|
|
/// Test whether the given binding can be considered a file-like object (i.e., a type that
|
|
/// implements `io.IOBase`).
|
|
pub fn is_io_base(binding: &Binding, semantic: &SemanticModel) -> bool {
|
|
check_type::<IoBaseChecker>(binding, semantic)
|
|
}
|
|
|
|
/// Test whether the given expression can be considered a file-like object (i.e., a type that
|
|
/// implements `io.IOBase`).
|
|
pub fn is_io_base_expr(expr: &Expr, semantic: &SemanticModel) -> bool {
|
|
IoBaseChecker::match_initializer(expr, semantic)
|
|
}
|
|
|
|
/// Test whether the given binding can be considered a `pathlib.PurePath`
|
|
/// or an instance of a subclass thereof.
|
|
pub fn is_pathlib_path(binding: &Binding, semantic: &SemanticModel) -> bool {
|
|
check_type::<PathlibPathChecker>(binding, semantic)
|
|
}
|
|
|
|
pub fn is_fastapi_route(binding: &Binding, semantic: &SemanticModel) -> bool {
|
|
check_type::<FastApiRouteChecker>(binding, semantic)
|
|
}
|
|
|
|
/// Test whether the given binding is for an old-style `TypeVar`, `TypeVarTuple` or a `ParamSpec`.
|
|
pub fn is_type_var_like(binding: &Binding, semantic: &SemanticModel) -> bool {
|
|
check_type::<TypeVarLikeChecker>(binding, semantic)
|
|
}
|
|
|
|
/// Find the [`ParameterWithDefault`] corresponding to the given [`Binding`].
|
|
#[inline]
|
|
fn find_parameter<'a>(
|
|
parameters: &'a Parameters,
|
|
binding: &Binding,
|
|
) -> Option<&'a ParameterWithDefault> {
|
|
parameters
|
|
.iter_non_variadic_params()
|
|
.find(|param| param.identifier() == binding.range())
|
|
}
|
|
|
|
/// Return the [`QualifiedName`] of the value to which the given [`Expr`] is assigned, if any.
|
|
///
|
|
/// For example, given:
|
|
/// ```python
|
|
/// import asyncio
|
|
///
|
|
/// loop = asyncio.get_running_loop()
|
|
/// loop.create_task(...)
|
|
/// ```
|
|
///
|
|
/// This function will return `["asyncio", "get_running_loop"]` for the `loop` binding.
|
|
///
|
|
/// This function will also automatically expand attribute accesses, so given:
|
|
/// ```python
|
|
/// from module import AppContainer
|
|
///
|
|
/// container = AppContainer()
|
|
/// container.app.get(...)
|
|
/// ```
|
|
///
|
|
/// This function will return `["module", "AppContainer", "app", "get"]` for the
|
|
/// attribute access `container.app.get`.
|
|
pub fn resolve_assignment<'a>(
|
|
expr: &'a Expr,
|
|
semantic: &'a SemanticModel<'a>,
|
|
) -> Option<QualifiedName<'a>> {
|
|
// Resolve any attribute chain.
|
|
let mut head_expr = expr;
|
|
let mut reversed_tail: SmallVec<[_; 4]> = smallvec![];
|
|
while let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = head_expr {
|
|
head_expr = value;
|
|
reversed_tail.push(attr.as_str());
|
|
}
|
|
|
|
// Resolve the left-most name, e.g. `foo` in `foo.bar.baz` to a qualified name,
|
|
// then append the attributes.
|
|
let name = head_expr.as_name_expr()?;
|
|
let binding_id = semantic.resolve_name(name)?;
|
|
let statement = semantic.binding(binding_id).statement(semantic)?;
|
|
match statement {
|
|
Stmt::Assign(ast::StmtAssign { value, .. })
|
|
| Stmt::AnnAssign(ast::StmtAnnAssign {
|
|
value: Some(value), ..
|
|
}) => {
|
|
let ast::ExprCall { func, .. } = value.as_call_expr()?;
|
|
|
|
let qualified_name = semantic.resolve_qualified_name(func)?;
|
|
Some(qualified_name.extend_members(reversed_tail.into_iter().rev()))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Find the assigned [`Expr`] for a given symbol, if any.
|
|
///
|
|
/// For example given:
|
|
/// ```python
|
|
/// foo = 42
|
|
/// (bar, bla) = 1, "str"
|
|
/// ```
|
|
///
|
|
/// This function will return a `NumberLiteral` with value `Int(42)` when called with `foo` and a
|
|
/// `StringLiteral` with value `"str"` when called with `bla`.
|
|
pub fn find_assigned_value<'a>(symbol: &str, semantic: &'a SemanticModel<'a>) -> Option<&'a Expr> {
|
|
let binding_id = semantic.lookup_symbol(symbol)?;
|
|
let binding = semantic.binding(binding_id);
|
|
find_binding_value(binding, semantic)
|
|
}
|
|
|
|
/// Find the assigned [`Expr`] for a given [`Binding`], if any.
|
|
///
|
|
/// For example given:
|
|
/// ```python
|
|
/// foo = 42
|
|
/// (bar, bla) = 1, "str"
|
|
/// ```
|
|
///
|
|
/// This function will return a `NumberLiteral` with value `Int(42)` when called with `foo` and a
|
|
/// `StringLiteral` with value `"str"` when called with `bla`.
|
|
#[allow(clippy::single_match)]
|
|
pub fn find_binding_value<'a>(binding: &Binding, semantic: &'a SemanticModel) -> Option<&'a Expr> {
|
|
match binding.kind {
|
|
// Ex) `x := 1`
|
|
BindingKind::NamedExprAssignment => {
|
|
let parent_id = binding.source?;
|
|
let parent = semantic
|
|
.expressions(parent_id)
|
|
.find_map(|expr| expr.as_named_expr());
|
|
if let Some(ast::ExprNamed { target, value, .. }) = parent {
|
|
return match_value(binding, target, value);
|
|
}
|
|
}
|
|
// Ex) `x = 1`
|
|
BindingKind::Assignment => match binding.statement(semantic) {
|
|
Some(Stmt::Assign(ast::StmtAssign { value, targets, .. })) => {
|
|
return targets
|
|
.iter()
|
|
.find_map(|target| match_value(binding, target, value))
|
|
}
|
|
Some(Stmt::AnnAssign(ast::StmtAnnAssign {
|
|
value: Some(value),
|
|
target,
|
|
..
|
|
})) => {
|
|
return match_value(binding, target, value);
|
|
}
|
|
_ => {}
|
|
},
|
|
// Ex) `with open("file.txt") as f:`
|
|
BindingKind::WithItemVar => match binding.statement(semantic) {
|
|
Some(Stmt::With(ast::StmtWith { items, .. })) => {
|
|
return items.iter().find_map(|item| {
|
|
let target = item.optional_vars.as_ref()?;
|
|
let value = &item.context_expr;
|
|
match_value(binding, target, value)
|
|
});
|
|
}
|
|
_ => {}
|
|
},
|
|
_ => {}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Given a target and value, find the value that's assigned to the given symbol.
|
|
fn match_value<'a>(binding: &Binding, target: &Expr, value: &'a Expr) -> Option<&'a Expr> {
|
|
match target {
|
|
Expr::Name(name) if name.range() == binding.range() => Some(value),
|
|
Expr::Tuple(ast::ExprTuple { elts, .. }) | Expr::List(ast::ExprList { elts, .. }) => {
|
|
match value {
|
|
Expr::Tuple(ast::ExprTuple {
|
|
elts: value_elts, ..
|
|
})
|
|
| Expr::List(ast::ExprList {
|
|
elts: value_elts, ..
|
|
})
|
|
| Expr::Set(ast::ExprSet {
|
|
elts: value_elts, ..
|
|
}) => match_target(binding, elts, value_elts),
|
|
_ => None,
|
|
}
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Given a target and value, find the value that's assigned to the given symbol.
|
|
fn match_target<'a>(binding: &Binding, targets: &[Expr], values: &'a [Expr]) -> Option<&'a Expr> {
|
|
for (target, value) in targets.iter().zip(values) {
|
|
match target {
|
|
Expr::Tuple(ast::ExprTuple {
|
|
elts: target_elts, ..
|
|
})
|
|
| Expr::List(ast::ExprList {
|
|
elts: target_elts, ..
|
|
})
|
|
| Expr::Set(ast::ExprSet {
|
|
elts: target_elts, ..
|
|
}) => {
|
|
// Collection types can be mismatched like in: (a, b, [c, d]) = [1, 2, {3, 4}]
|
|
match value {
|
|
Expr::Tuple(ast::ExprTuple {
|
|
elts: value_elts, ..
|
|
})
|
|
| Expr::List(ast::ExprList {
|
|
elts: value_elts, ..
|
|
})
|
|
| Expr::Set(ast::ExprSet {
|
|
elts: value_elts, ..
|
|
}) => {
|
|
if let Some(result) = match_target(binding, target_elts, value_elts) {
|
|
return Some(result);
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
Expr::Name(name) => {
|
|
if name.range() == binding.range() {
|
|
return Some(value);
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
None
|
|
}
|