Skip to content
4 changes: 4 additions & 0 deletions crates/ruff_db/src/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,10 @@ impl File {
.map_or(PySourceType::Python, PySourceType::from_extension),
}
}

pub fn structural_ordering(self, db: &dyn Db, other: Self) -> std::cmp::Ordering {
self.path(db).cmp(other.path(db))
}
}

impl fmt::Debug for File {
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_db/src/files/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::fmt::{Display, Formatter};
/// * a file stored on the [host system](crate::system::System).
/// * a virtual file stored on the [host system](crate::system::System).
/// * a vendored file stored in the [vendored file system](crate::vendored::VendoredFileSystem).
#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, get_size2::GetSize)]
pub enum FilePath {
/// Path to a file on the [host system](crate::system::System).
System(SystemPathBuf),
Expand Down
2 changes: 1 addition & 1 deletion crates/ruff_db/src/vendored/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl ToOwned for VendoredPath {
}

#[repr(transparent)]
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
#[derive(Debug, Eq, PartialEq, Clone, Hash, PartialOrd, Ord)]
pub struct VendoredPathBuf(Utf8PathBuf);

impl get_size2::GetSize for VendoredPathBuf {
Expand Down
46 changes: 23 additions & 23 deletions crates/ty_python_semantic/resources/mdtest/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2325,7 +2325,7 @@ class C:
def copy(self, other: "C"):
self.x = other.x

reveal_type(C().x) # revealed: Unknown | Literal[1]
reveal_type(C().x) # revealed: Literal[1] | Unknown
```

If the only assignment to a name is cyclic, we just infer `Unknown` for that attribute:
Expand Down Expand Up @@ -2381,8 +2381,8 @@ class B:
def copy(self, other: "A"):
self.x = other.x

reveal_type(B().x) # revealed: Unknown | Literal[1]
reveal_type(A().x) # revealed: Unknown | Literal[1]
reveal_type(B().x) # revealed: Literal[1] | Unknown
reveal_type(A().x) # revealed: Literal[1] | Unknown

class Base:
def flip(self) -> "Sub":
Expand All @@ -2400,7 +2400,7 @@ class C2:
def replace_with(self, other: "C2"):
self.x = other.x.flip()

reveal_type(C2(Sub()).x) # revealed: Unknown | Base
reveal_type(C2(Sub()).x) # revealed: Base | Unknown

class C3:
def __init__(self, x: Sub):
Expand All @@ -2409,8 +2409,8 @@ class C3:
def replace_with(self, other: "C3"):
self.x = [self.x[0].flip()]

# TODO: should be `Unknown | list[Unknown | Sub] | list[Unknown | Base]`
reveal_type(C3(Sub()).x) # revealed: Unknown | list[Unknown | Sub] | list[Divergent]
# TODO: should be `list[Unknown | Sub] | list[Unknown | Base] | Unknown`
reveal_type(C3(Sub()).x) # revealed: list[Divergent] | list[Unknown | Sub] | Unknown
```

And cycles between many attributes:
Expand Down Expand Up @@ -2453,13 +2453,13 @@ class ManyCycles:
self.x6 = self.x1 + self.x2 + self.x3 + self.x4 + self.x5 + self.x7
self.x7 = self.x1 + self.x2 + self.x3 + self.x4 + self.x5 + self.x6

reveal_type(self.x1) # revealed: Unknown | int
reveal_type(self.x2) # revealed: Unknown | int
reveal_type(self.x3) # revealed: Unknown | int
reveal_type(self.x4) # revealed: Unknown | int
reveal_type(self.x5) # revealed: Unknown | int
reveal_type(self.x6) # revealed: Unknown | int
reveal_type(self.x7) # revealed: Unknown | int
reveal_type(self.x1) # revealed: int | Unknown
reveal_type(self.x2) # revealed: int | Unknown
reveal_type(self.x3) # revealed: int | Unknown
reveal_type(self.x4) # revealed: int | Unknown
reveal_type(self.x5) # revealed: int | Unknown
reveal_type(self.x6) # revealed: int | Unknown
reveal_type(self.x7) # revealed: int | Unknown

class ManyCycles2:
def __init__(self: "ManyCycles2"):
Expand All @@ -2468,8 +2468,8 @@ class ManyCycles2:
self.x3 = [1]

def f1(self: "ManyCycles2"):
# TODO: should be Unknown | list[Unknown | int] | list[Divergent]
reveal_type(self.x3) # revealed: Unknown | list[Unknown | int] | list[Divergent] | list[Divergent]
# TODO: should be list[Unknown | int] | list[Divergent] | Unknown
reveal_type(self.x3) # revealed: list[Divergent] | list[Divergent] | list[Unknown | int] | Unknown

self.x1 = [self.x2] + [self.x3]
self.x2 = [self.x1] + [self.x3]
Expand Down Expand Up @@ -2528,7 +2528,7 @@ class Counter:
def increment(self: "Counter"):
self.count = self.count + 1

reveal_type(Counter().count) # revealed: Unknown | int
reveal_type(Counter().count) # revealed: int | Unknown
```

We also handle infinitely nested generics:
Expand All @@ -2541,7 +2541,7 @@ class NestedLists:
def f(self: "NestedLists"):
self.x = [self.x]

reveal_type(NestedLists().x) # revealed: Unknown | Literal[1] | list[Divergent]
reveal_type(NestedLists().x) # revealed: Literal[1] | list[Divergent] | Unknown

class NestedMixed:
def f(self: "NestedMixed"):
Expand All @@ -2550,7 +2550,7 @@ class NestedMixed:
def g(self: "NestedMixed"):
self.x = {self.x}

reveal_type(NestedMixed().x) # revealed: Unknown | list[Divergent] | set[Divergent]
reveal_type(NestedMixed().x) # revealed: list[Divergent] | set[Divergent] | Unknown
```

And cases where the types originate from annotations:
Expand All @@ -2567,7 +2567,7 @@ class NestedLists2:
def f(self: "NestedLists2"):
self.x = make_list(self.x)

reveal_type(NestedLists2().x) # revealed: Unknown | list[Divergent]
reveal_type(NestedLists2().x) # revealed: list[Divergent] | Unknown
```

### Builtin types attributes
Expand Down Expand Up @@ -2673,7 +2673,7 @@ class C:
def f(self, other: "C"):
self.x = (other.x, 1)

reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]]
reveal_type(C().x) # revealed: tuple[Divergent, Literal[1]] | Unknown
reveal_type(C().x[0]) # revealed: Unknown | Divergent
```

Expand All @@ -2691,7 +2691,7 @@ class D:
def f(self, other: "D"):
self.x = make_tuple(other.x)

reveal_type(D().x) # revealed: Unknown | tuple[Divergent, Literal[1]]
reveal_type(D().x) # revealed: tuple[Divergent, Literal[1]] | Unknown
```

The tuple type may also expand exponentially "in breadth":
Expand All @@ -2704,7 +2704,7 @@ class E:
def f(self: "E"):
self.x = duplicate(self.x)

reveal_type(E().x) # revealed: Unknown | tuple[Divergent, Divergent]
reveal_type(E().x) # revealed: tuple[Divergent, Divergent] | Unknown
```

And it also works for homogeneous tuples:
Expand All @@ -2717,7 +2717,7 @@ class F:
def f(self, other: "F"):
self.x = make_homogeneous_tuple(other.x)

reveal_type(F().x) # revealed: Unknown | tuple[Divergent, ...]
reveal_type(F().x) # revealed: tuple[Divergent, ...] | Unknown
```

## Attributes of standard library modules that aren't yet defined
Expand Down
6 changes: 3 additions & 3 deletions crates/ty_python_semantic/resources/mdtest/call/union.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ class RecursiveAttr:
def update(self):
self.i = self.i + 1

reveal_type(RecursiveAttr().i) # revealed: Unknown | int
reveal_type(RecursiveAttr().i) # revealed: int | Unknown

# Here are some recursive but saturating examples. Because it's difficult to statically determine whether literal unions saturate or diverge,
# we widen them early, even though they may actually be convergent.
Expand All @@ -266,7 +266,7 @@ class RecursiveAttr2:
def update(self):
self.i = (self.i + 1) % 9

reveal_type(RecursiveAttr2().i) # revealed: Unknown | Literal[0, 1, 2, 3, 4, 5, 6, 7, 8]
reveal_type(RecursiveAttr2().i) # revealed: Literal[0, 1, 2, 3, 4, 5, 6, 7, 8] | Unknown

class RecursiveAttr3:
def __init__(self):
Expand All @@ -276,7 +276,7 @@ class RecursiveAttr3:
self.i = (self.i + 1) % 10

# Going beyond the MAX_RECURSIVE_UNION_LITERALS limit:
reveal_type(RecursiveAttr3().i) # revealed: Unknown | int
reveal_type(RecursiveAttr3().i) # revealed: int | Unknown
```

## Simplifying gradually-equivalent types
Expand Down
4 changes: 2 additions & 2 deletions crates/ty_python_semantic/resources/mdtest/cycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class Point:
self.x, self.y = other.x, other.y

p = Point()
reveal_type(p.x) # revealed: Unknown | int
reveal_type(p.y) # revealed: Unknown | int
reveal_type(p.x) # revealed: int | Unknown
reveal_type(p.y) # revealed: int | Unknown
```

## Self-referential bare type alias
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,9 @@ IntOrStr = TypeAliasType(get_name(), int | str)
type OptNestedInt = int | tuple[OptNestedInt, ...] | None

def f(x: OptNestedInt) -> None:
reveal_type(x) # revealed: int | tuple[OptNestedInt, ...] | None
reveal_type(x) # revealed: tuple[OptNestedInt, ...] | None | int
if x is not None:
reveal_type(x) # revealed: int | tuple[OptNestedInt, ...]
reveal_type(x) # revealed: tuple[OptNestedInt, ...] | int
```

### Invalid self-referential
Expand Down Expand Up @@ -328,7 +328,7 @@ class B(A[Alias]):

def f(b: B):
reveal_type(b) # revealed: B
reveal_type(b.attr) # revealed: list[Alias] | int
reveal_type(b.attr) # revealed: int | list[Alias]
```

### Mutually recursive
Expand Down Expand Up @@ -451,5 +451,5 @@ type Y = X | str | dict[str, Y]

def _(y: Y):
if isinstance(y, dict):
reveal_type(y) # revealed: dict[str, X] | dict[str, Y]
reveal_type(y) # revealed: dict[str, Y] | dict[str, X]
```
2 changes: 1 addition & 1 deletion crates/ty_python_semantic/resources/mdtest/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -3029,7 +3029,7 @@ class B(A[P[int]]):
def f(b: B):
reveal_type(b) # revealed: B
reveal_type(b.attr) # revealed: P[int]
reveal_type(b.attr.attr) # revealed: P[int] | int
reveal_type(b.attr.attr) # revealed: int | P[int]
```

### Recursive generic protocols with property members
Expand Down
19 changes: 19 additions & 0 deletions crates/ty_python_semantic/src/module_resolver/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ impl<'db> Module<'db> {
.as_deref()
.unwrap_or_default()
}

pub(crate) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering {
match (self, other) {
(Module::File(left), Module::File(right)) => left.structural_ordering(db, right),
(Module::Namespace(left), Module::Namespace(right)) => {
left.name(db).cmp(right.name(db))
}
(Module::File(_), Module::Namespace(_)) => std::cmp::Ordering::Less,
(Module::Namespace(_), Module::File(_)) => std::cmp::Ordering::Greater,
}
}
}

impl std::fmt::Debug for Module<'_> {
Expand Down Expand Up @@ -274,6 +285,14 @@ pub struct FileModule<'db> {
pub(super) known: Option<KnownModule>,
}

impl FileModule<'_> {
pub(crate) fn structural_ordering(self, db: &dyn Db, other: Self) -> std::cmp::Ordering {
self.name(db)
.cmp(other.name(db))
.then_with(|| self.file(db).structural_ordering(db, other.file(db)))
}
}

/// A namespace package.
///
/// Namespace packages are special because there are
Expand Down
11 changes: 11 additions & 0 deletions crates/ty_python_semantic/src/semantic_index/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,17 @@ impl<'db> Definition<'db> {
_ => None,
}
}

pub(crate) fn structural_ordering(
self,
db: &'db dyn Db,
other: Definition<'db>,
) -> std::cmp::Ordering {
self.file(db)
.cmp(&other.file(db))
.then_with(|| self.file_scope(db).cmp(&other.file_scope(db)))
.then_with(|| self.place(db).cmp(&other.place(db)))
}
}

/// Get the module-level docstring for the given file
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_python_semantic/src/semantic_index/member.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ impl Hash for MemberExprRef<'_> {

/// Uniquely identifies a member in a scope.
#[newtype_index]
#[derive(get_size2::GetSize, salsa::Update)]
#[derive(PartialOrd, Ord, get_size2::GetSize, salsa::Update)]
pub struct ScopedMemberId;

/// The members of a scope. Allows lookup by member path and [`ScopedMemberId`].
Expand Down
4 changes: 3 additions & 1 deletion crates/ty_python_semantic/src/semantic_index/place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ impl std::fmt::Display for PlaceExprRef<'_> {
}

/// ID that uniquely identifies a place inside a [`Scope`](super::FileScopeId).
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, get_size2::GetSize, salsa::Update)]
#[derive(
Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, get_size2::GetSize, salsa::Update,
)]
pub enum ScopedPlaceId {
Symbol(ScopedSymbolId),
Member(ScopedMemberId),
Expand Down
12 changes: 11 additions & 1 deletion crates/ty_python_semantic/src/semantic_index/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,21 @@ impl<'db> ScopeId<'db> {
NodeWithScopeKind::GeneratorExpression(_) => "<generator>",
}
}

pub(crate) fn structural_ordering(
self,
db: &'db dyn Db,
other: ScopeId<'db>,
) -> std::cmp::Ordering {
self.file(db)
.cmp(&other.file(db))
.then_with(|| self.file_scope_id(db).cmp(&other.file_scope_id(db)))
}
}

/// ID that uniquely identifies a scope inside of a module.
#[newtype_index]
#[derive(salsa::Update, get_size2::GetSize)]
#[derive(salsa::Update, get_size2::GetSize, PartialOrd, Ord)]
pub struct FileScopeId;

impl FileScopeId {
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_python_semantic/src/semantic_index/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::ops::{Deref, DerefMut};

/// Uniquely identifies a symbol in a given scope.
#[newtype_index]
#[derive(get_size2::GetSize)]
#[derive(PartialOrd, Ord, get_size2::GetSize)]
pub struct ScopedSymbolId;

/// A symbol in a given scope.
Expand Down
Loading
Loading