Handle type[Any] correctly (#14876)

This adds support for `type[Any]`, which represents an unknown type (not
an instance of an unknown type), and `type`, which we are choosing to
interpret as `type[object]`.

Closes #14546
This commit is contained in:
Douglas Creager
2024-12-10 16:12:37 -05:00
committed by GitHub
parent 03fb2e5ac1
commit d4126f6049
6 changed files with 233 additions and 51 deletions

View File

@@ -0,0 +1,53 @@
# type[Any]
## Simple
```py
def f(x: type[Any]):
reveal_type(x) # revealed: type[Any]
# TODO: could be `<object.__repr__ type> & Any`
reveal_type(x.__repr__) # revealed: Any
class A: ...
x: type[Any] = object
x: type[Any] = type
x: type[Any] = A
x: type[Any] = A() # error: [invalid-assignment]
```
## Bare type
The interpretation of bare `type` is not clear: existing wording in the spec does not match the
behavior of mypy or pyright. For now we interpret it as simply "an instance of `builtins.type`",
which is equivalent to `type[object]`. This is similar to the current behavior of mypy, and pyright
in strict mode.
```py
def f(x: type):
reveal_type(x) # revealed: type
reveal_type(x.__repr__) # revealed: @Todo(instance attributes)
class A: ...
x: type = object
x: type = type
x: type = A
x: type = A() # error: [invalid-assignment]
```
## type[object] != type[Any]
```py
def f(x: type[object]):
reveal_type(x) # revealed: type[object]
# TODO: bound method types
reveal_type(x.__repr__) # revealed: Literal[__repr__]
class A: ...
x: type[object] = object
x: type[object] = type
x: type[object] = A
x: type[object] = A() # error: [invalid-assignment]
```

View File

@@ -562,7 +562,11 @@ impl<'db> Type<'db> {
}
pub const fn subclass_of(class: Class<'db>) -> Self {
Self::SubclassOf(SubclassOfType { class })
Self::subclass_of_base(ClassBase::Class(class))
}
pub const fn subclass_of_base(base: ClassBase<'db>) -> Self {
Self::SubclassOf(SubclassOfType { base })
}
pub fn string_literal(db: &'db dyn Db, string: &str) -> Self {
@@ -607,8 +611,6 @@ impl<'db> Type<'db> {
return false;
}
match (self, target) {
(Type::Unknown | Type::Any | Type::Todo(_), _) => false,
(_, Type::Unknown | Type::Any | Type::Todo(_)) => false,
(Type::Never, _) => true,
(_, Type::Never) => false,
(_, Type::Instance(InstanceType { class }))
@@ -651,19 +653,31 @@ impl<'db> Type<'db> {
},
)
}
(Type::ClassLiteral(..), Type::Instance(InstanceType { class }))
if class.is_known(db, KnownClass::Type) =>
{
true
}
(Type::ClassLiteral(self_class), Type::SubclassOf(target_class)) => {
self_class.class.is_subclass_of(db, target_class.class)
}
(Type::SubclassOf(self_class), Type::SubclassOf(target_class)) => {
self_class.class.is_subclass_of(db, target_class.class)
self_class.class.is_subclass_of_base(db, target_class.base)
}
(
Type::SubclassOf(SubclassOfType { class: self_class }),
Type::Instance(InstanceType { class: self_class }),
Type::SubclassOf(target_class),
) if self_class.is_known(db, KnownClass::Type) => {
self_class.is_subclass_of_base(db, target_class.base)
}
(
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(self_class),
}),
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(target_class),
}),
) => self_class.is_subclass_of(db, target_class),
// C ⊆ type
// type[C] ⊆ type
// Though note that this works regardless of which metaclass C has, not just for type.
(
Type::ClassLiteral(ClassLiteralType { class: self_class })
| Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(self_class),
}),
Type::Instance(InstanceType {
class: target_class,
}),
@@ -760,6 +774,30 @@ impl<'db> Type<'db> {
},
)
}
(
Type::SubclassOf(SubclassOfType {
base: ClassBase::Any,
}),
Type::SubclassOf(_),
) => true,
(
Type::SubclassOf(SubclassOfType {
base: ClassBase::Any,
}),
Type::Instance(target),
) if target.class.is_known(db, KnownClass::Type) => true,
(
Type::Instance(class),
Type::SubclassOf(SubclassOfType {
base: ClassBase::Any,
}),
) if class.class.is_known(db, KnownClass::Type) => true,
(
Type::ClassLiteral(_) | Type::SubclassOf(_),
Type::SubclassOf(SubclassOfType {
base: ClassBase::Any,
}),
) => true,
// TODO other types containing gradual forms (e.g. generics containing Any/Unknown)
_ => self.is_subtype_of(db, target),
}
@@ -775,27 +813,56 @@ impl<'db> Type<'db> {
return false;
}
// TODO: The following is a workaround that is required to unify the two different versions
// of `NoneType` and `NoDefaultType` in typeshed. This should not be required anymore once
// we understand `sys.version_info` branches.
if let (
Type::Instance(InstanceType { class: self_class }),
Type::Instance(InstanceType {
class: target_class,
}),
) = (self, other)
{
let self_known = self_class.known(db);
if matches!(
self_known,
Some(KnownClass::NoneType | KnownClass::NoDefaultType)
) && self_known == target_class.known(db)
{
return true;
}
}
// type[object] ≡ type
if let (
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(object_class),
}),
Type::Instance(InstanceType { class: type_class }),
)
| (
Type::Instance(InstanceType { class: type_class }),
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(object_class),
}),
) = (self, other)
{
// This is the only case where "instance of a class" is equivalent to "subclass of a
// class", so we don't need to fall through if we're not looking at instance[type] and
// type[object] specifically.
return object_class.is_known(db, KnownClass::Object)
&& type_class.is_known(db, KnownClass::Type);
}
// TODO equivalent but not identical structural types, differently-ordered unions and
// intersections, other cases?
// TODO: Once we have support for final classes, we can establish that
// `Type::SubclassOf('FinalClass')` is equivalent to `Type::ClassLiteral('FinalClass')`.
// TODO: The following is a workaround that is required to unify the two different versions
// of `NoneType` and `NoDefaultType` in typeshed. This should not be required anymore once
// we understand `sys.version_info` branches.
// For all other cases, types are equivalent iff they have the same internal
// representation.
self == other
|| matches!((self, other),
(
Type::Instance(InstanceType { class: self_class }),
Type::Instance(InstanceType { class: target_class })
)
if {
let self_known = self_class.known(db);
matches!(self_known, Some(KnownClass::NoneType | KnownClass::NoDefaultType))
&& self_known == target_class.known(db)
}
)
}
/// Returns true if both `self` and `other` are the same gradual form
@@ -862,7 +929,7 @@ impl<'db> Type<'db> {
(Type::SubclassOf(type_class), Type::ClassLiteral(class_literal))
| (Type::ClassLiteral(class_literal), Type::SubclassOf(type_class)) => {
!class_literal.class.is_subclass_of(db, type_class.class)
!class_literal.class.is_subclass_of_base(db, type_class.base)
}
(Type::SubclassOf(_), Type::SubclassOf(_)) => false,
(Type::SubclassOf(_), Type::Instance(_)) | (Type::Instance(_), Type::SubclassOf(_)) => {
@@ -1029,7 +1096,8 @@ impl<'db> Type<'db> {
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::KnownInstance(_) => true,
Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::Instance(_) => {
Type::SubclassOf(SubclassOfType { base }) => matches!(base, ClassBase::Class(_)),
Type::ClassLiteral(_) | Type::Instance(_) => {
// TODO: Ideally, we would iterate over the MRO of the class, check if all
// bases are fully static, and only return `true` if that is the case.
//
@@ -1654,7 +1722,10 @@ impl<'db> Type<'db> {
Type::Unknown => Type::Unknown,
Type::Never => Type::Never,
Type::ClassLiteral(ClassLiteralType { class }) => Type::instance(*class),
Type::SubclassOf(SubclassOfType { class }) => Type::instance(*class),
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(class),
}) => Type::instance(*class),
Type::SubclassOf(_) => Type::Any,
Type::Union(union) => union.map(db, |element| element.to_instance(db)),
// TODO: we can probably do better here: --Alex
Type::Intersection(_) => todo_type!(),
@@ -1683,6 +1754,8 @@ impl<'db> Type<'db> {
#[must_use]
pub fn in_type_expression(&self, db: &'db dyn Db) -> Type<'db> {
match self {
// In a type expression, a bare `type` is interpreted as "instance of `type`", which is
// equivalent to `type[object]`.
Type::ClassLiteral(_) | Type::SubclassOf(_) => self.to_instance(db),
Type::Union(union) => union.map(db, |element| element.in_type_expression(db)),
Type::Unknown => Type::Unknown,
@@ -1742,9 +1815,7 @@ impl<'db> Type<'db> {
pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> {
match self {
Type::Never => Type::Never,
Type::Instance(InstanceType { class }) => {
Type::SubclassOf(SubclassOfType { class: *class })
}
Type::Instance(InstanceType { class }) => Type::subclass_of(*class),
Type::KnownInstance(known_instance) => known_instance.class().to_class_literal(db),
Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
Type::BooleanLiteral(_) => KnownClass::Bool.to_class_literal(db),
@@ -1755,7 +1826,9 @@ impl<'db> Type<'db> {
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db),
Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db),
Type::ClassLiteral(ClassLiteralType { class }) => class.metaclass(db),
Type::SubclassOf(SubclassOfType { class }) => Type::subclass_of(
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(class),
}) => Type::subclass_of(
class
.try_metaclass(db)
.ok()
@@ -1763,10 +1836,9 @@ impl<'db> Type<'db> {
.unwrap_or_else(|| KnownClass::Type.to_class_literal(db).expect_class_literal())
.class,
),
Type::SubclassOf(_) => Type::Any,
Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class_literal(db),
// TODO: `type[Any]`?
Type::Any => Type::Any,
// TODO: `type[Unknown]`?
Type::Unknown => Type::Unknown,
// TODO intersections
Type::Intersection(_) => todo_type!(),
@@ -2737,12 +2809,12 @@ impl<'db> From<ClassLiteralType<'db>> for Type<'db> {
/// A type that represents `type[C]`, i.e. the class literal `C` and class literals that are subclasses of `C`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
pub struct SubclassOfType<'db> {
class: Class<'db>,
base: ClassBase<'db>,
}
impl<'db> SubclassOfType<'db> {
fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
self.class.class_member(db, name)
Type::from(self.base).member(db, name)
}
}
@@ -2961,6 +3033,8 @@ pub(crate) mod tests {
Union(Vec<Ty>),
Intersection { pos: Vec<Ty>, neg: Vec<Ty> },
Tuple(Vec<Ty>),
SubclassOfAny,
SubclassOfBuiltinClass(&'static str),
}
impl Ty {
@@ -2998,6 +3072,13 @@ pub(crate) mod tests {
let elements = tys.into_iter().map(|ty| ty.into_type(db));
Type::tuple(db, elements)
}
Ty::SubclassOfAny => Type::subclass_of_base(ClassBase::Any),
Ty::SubclassOfBuiltinClass(s) => Type::subclass_of(
builtins_symbol(db, s)
.expect_type()
.expect_class_literal()
.class,
),
}
}
}
@@ -3035,6 +3116,26 @@ pub(crate) mod tests {
)]
#[test_case(Ty::Tuple(vec![Ty::Todo]), Ty::Tuple(vec![Ty::IntLiteral(2)]))]
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::Todo]))]
#[test_case(Ty::SubclassOfAny, Ty::SubclassOfAny)]
#[test_case(Ty::SubclassOfAny, Ty::SubclassOfBuiltinClass("object"))]
#[test_case(Ty::SubclassOfAny, Ty::SubclassOfBuiltinClass("str"))]
#[test_case(Ty::SubclassOfAny, Ty::BuiltinInstance("type"))]
#[test_case(Ty::SubclassOfBuiltinClass("object"), Ty::SubclassOfAny)]
#[test_case(
Ty::SubclassOfBuiltinClass("object"),
Ty::SubclassOfBuiltinClass("object")
)]
#[test_case(Ty::SubclassOfBuiltinClass("object"), Ty::BuiltinInstance("type"))]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::SubclassOfAny)]
#[test_case(
Ty::SubclassOfBuiltinClass("str"),
Ty::SubclassOfBuiltinClass("object")
)]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::SubclassOfBuiltinClass("str"))]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::BuiltinInstance("type"))]
#[test_case(Ty::BuiltinInstance("type"), Ty::SubclassOfAny)]
#[test_case(Ty::BuiltinInstance("type"), Ty::SubclassOfBuiltinClass("object"))]
#[test_case(Ty::BuiltinInstance("type"), Ty::BuiltinInstance("type"))]
fn is_assignable_to(from: Ty, to: Ty) {
let db = setup_db();
assert!(from.into_type(&db).is_assignable_to(&db, to.into_type(&db)));
@@ -3052,6 +3153,11 @@ pub(crate) mod tests {
Ty::Union(vec![Ty::IntLiteral(1), Ty::None]),
Ty::Union(vec![Ty::BuiltinInstance("str"), Ty::None])
)]
#[test_case(
Ty::SubclassOfBuiltinClass("object"),
Ty::SubclassOfBuiltinClass("str")
)]
#[test_case(Ty::BuiltinInstance("type"), Ty::SubclassOfBuiltinClass("str"))]
fn is_not_assignable_to(from: Ty, to: Ty) {
let db = setup_db();
assert!(!from.into_type(&db).is_assignable_to(&db, to.into_type(&db)));
@@ -3206,10 +3312,13 @@ pub(crate) mod tests {
Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]),
Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)])
)]
#[test_case(Ty::SubclassOfBuiltinClass("object"), Ty::BuiltinInstance("type"))]
fn is_equivalent_to(from: Ty, to: Ty) {
let db = setup_db();
assert!(from.into_type(&db).is_equivalent_to(&db, to.into_type(&db)));
let from = from.into_type(&db);
let to = to.into_type(&db);
assert!(from.is_equivalent_to(&db, to));
assert!(to.is_equivalent_to(&db, from));
}
#[test_case(Ty::Any, Ty::Any)]
@@ -3220,8 +3329,10 @@ pub(crate) mod tests {
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2), Ty::IntLiteral(3)]))]
fn is_not_equivalent_to(from: Ty, to: Ty) {
let db = setup_db();
assert!(!from.into_type(&db).is_equivalent_to(&db, to.into_type(&db)));
let from = from.into_type(&db);
let to = to.into_type(&db);
assert!(!from.is_equivalent_to(&db, to));
assert!(!to.is_equivalent_to(&db, from));
}
#[test_case(Ty::Never, Ty::Never)]
@@ -3483,6 +3594,9 @@ pub(crate) mod tests {
#[test_case(Ty::Intersection{pos: vec![Ty::KnownClassInstance(KnownClass::Str)], neg: vec![Ty::LiteralString]})]
#[test_case(Ty::Tuple(vec![]))]
#[test_case(Ty::Tuple(vec![Ty::KnownClassInstance(KnownClass::Int), Ty::KnownClassInstance(KnownClass::Object)]))]
#[test_case(Ty::BuiltinInstance("type"))]
#[test_case(Ty::SubclassOfBuiltinClass("object"))]
#[test_case(Ty::SubclassOfBuiltinClass("str"))]
fn is_fully_static(from: Ty) {
let db = setup_db();
@@ -3496,6 +3610,7 @@ pub(crate) mod tests {
#[test_case(Ty::Union(vec![Ty::KnownClassInstance(KnownClass::Str), Ty::Unknown]))]
#[test_case(Ty::Intersection{pos: vec![Ty::Any], neg: vec![Ty::LiteralString]})]
#[test_case(Ty::Tuple(vec![Ty::KnownClassInstance(KnownClass::Int), Ty::Any]))]
#[test_case(Ty::SubclassOfAny)]
fn is_not_fully_static(from: Ty) {
let db = setup_db();

View File

@@ -6,6 +6,7 @@ use ruff_db::display::FormatterJoinExtension;
use ruff_python_ast::str::Quote;
use ruff_python_literal::escape::AsciiEscape;
use crate::types::mro::ClassBase;
use crate::types::{
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType,
SubclassOfType, Type, UnionType,
@@ -83,9 +84,16 @@ impl Display for DisplayRepresentation<'_> {
}
// TODO functions and classes should display using a fully qualified name
Type::ClassLiteral(ClassLiteralType { class }) => f.write_str(class.name(self.db)),
Type::SubclassOf(SubclassOfType { class }) => {
Type::SubclassOf(SubclassOfType {
base: ClassBase::Class(class),
}) => {
// Only show the bare class name here; ClassBase::display would render this as
// type[<class 'Foo'>] instead of type[Foo].
write!(f, "type[{}]", class.name(self.db))
}
Type::SubclassOf(SubclassOfType { base }) => {
write!(f, "type[{}]", base.display(self.db))
}
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)),
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
Type::Union(union) => union.display(self.db).fmt(f),

View File

@@ -57,7 +57,7 @@ use crate::types::diagnostic::{
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_ATTRIBUTE, POSSIBLY_UNBOUND_IMPORT,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
};
use crate::types::mro::MroErrorKind;
use crate::types::mro::{ClassBase, MroErrorKind};
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, todo_type,
@@ -4706,10 +4706,12 @@ impl<'db> TypeInferenceBuilder<'db> {
match slice {
ast::Expr::Name(_) | ast::Expr::Attribute(_) => {
let name_ty = self.infer_expression(slice);
if let Some(ClassLiteralType { class }) = name_ty.into_class_literal() {
Type::subclass_of(class)
} else {
todo_type!("unsupported type[X] special form")
match name_ty {
Type::ClassLiteral(ClassLiteralType { class }) => Type::subclass_of(class),
Type::KnownInstance(KnownInstanceType::Any) => {
Type::subclass_of_base(ClassBase::Any)
}
_ => todo_type!("unsupported type[X] special form"),
}
}
ast::Expr::BinOp(binary) if binary.op == ast::Operator::BitOr => {

View File

@@ -299,8 +299,8 @@ pub(super) enum MroErrorKind<'db> {
/// This is much more limited than the [`Type`] enum:
/// all types that would be invalid to have as a class base are
/// transformed into [`ClassBase::Unknown`]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(super) enum ClassBase<'db> {
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
pub enum ClassBase<'db> {
Any,
Unknown,
Todo,
@@ -308,7 +308,7 @@ pub(super) enum ClassBase<'db> {
}
impl<'db> ClassBase<'db> {
pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
pub fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
struct Display<'db> {
base: ClassBase<'db>,
db: &'db dyn Db,

View File

@@ -64,6 +64,10 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty {
Ty::BuiltinClassLiteral("int"),
Ty::BuiltinClassLiteral("bool"),
Ty::BuiltinClassLiteral("object"),
Ty::BuiltinInstance("type"),
Ty::SubclassOfAny,
Ty::SubclassOfBuiltinClass("object"),
Ty::SubclassOfBuiltinClass("str"),
])
.unwrap()
.clone()