mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 14:52:01 +00:00
[ty] Narrowing for hasattr()
(#18053)
This commit is contained in:
parent
a97e72fb5e
commit
c7b6108cb8
5 changed files with 105 additions and 20 deletions
26
crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md
Normal file
26
crates/ty_python_semantic/resources/mdtest/narrow/hasattr.md
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# Narrowing using `hasattr()`
|
||||||
|
|
||||||
|
The builtin function `hasattr()` can be used to narrow nominal and structural types. This is
|
||||||
|
accomplished using an intersection with a synthesized protocol:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import final
|
||||||
|
|
||||||
|
class Foo: ...
|
||||||
|
|
||||||
|
@final
|
||||||
|
class Bar: ...
|
||||||
|
|
||||||
|
def f(x: Foo):
|
||||||
|
if hasattr(x, "spam"):
|
||||||
|
reveal_type(x) # revealed: Foo & <Protocol with members 'spam'>
|
||||||
|
reveal_type(x.spam) # revealed: object
|
||||||
|
|
||||||
|
if hasattr(x, "not-an-identifier"):
|
||||||
|
reveal_type(x) # revealed: Foo
|
||||||
|
|
||||||
|
def y(x: Bar):
|
||||||
|
if hasattr(x, "spam"):
|
||||||
|
reveal_type(x) # revealed: Never
|
||||||
|
reveal_type(x.spam) # revealed: Never
|
||||||
|
```
|
|
@ -28,7 +28,7 @@ pub(crate) use self::infer::{
|
||||||
infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types,
|
infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types,
|
||||||
infer_scope_types,
|
infer_scope_types,
|
||||||
};
|
};
|
||||||
pub(crate) use self::narrow::KnownConstraintFunction;
|
pub(crate) use self::narrow::ClassInfoConstraintFunction;
|
||||||
pub(crate) use self::signatures::{CallableSignature, Signature, Signatures};
|
pub(crate) use self::signatures::{CallableSignature, Signature, Signatures};
|
||||||
pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};
|
pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};
|
||||||
use crate::module_name::ModuleName;
|
use crate::module_name::ModuleName;
|
||||||
|
@ -6939,6 +6939,9 @@ pub enum KnownFunction {
|
||||||
/// `builtins.issubclass`
|
/// `builtins.issubclass`
|
||||||
#[strum(serialize = "issubclass")]
|
#[strum(serialize = "issubclass")]
|
||||||
IsSubclass,
|
IsSubclass,
|
||||||
|
/// `builtins.hasattr`
|
||||||
|
#[strum(serialize = "hasattr")]
|
||||||
|
HasAttr,
|
||||||
/// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type`
|
/// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type`
|
||||||
RevealType,
|
RevealType,
|
||||||
/// `builtins.len`
|
/// `builtins.len`
|
||||||
|
@ -7005,10 +7008,10 @@ pub enum KnownFunction {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KnownFunction {
|
impl KnownFunction {
|
||||||
pub fn into_constraint_function(self) -> Option<KnownConstraintFunction> {
|
pub fn into_classinfo_constraint_function(self) -> Option<ClassInfoConstraintFunction> {
|
||||||
match self {
|
match self {
|
||||||
Self::IsInstance => Some(KnownConstraintFunction::IsInstance),
|
Self::IsInstance => Some(ClassInfoConstraintFunction::IsInstance),
|
||||||
Self::IsSubclass => Some(KnownConstraintFunction::IsSubclass),
|
Self::IsSubclass => Some(ClassInfoConstraintFunction::IsSubclass),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7027,7 +7030,9 @@ impl KnownFunction {
|
||||||
/// Return `true` if `self` is defined in `module` at runtime.
|
/// Return `true` if `self` is defined in `module` at runtime.
|
||||||
const fn check_module(self, module: KnownModule) -> bool {
|
const fn check_module(self, module: KnownModule) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::IsInstance | Self::IsSubclass | Self::Len | Self::Repr => module.is_builtins(),
|
Self::IsInstance | Self::IsSubclass | Self::HasAttr | Self::Len | Self::Repr => {
|
||||||
|
module.is_builtins()
|
||||||
|
}
|
||||||
Self::AssertType
|
Self::AssertType
|
||||||
| Self::AssertNever
|
| Self::AssertNever
|
||||||
| Self::Cast
|
| Self::Cast
|
||||||
|
@ -8423,6 +8428,7 @@ pub(crate) mod tests {
|
||||||
KnownFunction::Len
|
KnownFunction::Len
|
||||||
| KnownFunction::Repr
|
| KnownFunction::Repr
|
||||||
| KnownFunction::IsInstance
|
| KnownFunction::IsInstance
|
||||||
|
| KnownFunction::HasAttr
|
||||||
| KnownFunction::IsSubclass => KnownModule::Builtins,
|
| KnownFunction::IsSubclass => KnownModule::Builtins,
|
||||||
|
|
||||||
KnownFunction::AbstractMethod => KnownModule::Abc,
|
KnownFunction::AbstractMethod => KnownModule::Abc,
|
||||||
|
|
|
@ -25,6 +25,15 @@ impl<'db> Type<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn synthesized_protocol<'a, M>(db: &'db dyn Db, members: M) -> Self
|
||||||
|
where
|
||||||
|
M: IntoIterator<Item = (&'a str, Type<'db>)>,
|
||||||
|
{
|
||||||
|
Self::ProtocolInstance(ProtocolInstanceType(Protocol::Synthesized(
|
||||||
|
SynthesizedProtocolType::new(db, ProtocolInterface::with_members(db, members)),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Return `true` if `self` conforms to the interface described by `protocol`.
|
/// Return `true` if `self` conforms to the interface described by `protocol`.
|
||||||
///
|
///
|
||||||
/// TODO: we may need to split this into two methods in the future, once we start
|
/// TODO: we may need to split this into two methods in the future, once we start
|
||||||
|
|
|
@ -11,13 +11,16 @@ use crate::types::{
|
||||||
UnionBuilder,
|
UnionBuilder,
|
||||||
};
|
};
|
||||||
use crate::Db;
|
use crate::Db;
|
||||||
|
|
||||||
|
use ruff_python_stdlib::identifiers::is_identifier;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ruff_python_ast as ast;
|
use ruff_python_ast as ast;
|
||||||
use ruff_python_ast::{BoolOp, ExprBoolOp};
|
use ruff_python_ast::{BoolOp, ExprBoolOp};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
|
|
||||||
use super::UnionType;
|
use super::{KnownFunction, UnionType};
|
||||||
|
|
||||||
/// Return the type constraint that `test` (if true) would place on `symbol`, if any.
|
/// Return the type constraint that `test` (if true) would place on `symbol`, if any.
|
||||||
///
|
///
|
||||||
|
@ -138,23 +141,27 @@ fn negative_constraints_for_expression_cycle_initial<'db>(
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Functions that can be used to narrow the type of a first argument using a "classinfo" second argument.
|
||||||
|
///
|
||||||
|
/// A "classinfo" argument is either a class or a tuple of classes, or a tuple of tuples of classes
|
||||||
|
/// (etc. for arbitrary levels of recursion)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum KnownConstraintFunction {
|
pub enum ClassInfoConstraintFunction {
|
||||||
/// `builtins.isinstance`
|
/// `builtins.isinstance`
|
||||||
IsInstance,
|
IsInstance,
|
||||||
/// `builtins.issubclass`
|
/// `builtins.issubclass`
|
||||||
IsSubclass,
|
IsSubclass,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KnownConstraintFunction {
|
impl ClassInfoConstraintFunction {
|
||||||
/// Generate a constraint from the type of a `classinfo` argument to `isinstance` or `issubclass`.
|
/// Generate a constraint from the type of a `classinfo` argument to `isinstance` or `issubclass`.
|
||||||
///
|
///
|
||||||
/// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604
|
/// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604
|
||||||
/// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type.
|
/// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type.
|
||||||
fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option<Type<'db>> {
|
fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option<Type<'db>> {
|
||||||
let constraint_fn = |class| match self {
|
let constraint_fn = |class| match self {
|
||||||
KnownConstraintFunction::IsInstance => Type::instance(db, class),
|
ClassInfoConstraintFunction::IsInstance => Type::instance(db, class),
|
||||||
KnownConstraintFunction::IsSubclass => SubclassOfType::from(db, class),
|
ClassInfoConstraintFunction::IsSubclass => SubclassOfType::from(db, class),
|
||||||
};
|
};
|
||||||
|
|
||||||
match classinfo {
|
match classinfo {
|
||||||
|
@ -704,20 +711,38 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||||
// and `issubclass`, for example `isinstance(x, str | (int | float))`.
|
// and `issubclass`, for example `isinstance(x, str | (int | float))`.
|
||||||
match callable_ty {
|
match callable_ty {
|
||||||
Type::FunctionLiteral(function_type) if expr_call.arguments.keywords.is_empty() => {
|
Type::FunctionLiteral(function_type) if expr_call.arguments.keywords.is_empty() => {
|
||||||
let function = function_type.known(self.db)?.into_constraint_function()?;
|
let [first_arg, second_arg] = &*expr_call.arguments.args else {
|
||||||
|
return None;
|
||||||
let (id, class_info) = match &*expr_call.arguments.args {
|
|
||||||
[first, class_info] => match expr_name(first) {
|
|
||||||
Some(id) => (id, class_info),
|
|
||||||
None => return None,
|
|
||||||
},
|
|
||||||
_ => return None,
|
|
||||||
};
|
};
|
||||||
|
let first_arg = expr_name(first_arg)?;
|
||||||
|
let function = function_type.known(self.db)?;
|
||||||
|
let symbol = self.expect_expr_name_symbol(first_arg);
|
||||||
|
|
||||||
let symbol = self.expect_expr_name_symbol(id);
|
if function == KnownFunction::HasAttr {
|
||||||
|
let attr = inference
|
||||||
|
.expression_type(second_arg.scoped_expression_id(self.db, scope))
|
||||||
|
.into_string_literal()?
|
||||||
|
.value(self.db);
|
||||||
|
|
||||||
|
if !is_identifier(attr) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let constraint = Type::synthesized_protocol(
|
||||||
|
self.db,
|
||||||
|
[(attr, KnownClass::Object.to_instance(self.db))],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Some(NarrowingConstraints::from_iter([(
|
||||||
|
symbol,
|
||||||
|
constraint.negate_if(self.db, !is_positive),
|
||||||
|
)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let function = function.into_classinfo_constraint_function()?;
|
||||||
|
|
||||||
let class_info_ty =
|
let class_info_ty =
|
||||||
inference.expression_type(class_info.scoped_expression_id(self.db, scope));
|
inference.expression_type(second_arg.scoped_expression_id(self.db, scope));
|
||||||
|
|
||||||
function
|
function
|
||||||
.generate_constraint(self.db, class_info_ty)
|
.generate_constraint(self.db, class_info_ty)
|
||||||
|
|
|
@ -70,6 +70,25 @@ pub(super) enum ProtocolInterface<'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'db> ProtocolInterface<'db> {
|
impl<'db> ProtocolInterface<'db> {
|
||||||
|
pub(super) fn with_members<'a, M>(db: &'db dyn Db, members: M) -> Self
|
||||||
|
where
|
||||||
|
M: IntoIterator<Item = (&'a str, Type<'db>)>,
|
||||||
|
{
|
||||||
|
let members: BTreeMap<_, _> = members
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, ty)| {
|
||||||
|
(
|
||||||
|
Name::new(name),
|
||||||
|
ProtocolMemberData {
|
||||||
|
ty: ty.normalized(db),
|
||||||
|
qualifiers: TypeQualifiers::default(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Self::Members(ProtocolInterfaceMembers::new(db, members))
|
||||||
|
}
|
||||||
|
|
||||||
fn empty(db: &'db dyn Db) -> Self {
|
fn empty(db: &'db dyn Db) -> Self {
|
||||||
Self::Members(ProtocolInterfaceMembers::new(db, BTreeMap::default()))
|
Self::Members(ProtocolInterfaceMembers::new(db, BTreeMap::default()))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue