diff --git a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md index 974d0be94f..53d272b834 100644 --- a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md +++ b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md @@ -281,12 +281,10 @@ def if_else_exhaustive(x: A[D] | B[E] | C[F]): elif isinstance(x, C): pass else: - # TODO: both of these are false positives (https://github.com/astral-sh/ty/issues/456) - no_diagnostic_here # error: [unresolved-reference] - assert_never(x) # error: [type-assertion-failure] + no_diagnostic_here + assert_never(x) -# TODO: false-positive diagnostic (https://github.com/astral-sh/ty/issues/456) -def if_else_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: # error: [invalid-return-type] +def if_else_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: if isinstance(x, A): return 0 elif isinstance(x, B): diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 17fde60063..d28d261fb1 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -308,3 +308,88 @@ def i[T: Intersection[type[Bar], type[Baz | Spam]], U: (type[Eggs], type[Ham])]( return (y, z) ``` + +## Narrowing with generics + +```toml +[environment] +python-version = "3.12" +``` + +Narrowing to a generic class using `isinstance()` uses the top materialization of the generic. With +a covariant generic, this is equivalent to using the upper bound of the type parameter (by default, +`object`): + +```py +class Covariant[T]: + def get(self) -> T: + raise NotImplementedError + +def _(x: object): + if isinstance(x, Covariant): + reveal_type(x) # revealed: Covariant[object] + reveal_type(x.get()) # revealed: object +``` + +Similarly, contravariant type parameters use their lower bound of `Never`: + +```py +class Contravariant[T]: + def push(self, x: T) -> None: ... + +def _(x: object): + if isinstance(x, Contravariant): + reveal_type(x) # revealed: Contravariant[Never] + # error: [invalid-argument-type] "Argument to bound method `push` is incorrect: Expected `Never`, found `Literal[42]`" + x.push(42) +``` + +Invariant generics are trickiest. The top materialization, conceptually the type that includes all +instances of the generic class regardless of the type parameter, cannot be represented directly in +the type system, so we represent it with the internal `Top[]` special form. + +```py +class Invariant[T]: + def push(self, x: T) -> None: ... + def get(self) -> T: + raise NotImplementedError + +def _(x: object): + if isinstance(x, Invariant): + reveal_type(x) # revealed: Top[Invariant[Unknown]] + reveal_type(x.get()) # revealed: object + # error: [invalid-argument-type] "Argument to bound method `push` is incorrect: Expected `Never`, found `Literal[42]`" + x.push(42) +``` + +When more complex types are involved, the `Top[]` type may get simplified away. + +```py +def _(x: list[int] | set[str]): + if isinstance(x, list): + reveal_type(x) # revealed: list[int] + else: + reveal_type(x) # revealed: set[str] +``` + +Though if the types involved are not disjoint bases, we necessarily keep a more complex type. + +```py +def _(x: Invariant[int] | Covariant[str]): + if isinstance(x, Invariant): + reveal_type(x) # revealed: Invariant[int] | (Covariant[str] & Top[Invariant[Unknown]]) + else: + reveal_type(x) # revealed: Covariant[str] & ~Top[Invariant[Unknown]] +``` + +The behavior of `issubclass()` is similar. + +```py +def _(x: type[object], y: type[object], z: type[object]): + if issubclass(x, Covariant): + reveal_type(x) # revealed: type[Covariant[object]] + if issubclass(y, Contravariant): + reveal_type(y) # revealed: type[Contravariant[Never]] + if issubclass(z, Invariant): + reveal_type(z) # revealed: type[Top[Invariant[Unknown]]] +``` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 60ab729d32..d4a5894053 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -29,10 +29,10 @@ use crate::types::typed_dict::typed_dict_params_from_class_def; use crate::types::{ ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, - IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, NormalizedVisitor, - PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping, TypeRelation, - TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams, UnionBuilder, - VarianceInferable, declaration_type, infer_definition_types, + IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind, + NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping, + TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams, + UnionBuilder, VarianceInferable, declaration_type, infer_definition_types, }; use crate::{ Db, FxIndexMap, FxOrderSet, Program, @@ -1470,6 +1470,18 @@ impl<'db> ClassLiteral<'db> { }) } + pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> { + self.apply_specialization(db, |generic_context| { + generic_context + .default_specialization(db, self.known(db)) + .materialize_impl( + db, + MaterializationKind::Top, + &ApplyTypeMappingVisitor::default(), + ) + }) + } + /// Returns the default specialization of this class. For non-generic classes, the class is /// returned unchanged. For a non-specialized generic class, we return a generic alias that /// applies the default specialization to the class's typevars. diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index fcfae4851a..0c6beb2b7d 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -174,10 +174,10 @@ impl ClassInfoConstraintFunction { fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option> { let constraint_fn = |class: ClassLiteral<'db>| match self { ClassInfoConstraintFunction::IsInstance => { - Type::instance(db, class.default_specialization(db)) + Type::instance(db, class.top_materialization(db)) } ClassInfoConstraintFunction::IsSubclass => { - SubclassOfType::from(db, class.default_specialization(db)) + SubclassOfType::from(db, class.top_materialization(db)) } };