Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/ty_ide/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2497,6 +2497,7 @@ fn completion_kind_from_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Comp
.iter_positive(db)
.find_map(|ty| imp(db, ty, visitor))?,
Type::Dynamic(_)
| Type::Divergent(_)
| Type::Never
| Type::SpecialForm(_)
| Type::KnownInstance(_)
Expand Down
16 changes: 16 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/directives/cast.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ def f(x: Any, y: Unknown, z: Any | str | int):
e = cast(str | int | Any, z) # error: [redundant-cast]
```

Recursive aliases that fall back to `Divergent` should not trigger `redundant-cast`.

```toml
[environment]
python-version = "3.12"
```

```py
from typing import cast

RecursiveAlias = list["RecursiveAlias | None"]

def f(x: RecursiveAlias):
cast(RecursiveAlias, x)
```

## Diagnostic snapshots

<!-- snapshot-diagnostics -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,29 @@ static_assert(has_member(C(), "static_method"))
static_assert(not has_member(C(), "non_existent"))
```

Recursive attribute inference can fall back to `Divergent`, but should still preserve members that
were available before the cycle was introduced:

```py
from ty_extensions import has_member, static_assert

class Base:
def flip(self) -> "Base":
return Base()

class Sub(Base):
pass

class C:
def __init__(self, x: Sub):
self.x = [x]

def replace_with(self, other: "C"):
self.x = [self.x[0].flip()]

static_assert(has_member(C(Sub()).x[0], "flip"))
```

### Class objects

```toml
Expand Down
9 changes: 9 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/typed_dict.md
Original file line number Diff line number Diff line change
Expand Up @@ -1508,17 +1508,24 @@ _ = cast(Bar2, foo) # error: [redundant-cast]
```py
from typing import TypedDict, Final, Literal, Any

RecursiveKey = list["RecursiveKey | None"]

class Person(TypedDict):
name: str
age: int | None

class Animal(TypedDict):
name: str

class Movie(TypedDict):
name: str

NAME_FINAL: Final = "name"
AGE_FINAL: Final[Literal["age"]] = "age"

