[ty] Fix redundant-cast false positives when casting to Unknown (#18111)

This commit is contained in:
Alex Waygood 2025-05-14 22:38:53 -04:00 committed by GitHub
parent b600ff106a
commit 9aa6330bb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 92 additions and 39 deletions

View file

@ -56,6 +56,10 @@ readers of ty's output. For `Unknown` in particular, we may consider it differen
of some opt-in diagnostics, as it indicates that the gradual type has come about due to an invalid of some opt-in diagnostics, as it indicates that the gradual type has come about due to an invalid
annotation, missing annotation or missing type argument somewhere. annotation, missing annotation or missing type argument somewhere.
A cast from `Unknown` to `Todo` or `Any` is also not considered a "redundant cast", as this breaks
the gradual guarantee and leads to cascading errors when an object is inferred as having type
`Unknown` due to a missing import or similar.
```py ```py
from ty_extensions import Unknown from ty_extensions import Unknown
@ -66,5 +70,8 @@ def f(x: Any, y: Unknown, z: Any | str | int):
b = cast(Any, y) b = cast(Any, y)
reveal_type(b) # revealed: Any reveal_type(b) # revealed: Any
c = cast(str | int | Any, z) # error: [redundant-cast] c = cast(Unknown, y)
reveal_type(c) # revealed: Unknown
d = cast(str | int | Any, z) # error: [redundant-cast]
``` ```

View file

@ -657,25 +657,26 @@ impl<'db> Type<'db> {
} }
} }
pub fn contains_todo(&self, db: &'db dyn Db) -> bool { /// Return `true` if `self`, or any of the types contained in `self`, match the closure passed in.
match self { pub fn any_over_type(self, db: &'db dyn Db, type_fn: &dyn Fn(Type<'db>) -> bool) -> bool {
Self::Dynamic(DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec) => true, if type_fn(self) {
return true;
}
match self {
Self::AlwaysFalsy Self::AlwaysFalsy
| Self::AlwaysTruthy | Self::AlwaysTruthy
| Self::Never | Self::Never
| Self::BooleanLiteral(_) | Self::BooleanLiteral(_)
| Self::BytesLiteral(_) | Self::BytesLiteral(_)
| Self::FunctionLiteral(_)
| Self::NominalInstance(_)
| Self::ModuleLiteral(_) | Self::ModuleLiteral(_)
| Self::FunctionLiteral(_)
| Self::ClassLiteral(_) | Self::ClassLiteral(_)
| Self::KnownInstance(_) | Self::KnownInstance(_)
| Self::PropertyInstance(_)
| Self::StringLiteral(_) | Self::StringLiteral(_)
| Self::IntLiteral(_) | Self::IntLiteral(_)
| Self::LiteralString | Self::LiteralString
| Self::Dynamic(DynamicType::Unknown | DynamicType::Any) | Self::Dynamic(_)
| Self::BoundMethod(_) | Self::BoundMethod(_)
| Self::WrapperDescriptor(_) | Self::WrapperDescriptor(_)
| Self::MethodWrapper(_) | Self::MethodWrapper(_)
@ -686,7 +687,8 @@ impl<'db> Type<'db> {
.specialization(db) .specialization(db)
.types(db) .types(db)
.iter() .iter()
.any(|ty| ty.contains_todo(db)), .copied()
.any(|ty| ty.any_over_type(db, type_fn)),
Self::Callable(callable) => { Self::Callable(callable) => {
let signatures = callable.signatures(db); let signatures = callable.signatures(db);
@ -694,54 +696,73 @@ impl<'db> Type<'db> {
signature.parameters().iter().any(|param| { signature.parameters().iter().any(|param| {
param param
.annotated_type() .annotated_type()
.is_some_and(|ty| ty.contains_todo(db)) .is_some_and(|ty| ty.any_over_type(db, type_fn))
}) || signature.return_ty.is_some_and(|ty| ty.contains_todo(db)) }) || signature
.return_ty
.is_some_and(|ty| ty.any_over_type(db, type_fn))
}) })
} }
Self::SubclassOf(subclass_of) => match subclass_of.subclass_of() { Self::SubclassOf(subclass_of) => {
SubclassOfInner::Dynamic( Type::from(subclass_of.subclass_of()).any_over_type(db, type_fn)
DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec, }
) => true,
SubclassOfInner::Dynamic(DynamicType::Unknown | DynamicType::Any) => false,
SubclassOfInner::Class(_) => false,
},
Self::TypeVar(typevar) => match typevar.bound_or_constraints(db) { Self::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
None => false, None => false,
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.contains_todo(db), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.any_over_type(db, type_fn)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.elements(db) .elements(db)
.iter() .iter()
.any(|constraint| constraint.contains_todo(db)), .any(|constraint| constraint.any_over_type(db, type_fn)),
}, },
Self::BoundSuper(bound_super) => { Self::BoundSuper(bound_super) => {
matches!( Type::from(bound_super.pivot_class(db)).any_over_type(db, type_fn)
bound_super.pivot_class(db), || Type::from(bound_super.owner(db)).any_over_type(db, type_fn)
ClassBase::Dynamic(DynamicType::Todo(_))
) || matches!(
bound_super.owner(db),
SuperOwnerKind::Dynamic(DynamicType::Todo(_))
)
} }
Self::Tuple(tuple) => tuple.elements(db).iter().any(|ty| ty.contains_todo(db)), Self::Tuple(tuple) => tuple
.elements(db)
.iter()
.any(|ty| ty.any_over_type(db, type_fn)),
Self::Union(union) => union.elements(db).iter().any(|ty| ty.contains_todo(db)), Self::Union(union) => union
.elements(db)
.iter()
.any(|ty| ty.any_over_type(db, type_fn)),
Self::Intersection(intersection) => { Self::Intersection(intersection) => {
intersection intersection
.positive(db) .positive(db)
.iter() .iter()
.any(|ty| ty.contains_todo(db)) .any(|ty| ty.any_over_type(db, type_fn))
|| intersection || intersection
.negative(db) .negative(db)
.iter() .iter()
.any(|ty| ty.contains_todo(db)) .any(|ty| ty.any_over_type(db, type_fn))
} }
Self::ProtocolInstance(protocol) => protocol.contains_todo(db), Self::ProtocolInstance(protocol) => protocol.any_over_type(db, type_fn),
Self::PropertyInstance(property) => {
property
.getter(db)
.is_some_and(|ty| ty.any_over_type(db, type_fn))
|| property
.setter(db)
.is_some_and(|ty| ty.any_over_type(db, type_fn))
}
Self::NominalInstance(instance) => match instance.class {
ClassType::NonGeneric(_) => false,
ClassType::Generic(generic) => generic
.specialization(db)
.types(db)
.iter()
.any(|ty| ty.any_over_type(db, type_fn)),
},
} }
} }
@ -8172,6 +8193,16 @@ impl<'db> SuperOwnerKind<'db> {
} }
} }
impl<'db> From<SuperOwnerKind<'db>> for Type<'db> {
fn from(owner: SuperOwnerKind<'db>) -> Self {
match owner {
SuperOwnerKind::Dynamic(dynamic) => Type::Dynamic(dynamic),
SuperOwnerKind::Class(class) => class.into(),
SuperOwnerKind::Instance(instance) => instance.into(),
}
}
}
/// Represent a bound super object like `super(PivotClass, owner)` /// Represent a bound super object like `super(PivotClass, owner)`
#[salsa::interned(debug)] #[salsa::interned(debug)]
pub struct BoundSuperType<'db> { pub struct BoundSuperType<'db> {

View file

@ -5051,7 +5051,13 @@ impl<'db> TypeInferenceBuilder<'db> {
if (source_type.is_equivalent_to(db, *casted_type) if (source_type.is_equivalent_to(db, *casted_type)
|| source_type.normalized(db) || source_type.normalized(db)
== casted_type.normalized(db)) == casted_type.normalized(db))
&& !source_type.contains_todo(db) && !source_type.any_over_type(db, &|ty| {
matches!(
ty,
Type::Dynamic(dynamic)
if dynamic != DynamicType::Any
)
})
{ {
if let Some(builder) = self if let Some(builder) = self
.context .context

View file

@ -237,9 +237,13 @@ impl<'db> ProtocolInstanceType<'db> {
} }
} }
/// Return `true` if any of the members of this protocol type contain any `Todo` types. /// Return `true` if the types of any of the members match the closure passed in.
pub(super) fn contains_todo(self, db: &'db dyn Db) -> bool { pub(super) fn any_over_type(
self.inner.interface(db).contains_todo(db) self,
db: &'db dyn Db,
type_fn: &dyn Fn(Type<'db>) -> bool,
) -> bool {
self.inner.interface(db).any_over_type(db, type_fn)
} }
/// Return `true` if this protocol type is fully static. /// Return `true` if this protocol type is fully static.

View file

@ -149,9 +149,14 @@ impl<'db> ProtocolInterface<'db> {
} }
} }
/// Return `true` if any of the members of this protocol type contain any `Todo` types. /// Return `true` if the types of any of the members match the closure passed in.
pub(super) fn contains_todo(self, db: &'db dyn Db) -> bool { pub(super) fn any_over_type(
self.members(db).any(|member| member.ty.contains_todo(db)) self,
db: &'db dyn Db,
type_fn: &dyn Fn(Type<'db>) -> bool,
) -> bool {
self.members(db)
.any(|member| member.ty.any_over_type(db, type_fn))
} }
pub(super) fn normalized(self, db: &'db dyn Db) -> Self { pub(super) fn normalized(self, db: &'db dyn Db) -> Self {