[ty] Narrowing for hasattr() (#18053)

This commit is contained in:
Alex Waygood 2025-05-12 18:58:14 -04:00 committed by GitHub
parent a97e72fb5e
commit c7b6108cb8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 105 additions and 20 deletions

View file

@ -28,7 +28,7 @@ pub(crate) use self::infer::{
infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_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::subclass_of::{SubclassOfInner, SubclassOfType};
use crate::module_name::ModuleName;
@ -6939,6 +6939,9 @@ pub enum KnownFunction {
/// `builtins.issubclass`
#[strum(serialize = "issubclass")]
IsSubclass,
/// `builtins.hasattr`
#[strum(serialize = "hasattr")]
HasAttr,
/// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type`
RevealType,
/// `builtins.len`
@ -7005,10 +7008,10 @@ pub enum KnownFunction {
}
impl KnownFunction {
pub fn into_constraint_function(self) -> Option<KnownConstraintFunction> {
pub fn into_classinfo_constraint_function(self) -> Option<ClassInfoConstraintFunction> {
match self {
Self::IsInstance => Some(KnownConstraintFunction::IsInstance),
Self::IsSubclass => Some(KnownConstraintFunction::IsSubclass),
Self::IsInstance => Some(ClassInfoConstraintFunction::IsInstance),
Self::IsSubclass => Some(ClassInfoConstraintFunction::IsSubclass),
_ => None,
}
}
@ -7027,7 +7030,9 @@ impl KnownFunction {
/// Return `true` if `self` is defined in `module` at runtime.
const fn check_module(self, module: KnownModule) -> bool {
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::AssertNever
| Self::Cast
@ -8423,6 +8428,7 @@ pub(crate) mod tests {
KnownFunction::Len
| KnownFunction::Repr
| KnownFunction::IsInstance
| KnownFunction::HasAttr
| KnownFunction::IsSubclass => KnownModule::Builtins,
KnownFunction::AbstractMethod => KnownModule::Abc,

View file

@ -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`.
///
/// TODO: we may need to split this into two methods in the future, once we start

View file

@ -11,13 +11,16 @@ use crate::types::{
UnionBuilder,
};
use crate::Db;
use ruff_python_stdlib::identifiers::is_identifier;
use itertools::Itertools;
use ruff_python_ast as ast;
use ruff_python_ast::{BoolOp, ExprBoolOp};
use rustc_hash::FxHashMap;
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.
///
@ -138,23 +141,27 @@ fn negative_constraints_for_expression_cycle_initial<'db>(
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)]
pub enum KnownConstraintFunction {
pub enum ClassInfoConstraintFunction {
/// `builtins.isinstance`
IsInstance,
/// `builtins.issubclass`
IsSubclass,
}
impl KnownConstraintFunction {
impl ClassInfoConstraintFunction {
/// 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
/// 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>> {
let constraint_fn = |class| match self {
KnownConstraintFunction::IsInstance => Type::instance(db, class),
KnownConstraintFunction::IsSubclass => SubclassOfType::from(db, class),
ClassInfoConstraintFunction::IsInstance => Type::instance(db, class),
ClassInfoConstraintFunction::IsSubclass => SubclassOfType::from(db, class),
};
match classinfo {
@ -704,20 +711,38 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
// and `issubclass`, for example `isinstance(x, str | (int | float))`.
match callable_ty {
Type::FunctionLiteral(function_type) if expr_call.arguments.keywords.is_empty() => {
let function = function_type.known(self.db)?.into_constraint_function()?;
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, second_arg] = &*expr_call.arguments.args else {
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 =
inference.expression_type(class_info.scoped_expression_id(self.db, scope));
inference.expression_type(second_arg.scoped_expression_id(self.db, scope));
function
.generate_constraint(self.db, class_info_ty)

View file

@ -70,6 +70,25 @@ pub(super) enum 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 {
Self::Members(ProtocolInterfaceMembers::new(db, BTreeMap::default()))
}