//! 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, extend_generics: &[String], ) -> Option { 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 { 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 { /// 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 }