def _(
recursive_key: RecursiveKey,
movie: Movie,
person: Person,
animal: Animal,
being: Person | Animal,
Expand Down Expand Up @@ -1546,6 +1553,8 @@ def _(
# No error here:
reveal_type(person[unknown_key]) # revealed: Unknown

reveal_type(movie[recursive_key[0]]) # revealed: Unknown

# error: [invalid-key] "Unknown key "anything" for TypedDict `Animal`"
reveal_type(animal["anything"]) # revealed: Unknown

Expand Down
62 changes: 31 additions & 31 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,8 @@ impl<'db> DataclassParams<'db> {
pub enum Type<'db> {
/// The dynamic type: a statically unknown set of values
Dynamic(DynamicType<'db>),
/// A cycle marker used during recursive type inference.
Divergent(DivergentType),
/// The empty set of values
Never,
/// A specific function object
Expand Down Expand Up @@ -777,11 +779,11 @@ impl<'db> Type<'db> {
}

pub(crate) fn divergent(id: salsa::Id) -> Self {
Self::Dynamic(DynamicType::Divergent(DivergentType { id }))
Self::Divergent(DivergentType { id })
}

pub(crate) const fn is_divergent(&self) -> bool {
matches!(self, Type::Dynamic(DynamicType::Divergent(_)))
matches!(self, Type::Divergent(_))
}

pub const fn is_unknown(&self) -> bool {
Expand Down Expand Up @@ -925,7 +927,6 @@ impl<'db> Type<'db> {
DynamicType::Any
| DynamicType::Unknown
| DynamicType::UnknownGeneric(_)
| DynamicType::Divergent(_)
| DynamicType::UnspecializedTypeVar => false,
DynamicType::Todo(_)
| DynamicType::TodoStarredExpression
Expand Down Expand Up @@ -976,7 +977,7 @@ impl<'db> Type<'db> {
}

pub(crate) const fn is_dynamic(&self) -> bool {
matches!(self, Type::Dynamic(_))
matches!(self, Type::Dynamic(_) | Type::Divergent(_))
}

const fn is_non_divergent_dynamic(&self) -> bool {
Expand Down Expand Up @@ -1551,7 +1552,7 @@ impl<'db> Type<'db> {
match self {
Type::Never => Type::object(),

Type::Dynamic(_) => *self,
Type::Dynamic(_) | Type::Divergent(_) => *self,

Type::NominalInstance(instance) if instance.is_object() => Type::Never,

Expand Down Expand Up @@ -1619,6 +1620,7 @@ impl<'db> Type<'db> {
| Type::TypeAlias(_)
| Type::SubclassOf(_)=> true,
Type::Intersection(_)
| Type::Divergent(_)
| Type::SpecialForm(_)
| Type::BoundSuper(_)
| Type::BoundMethod(_)
Expand Down Expand Up @@ -1814,6 +1816,7 @@ impl<'db> Type<'db> {
Type::TypeGuard(type_guard) => {
recursive_type_normalize_type_guard_like(db, type_guard, div, nested)
}
Type::Divergent(_) => Some(self),
Type::Dynamic(dynamic) => Some(Type::Dynamic(dynamic.recursive_type_normalized())),
Type::TypedDict(_) => {
// TODO: Normalize TypedDicts
Expand Down Expand Up @@ -1925,7 +1928,7 @@ impl<'db> Type<'db> {
/// for more complicated types that are actually singletons.
pub(crate) fn is_singleton(self, db: &'db dyn Db) -> bool {
match self {
Type::Dynamic(_) | Type::Never => false,
Type::Dynamic(_) | Type::Divergent(_) | Type::Never => false,

Type::LiteralValue(literal) => match literal.kind() {
LiteralValueTypeKind::Int(..)
Expand Down Expand Up @@ -2114,6 +2117,7 @@ impl<'db> Type<'db> {
Type::TypeAlias(alias) => alias.value_type(db).is_single_valued(db),

Type::Dynamic(_)
| Type::Divergent(_)
| Type::Never
| Type::Union(..)
| Type::Intersection(..)
Expand Down Expand Up @@ -2161,7 +2165,7 @@ impl<'db> Type<'db> {
}))
}

Type::Dynamic(_) | Type::Never => Some(Place::bound(self).into()),
Type::Dynamic(_) | Type::Divergent(_) | Type::Never => Some(Place::bound(self).into()),

Type::ClassLiteral(class) if class.is_typed_dict(db) => {
Some(class.typed_dict_member(db, None, name, policy))
Expand Down Expand Up @@ -2363,7 +2367,7 @@ impl<'db> Type<'db> {
Type::Intersection(intersection) => intersection
.map_with_boundness_and_qualifiers(db, |elem| elem.instance_member(db, name)),

Type::Dynamic(_) | Type::Never => Place::bound(self).into(),
Type::Dynamic(_) | Type::Divergent(_) | Type::Never => Place::bound(self).into(),

Type::NominalInstance(instance) => instance.class(db).instance_member(db, name),
Type::NewTypeInstance(newtype) => {
Expand Down Expand Up @@ -2587,7 +2591,7 @@ impl<'db> Type<'db> {
PlaceAndQualifiers {
place:
Place::Defined(DefinedPlace {
ty: Type::Dynamic(_) | Type::Never,
ty: Type::Dynamic(_) | Type::Divergent(_) | Type::Never,
..
}),
qualifiers: _,
Expand Down Expand Up @@ -2906,7 +2910,7 @@ impl<'db> Type<'db> {
elem.member_lookup_with_policy(db, name_str.into(), policy)
}),

Type::Dynamic(..) | Type::Never => Place::bound(self).into(),
Type::Dynamic(..) | Type::Divergent(_) | Type::Never => Place::bound(self).into(),

Type::FunctionLiteral(function) if name == "__get__" => Place::bound(
Type::KnownBoundMethod(KnownBoundMethodType::FunctionTypeDunderGet(function)),
Expand Down Expand Up @@ -3775,7 +3779,7 @@ impl<'db> Type<'db> {

// Dynamic types are callable, and the return type is the same dynamic type. Similarly,
// `Never` is always callable and returns `Never`.
Type::Dynamic(_) | Type::Never => {
Type::Dynamic(_) | Type::Divergent(_) | Type::Never => {
Binding::single(self, Signature::dynamic(self)).into()
}

Expand Down Expand Up @@ -4870,7 +4874,7 @@ impl<'db> Type<'db> {
return_ty: return_builder.map(IntersectionBuilder::build),
})
}
ty @ (Type::Dynamic(_) | Type::Never) => Some(GeneratorTypes {
ty @ (Type::Dynamic(_) | Type::Divergent(_) | Type::Never) => Some(GeneratorTypes {
yield_ty: Some(ty),
send_ty: Some(ty),
return_ty: Some(ty),
Expand All @@ -4892,7 +4896,7 @@ impl<'db> Type<'db> {
#[must_use]
pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Type::Dynamic(_) | Type::Never => Some(self),
Type::Dynamic(_) | Type::Divergent(_) | Type::Never => Some(self),
Type::ClassLiteral(class) => Some(Type::instance(db, class.default_specialization(db))),
Type::GenericAlias(alias) => Some(Type::instance(db, ClassType::from(alias))),
Type::SubclassOf(subclass_of_ty) => Some(subclass_of_ty.to_instance(db)),
Expand Down Expand Up @@ -5109,7 +5113,7 @@ impl<'db> Type<'db> {
}
}

Type::Dynamic(_) => Ok(*self),
Type::Dynamic(_) | Type::Divergent(_) => Ok(*self),

Type::NominalInstance(instance) => match instance.known_class(db) {
Some(KnownClass::NoneType) => Ok(Type::none(db)),
Expand Down Expand Up @@ -5191,6 +5195,7 @@ impl<'db> Type<'db> {
Type::GenericAlias(alias) => ClassType::from(alias).metaclass(db),
Type::SubclassOf(subclass_of_ty) => subclass_of_ty.to_meta_type(db),
Type::Dynamic(dynamic) => SubclassOfType::from(db, SubclassOfInner::Dynamic(dynamic)),
Type::Divergent(_) => self,
// TODO intersections
Type::Intersection(_) => {
SubclassOfType::try_from_type(db, todo_type!("Intersection meta-type"))
Expand Down Expand Up @@ -5525,19 +5530,15 @@ impl<'db> Type<'db> {
TypeMapping::ReplaceParameterDefaults |
TypeMapping::EagerExpansion |
TypeMapping::RescopeReturnCallables(_) => self,
TypeMapping::Materialize(materialization_kind) => match self {
// `Divergent` is an internal cycle marker rather than a gradual type like
// `Any` or `Unknown`. Materializing it away would destroy the marker we rely
// on for recursive alias convergence.
// TODO: We elsewhere treat `Divergent` as a dynamic type, so failing to
// materialize it away here could lead to odd behavior.
Type::Dynamic(DynamicType::Divergent(_)) => self,
_ => match materialization_kind {
MaterializationKind::Top => Type::object(),
MaterializationKind::Bottom => Type::Never,
},
TypeMapping::Materialize(materialization_kind) => match materialization_kind {
MaterializationKind::Top => Type::object(),
MaterializationKind::Bottom => Type::Never,
}
}
// `Divergent` is an internal cycle marker rather than a gradual type like `Any` or
// `Unknown`. Materializing it away would destroy the marker we rely on for recursive
// alias convergence.
Type::Divergent(_) => self,

Type::Never
| Type::AlwaysTruthy
Expand Down Expand Up @@ -5613,6 +5614,7 @@ impl<'db> Type<'db> {
typevars.insert(bound_typevar);
}
}
Type::Divergent(_) => {}

Type::FunctionLiteral(function) => {
visitor.visit(self, || {
Expand Down Expand Up @@ -5970,9 +5972,9 @@ impl<'db> Type<'db> {
Self::AlwaysFalsy => Type::SpecialForm(SpecialFormType::AlwaysFalsy).definition(db),

// These types have no definition
Self::Dynamic(
DynamicType::Divergent(_)
| DynamicType::Todo(_)
Self::Divergent(_)
| Self::Dynamic(
DynamicType::Todo(_)
| DynamicType::TodoUnpack
| DynamicType::TodoStarredExpression
| DynamicType::TodoTypeVarTuple
Expand Down Expand Up @@ -6149,6 +6151,7 @@ impl<'db> VarianceInferable<'db> for Type<'db> {
Type::TypeGuard(type_guard_type) => type_guard_type.variance_of(db, typevar),
Type::KnownInstance(known_instance) => known_instance.variance_of(db, typevar),
Type::Dynamic(_)
| Type::Divergent(_)
| Type::Never
| Type::WrapperDescriptor(_)
| Type::KnownBoundMethod(_)
Expand Down Expand Up @@ -6459,8 +6462,6 @@ pub enum DynamicType<'db> {
TodoStarredExpression,
/// A special Todo-variant for `TypeVarTuple` instances encountered in type expressions
TodoTypeVarTuple,
/// A type that is determined to be divergent during recursive type inference.
Divergent(DivergentType),
}

impl DynamicType<'_> {
Expand All @@ -6485,7 +6486,6 @@ impl std::fmt::Display for DynamicType<'_> {
DynamicType::TodoUnpack => f.write_str("@Todo(typing.Unpack)"),
DynamicType::TodoStarredExpression => f.write_str("@Todo(StarredExpression)"),
DynamicType::TodoTypeVarTuple => f.write_str("@Todo(TypeVarTuple)"),
DynamicType::Divergent(_) => f.write_str("Divergent"),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/ty_python_semantic/src/types/bool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ impl<'db> Type<'db> {

let truthiness = match self {
Type::Dynamic(_)
| Type::Divergent(_)
| Type::Never
| Type::Callable(_)
| Type::TypeIs(_)
Expand Down
Loading
Loading