diff --git a/crates/ty_python_semantic/resources/mdtest/directives/cast.md b/crates/ty_python_semantic/resources/mdtest/directives/cast.md index 66ba7938a5..6943eceee6 100644 --- a/crates/ty_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/ty_python_semantic/resources/mdtest/directives/cast.md @@ -73,5 +73,8 @@ def f(x: Any, y: Unknown, z: Any | str | int): c = cast(Unknown, y) reveal_type(c) # revealed: Unknown - d = cast(str | int | Any, z) # error: [redundant-cast] + d = cast(Unknown, x) + reveal_type(d) # revealed: Unknown + + e = cast(str | int | Any, z) # error: [redundant-cast] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md index 885153d32b..ae6631216c 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md @@ -118,6 +118,23 @@ class R: ... static_assert(is_equivalent_to(Intersection[tuple[P | Q], R], Intersection[tuple[Q | P], R])) ``` +## Unions containing generic instances parameterized by unions + +```toml +[environment] +python-version = "3.12" +``` + +```py +from ty_extensions import is_equivalent_to, static_assert + +class A: ... +class B: ... +class Foo[T]: ... + +static_assert(is_equivalent_to(A | Foo[A | B], Foo[B | A] | A)) +``` + ## Callable ### Equivalent diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index cee9d1146b..ccf6557e8f 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -985,15 +985,15 @@ impl<'db> Type<'db> { Type::Tuple(tuple) => Type::Tuple(tuple.normalized(db)), Type::Callable(callable) => Type::Callable(callable.normalized(db)), Type::ProtocolInstance(protocol) => protocol.normalized(db), + Type::NominalInstance(instance) => Type::NominalInstance(instance.normalized(db)), + Type::Dynamic(_) => Type::any(), Type::LiteralString - | Type::NominalInstance(_) | Type::PropertyInstance(_) | Type::AlwaysFalsy | Type::AlwaysTruthy | Type::BooleanLiteral(_) | Type::BytesLiteral(_) | Type::StringLiteral(_) - | Type::Dynamic(_) | Type::Never | Type::FunctionLiteral(_) | Type::MethodWrapper(_) @@ -1007,10 +1007,7 @@ impl<'db> Type<'db> { | Type::IntLiteral(_) | Type::BoundSuper(_) | Type::SubclassOf(_) => self, - Type::GenericAlias(generic) => { - let specialization = generic.specialization(db).normalized(db); - Type::GenericAlias(GenericAlias::new(db, generic.origin(db), specialization)) - } + Type::GenericAlias(generic) => Type::GenericAlias(generic.normalized(db)), Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { Type::TypeVar(TypeVarInstance::new( diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 8a6836253b..59b6efcd04 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -164,6 +164,10 @@ pub struct GenericAlias<'db> { } impl<'db> GenericAlias<'db> { + pub(super) fn normalized(self, db: &'db dyn Db) -> Self { + Self::new(db, self.origin(db), self.specialization(db).normalized(db)) + } + pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> { self.origin(db).definition(db) } @@ -207,6 +211,13 @@ pub enum ClassType<'db> { #[salsa::tracked] impl<'db> ClassType<'db> { + pub(super) fn normalized(self, db: &'db dyn Db) -> Self { + match self { + Self::NonGeneric(_) => self, + Self::Generic(generic) => Self::Generic(generic.normalized(db)), + } + } + /// Returns the class literal and specialization for this class. For a non-generic class, this /// is the class itself. For a generic alias, this is the alias's origin. pub(crate) fn class_literal( diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 7291586242..3a40e660b2 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -5048,16 +5048,16 @@ impl<'db> TypeInferenceBuilder<'db> { overload.parameter_types() { let db = self.db(); - if (source_type.is_equivalent_to(db, *casted_type) - || source_type.normalized(db) - == casted_type.normalized(db)) - && !source_type.any_over_type(db, &|ty| { - matches!( - ty, - Type::Dynamic(dynamic) - if dynamic != DynamicType::Any - ) - }) + let contains_unknown_or_todo = |ty| matches!(ty, Type::Dynamic(dynamic) if dynamic != DynamicType::Any); + if source_type.is_equivalent_to(db, *casted_type) + || (source_type.normalized(db) + == casted_type.normalized(db) + && !casted_type.any_over_type(db, &|ty| { + contains_unknown_or_todo(ty) + }) + && !source_type.any_over_type(db, &|ty| { + contains_unknown_or_todo(ty) + })) { if let Some(builder) = self .context diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 193c0f626e..3a5412f866 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -75,6 +75,10 @@ impl<'db> NominalInstanceType<'db> { } } + pub(super) fn normalized(self, db: &'db dyn Db) -> Self { + Self::from_class(self.class.normalized(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)