From f68dbfdef150edbc8efe76e8ed63255d69ca9315 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 12 May 2025 16:34:14 -0700 Subject: [PATCH] [ty] recognize non-fully-static specializations --- .../mdtest/type_properties/is_fully_static.md | 10 +++++++++ .../mdtest/type_properties/truthiness.md | 2 +- crates/ty_python_semantic/src/types.rs | 21 +++---------------- crates/ty_python_semantic/src/types/class.rs | 19 +++++++++++++++++ .../ty_python_semantic/src/types/generics.rs | 4 ++++ .../ty_python_semantic/src/types/instance.rs | 4 ++++ 6 files changed, 41 insertions(+), 19 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_fully_static.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_fully_static.md index 3551742960..41f2b99017 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_fully_static.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_fully_static.md @@ -130,3 +130,13 @@ static_assert(is_fully_static(TypeOf[static])) static_assert(not is_fully_static(CallableTypeOf[gradual])) static_assert(is_fully_static(CallableTypeOf[static])) ``` + +## Generics + +```py +from typing import Any +from ty_extensions import static_assert, is_fully_static + +static_assert(is_fully_static(list[int])) +static_assert(not is_fully_static(list[Any])) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md index 4cb8bdbc45..008de332eb 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md @@ -112,7 +112,7 @@ from typing_extensions import _NoDefaultType static_assert(is_subtype_of(sys.version_info.__class__, AlwaysTruthy)) static_assert(is_subtype_of(types.EllipsisType, AlwaysTruthy)) static_assert(is_subtype_of(_NoDefaultType, AlwaysTruthy)) -static_assert(is_subtype_of(slice, AlwaysTruthy)) +static_assert(is_subtype_of(slice[int, int, int], AlwaysTruthy)) static_assert(is_subtype_of(types.FunctionType, AlwaysTruthy)) static_assert(is_subtype_of(types.MethodType, AlwaysTruthy)) static_assert(is_subtype_of(typing.TypeVar, AlwaysTruthy)) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 9212781eab..8d7c7d7ec4 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2144,6 +2144,7 @@ impl<'db> Type<'db> { | Type::PropertyInstance(_) => true, Type::ProtocolInstance(protocol) => protocol.is_fully_static(db), + Type::NominalInstance(instance) => instance.is_fully_static(db), Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { None => true, @@ -2159,24 +2160,8 @@ impl<'db> Type<'db> { !matches!(bound_super.pivot_class(db), ClassBase::Dynamic(_)) && !matches!(bound_super.owner(db), SuperOwnerKind::Dynamic(_)) } - Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::NominalInstance(_) => { - // 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. - // - // This does not work yet, because we currently infer `Unknown` for some - // generic base classes that we don't understand yet. For example, `str` - // is defined as `class str(Sequence[str])` in typeshed and we currently - // compute its MRO as `(str, Unknown, object)`. This would make us think - // that `str` is a gradual type, which causes all sorts of downstream - // issues because it does not participate in equivalence/subtyping etc. - // - // Another problem is that we run into problems if we eagerly query the - // MRO of class literals here. I have not fully investigated this, but - // iterating over the MRO alone, without even acting on it, causes us to - // infer `Unknown` for many classes. - - true - } + Type::ClassLiteral(class) => class.is_fully_static(db), + Type::GenericAlias(alias) => alias.is_fully_static(db), Type::Union(union) => union.is_fully_static(db), Type::Intersection(intersection) => intersection.is_fully_static(db), // TODO: Once we support them, make sure that we return `false` for other types diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index e99844a9d8..827d29883a 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -167,6 +167,10 @@ impl<'db> GenericAlias<'db> { self.origin(db).definition(db) } + pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool { + self.origin(db).is_fully_static(db) && self.specialization(db).is_fully_static(db) + } + fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: TypeMapping<'a, 'db>) -> Self { Self::new( db, @@ -246,6 +250,13 @@ impl<'db> ClassType<'db> { self.known(db) == Some(known_class) } + pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool { + match self { + Self::NonGeneric(class_literal) => class_literal.is_fully_static(db), + Self::Generic(alias) => alias.is_fully_static(db), + } + } + /// Return `true` if this class represents the builtin class `object` pub(crate) fn is_object(self, db: &'db dyn Db) -> bool { self.is_known(db, KnownClass::Object) @@ -506,6 +517,14 @@ impl<'db> ClassLiteral<'db> { self.known(db) == Some(known_class) } + #[expect(clippy::unused_self)] + pub(crate) fn is_fully_static(self, _db: &'db dyn Db) -> bool { + // 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. But there may be + // cycle issues trying to infer base classes this eagerly. + true + } + pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { // Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code // the knowledge that this class is not generic. diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index cacad480c8..a646516a55 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -292,6 +292,10 @@ pub struct Specialization<'db> { } impl<'db> Specialization<'db> { + pub(crate) fn is_fully_static(self, db: &'db dyn Db) -> bool { + self.types(db).iter().all(|ty| ty.is_fully_static(db)) + } + pub(crate) fn type_mapping(self) -> TypeMapping<'db, 'db> { TypeMapping::Specialization(self) } diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index b492a55952..d0131b7256 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -66,6 +66,10 @@ impl<'db> NominalInstanceType<'db> { self.class } + pub(super) fn is_fully_static(self, db: &'db dyn Db) -> bool { + self.class.is_fully_static(db) + } + pub(super) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { // N.B. The subclass relation is fully static self.class.is_subclass_of(db, other.class)