mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-18 09:30:35 +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
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue