Compare commits
1 Commits
charlie/dy
...
jack/loop-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a38a18e2d3 |
@@ -101,7 +101,7 @@ pub(crate) fn private_member_access(checker: &Checker, expr: &Expr) {
|
||||
}
|
||||
}
|
||||
|
||||
// Allow some public functions whose names start with an underscore, like `os._exit()`.
|
||||
// Allow some documented private methods, like `os._exit()`.
|
||||
if let Some(qualified_name) = semantic.resolve_qualified_name(expr) {
|
||||
if matches!(qualified_name.segments(), ["os", "_exit"]) {
|
||||
return;
|
||||
|
||||
@@ -1649,65 +1649,6 @@ Traceb<CURSOR>ackType
|
||||
assert_snapshot!(test.goto_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
/// goto-definition on a dynamic class literal (created via `type()`)
|
||||
#[test]
|
||||
fn goto_definition_dynamic_class_literal() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
DynClass = type("DynClass", (), {})
|
||||
|
||||
x = DynCla<CURSOR>ss()
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r#"
|
||||
info[goto-definition]: Go to definition
|
||||
--> main.py:4:5
|
||||
|
|
||||
2 | DynClass = type("DynClass", (), {})
|
||||
3 |
|
||||
4 | x = DynClass()
|
||||
| ^^^^^^^^ Clicking here
|
||||
|
|
||||
info: Found 2 definitions
|
||||
--> main.py:2:1
|
||||
|
|
||||
2 | DynClass = type("DynClass", (), {})
|
||||
| --------
|
||||
3 |
|
||||
4 | x = DynClass()
|
||||
|
|
||||
::: stdlib/builtins.pyi:137:9
|
||||
|
|
||||
135 | def __class__(self, type: type[Self], /) -> None: ...
|
||||
136 | def __init__(self) -> None: ...
|
||||
137 | def __new__(cls) -> Self: ...
|
||||
| -------
|
||||
138 | # N.B. `object.__setattr__` and `object.__delattr__` are heavily special-cased by type checkers.
|
||||
139 | # Overriding them in subclasses has different semantics, even if the override has an identical signature.
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
/// goto-definition on a dangling dynamic class literal (not assigned to a variable)
|
||||
#[test]
|
||||
fn goto_definition_dangling_dynamic_class_literal() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
class Foo(type("Ba<CURSOR>r", (), {})):
|
||||
pass
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
// TODO: Should only list `a: int`
|
||||
#[test]
|
||||
fn redeclarations() {
|
||||
|
||||
@@ -1208,7 +1208,7 @@ def _(flag: bool):
|
||||
reveal_type(C1.y) # revealed: int | str
|
||||
|
||||
C1.y = 100
|
||||
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3:15'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:8:15'>`"
|
||||
# error: [invalid-assignment] "Object of type `Literal["problematic"]` is not assignable to attribute `y` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:8'>`"
|
||||
C1.y = "problematic"
|
||||
|
||||
class C2:
|
||||
|
||||
@@ -464,42 +464,6 @@ reveal_type(C.f2(1)) # revealed: str
|
||||
reveal_type(C().f2(1)) # revealed: str
|
||||
```
|
||||
|
||||
### Classmethods with `Self` and callable-returning decorators
|
||||
|
||||
When a classmethod is decorated with a decorator that returns a callable type (like
|
||||
`@contextmanager`), `Self` in the return type should correctly resolve to the subclass when accessed
|
||||
on a derived class.
|
||||
|
||||
```py
|
||||
from contextlib import contextmanager
|
||||
from typing import Iterator
|
||||
from typing_extensions import Self
|
||||
|
||||
class Base:
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def create(cls) -> Iterator[Self]:
|
||||
yield cls()
|
||||
|
||||
class Child(Base): ...
|
||||
|
||||
reveal_type(Base.create()) # revealed: _GeneratorContextManager[Base, None, None]
|
||||
with Base.create() as base:
|
||||
reveal_type(base) # revealed: Base
|
||||
|
||||
reveal_type(Base().create()) # revealed: _GeneratorContextManager[Base, None, None]
|
||||
with Base().create() as base:
|
||||
reveal_type(base) # revealed: Base
|
||||
|
||||
reveal_type(Child.create()) # revealed: _GeneratorContextManager[Child, None, None]
|
||||
with Child.create() as child:
|
||||
reveal_type(child) # revealed: Child
|
||||
|
||||
reveal_type(Child().create()) # revealed: _GeneratorContextManager[Child, None, None]
|
||||
with Child().create() as child:
|
||||
reveal_type(child) # revealed: Child
|
||||
```
|
||||
|
||||
### `__init_subclass__`
|
||||
|
||||
The [`__init_subclass__`] method is implicitly a classmethod:
|
||||
|
||||
@@ -20,13 +20,16 @@ class Base: ...
|
||||
class Mixin: ...
|
||||
|
||||
# We synthesize a class type using the name argument
|
||||
reveal_type(type("Foo", (), {})) # revealed: <class 'Foo'>
|
||||
Foo = type("Foo", (), {})
|
||||
reveal_type(Foo) # revealed: <class 'Foo'>
|
||||
|
||||
# With a single base class
|
||||
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: <class 'Foo'>
|
||||
Foo2 = type("Foo", (Base,), {"attr": 1})
|
||||
reveal_type(Foo2) # revealed: <class 'Foo'>
|
||||
|
||||
# With multiple base classes
|
||||
reveal_type(type("Foo", (Base, Mixin), {})) # revealed: <class 'Foo'>
|
||||
Foo3 = type("Foo", (Base, Mixin), {})
|
||||
reveal_type(Foo3) # revealed: <class 'Foo'>
|
||||
|
||||
# The inferred type is assignable to type[Base] since Foo inherits from Base
|
||||
tests: list[type[Base]] = []
|
||||
@@ -74,9 +77,9 @@ def takes_foo2(x: Foo2) -> None: ...
|
||||
takes_foo1(foo1) # OK
|
||||
takes_foo2(foo2) # OK
|
||||
|
||||
# error: [invalid-argument-type] "Argument to function `takes_foo1` is incorrect: Expected `mdtest_snippet.Foo @ src/mdtest_snippet.py:5:8`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:6:8`"
|
||||
# error: [invalid-argument-type] "Argument to function `takes_foo1` is incorrect: Expected `mdtest_snippet.Foo @ src/mdtest_snippet.py:5`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:6`"
|
||||
takes_foo1(foo2)
|
||||
# error: [invalid-argument-type] "Argument to function `takes_foo2` is incorrect: Expected `mdtest_snippet.Foo @ src/mdtest_snippet.py:6:8`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:5:8`"
|
||||
# error: [invalid-argument-type] "Argument to function `takes_foo2` is incorrect: Expected `mdtest_snippet.Foo @ src/mdtest_snippet.py:6`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:5`"
|
||||
takes_foo2(foo1)
|
||||
```
|
||||
|
||||
@@ -112,116 +115,17 @@ reveal_type(bar.base_attr) # revealed: int
|
||||
reveal_type(bar.mixin_attr) # revealed: str
|
||||
```
|
||||
|
||||
Attributes from the namespace dict (third argument) are tracked:
|
||||
Attributes from the namespace dict (third argument) are not tracked. Like Pyright, we error when
|
||||
attempting to access them:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
Foo = type("Foo", (Base,), {"custom_attr": 42})
|
||||
|
||||
# Class attribute access
|
||||
reveal_type(Foo.custom_attr) # revealed: Literal[42]
|
||||
|
||||
# Instance attribute access
|
||||
foo = Foo()
|
||||
reveal_type(foo.custom_attr) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
When the namespace dict is not a literal (e.g., passed as a parameter), attribute access returns
|
||||
`Unknown` since we can't know what attributes might be defined:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class DynamicBase: ...
|
||||
|
||||
def f(attributes: dict[str, Any]):
|
||||
X = type("X", (DynamicBase,), attributes)
|
||||
|
||||
reveal_type(X) # revealed: <class 'X'>
|
||||
|
||||
# Attribute access returns Unknown since the namespace is dynamic
|
||||
reveal_type(X.foo) # revealed: Unknown
|
||||
|
||||
x = X()
|
||||
reveal_type(x.bar) # revealed: Unknown
|
||||
```
|
||||
|
||||
When a namespace dictionary is partially dynamic (e.g., a dict literal with spread or non-literal
|
||||
keys), static attributes have precise types while unknown attributes return `Unknown`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def f(extra_attrs: dict[str, Any], y: str):
|
||||
X = type("X", (), {"a": 42, **extra_attrs})
|
||||
|
||||
# Static attributes in the namespace dictionary have precise types,
|
||||
# but the dictionary was not entirely static, so other attributes
|
||||
# are still available and resolve to `Unknown`:
|
||||
reveal_type(X().a) # revealed: Literal[42]
|
||||
reveal_type(X().whatever) # revealed: Unknown
|
||||
|
||||
Y = type("Y", (), {"a": 56, y: 72})
|
||||
reveal_type(Y().a) # revealed: Literal[56]
|
||||
reveal_type(Y().whatever) # revealed: Unknown
|
||||
```
|
||||
|
||||
When a `TypedDict` is passed as the namespace argument, we synthesize a class type with the known
|
||||
keys from the `TypedDict` as attributes. Since `TypedDict` instances are "open" (they can have
|
||||
arbitrary additional string keys), unknown attributes return `Unknown`:
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
class Namespace(TypedDict):
|
||||
z: int
|
||||
|
||||
def g(attributes: Namespace):
|
||||
Y = type("Y", (), attributes)
|
||||
|
||||
reveal_type(Y) # revealed: <class 'Y'>
|
||||
|
||||
# Known keys from the TypedDict are tracked as attributes
|
||||
reveal_type(Y.z) # revealed: int
|
||||
|
||||
y = Y()
|
||||
reveal_type(y.z) # revealed: int
|
||||
|
||||
# Unknown attributes return Unknown since TypedDicts are open
|
||||
reveal_type(Y.unknown) # revealed: Unknown
|
||||
reveal_type(y.unknown) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Closed TypedDicts (PEP-728)
|
||||
|
||||
TODO: We don't support the PEP-728 `closed=True` keyword argument to `TypedDict` yet. When we do, a
|
||||
closed TypedDict namespace should NOT be marked as dynamic, and accessing unknown attributes should
|
||||
emit an error instead of returning `Unknown`.
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
class ClosedNamespace(TypedDict, closed=True):
|
||||
x: int
|
||||
y: str
|
||||
|
||||
def h(ns: ClosedNamespace):
|
||||
X = type("X", (), ns)
|
||||
|
||||
reveal_type(X) # revealed: <class 'X'>
|
||||
|
||||
# Known keys from the TypedDict are tracked as attributes
|
||||
reveal_type(X.x) # revealed: int
|
||||
reveal_type(X.y) # revealed: str
|
||||
|
||||
x = X()
|
||||
reveal_type(x.x) # revealed: int
|
||||
reveal_type(x.y) # revealed: str
|
||||
|
||||
# TODO: Once we support `closed=True`, these should emit errors instead of returning Unknown
|
||||
reveal_type(X.unknown) # revealed: Unknown
|
||||
reveal_type(x.unknown) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "Object of type `Foo` has no attribute `custom_attr`"
|
||||
reveal_type(foo.custom_attr) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Inheritance from dynamic classes
|
||||
@@ -498,34 +402,37 @@ Other numbers of arguments are invalid:
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
reveal_type(type("Foo", ())) # revealed: Unknown
|
||||
|
||||
# The keyword arguments for `Foo`/`Bar`/`Baz` here are invalid
|
||||
# TODO: the keyword arguments for `Foo`/`Bar`/`Baz` here are invalid
|
||||
# (you cannot pass `metaclass=` to `type()`, and none of them have
|
||||
# base classes with `__init_subclass__` methods).
|
||||
# We return `type[Unknown]` to reduce false positives from attribute access.
|
||||
# base classes with `__init_subclass__` methods),
|
||||
# but `type[Unknown]` would be better than `Unknown` here
|
||||
#
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
reveal_type(type("Foo", (), {}, weird_other_arg=42)) # revealed: type[Unknown]
|
||||
reveal_type(type("Foo", (), {}, weird_other_arg=42)) # revealed: Unknown
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
reveal_type(type("Bar", (int,), {}, weird_other_arg=42)) # revealed: type[Unknown]
|
||||
reveal_type(type("Bar", (int,), {}, weird_other_arg=42)) # revealed: Unknown
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
reveal_type(type("Baz", (), {}, metaclass=type)) # revealed: type[Unknown]
|
||||
reveal_type(type("Baz", (), {}, metaclass=type)) # revealed: Unknown
|
||||
```
|
||||
|
||||
The following calls are also invalid, due to incorrect argument types:
|
||||
The following calls are also invalid, due to incorrect argument types.
|
||||
|
||||
Inline calls (not assigned to a variable) fall back to regular `type` overload matching, which
|
||||
produces slightly different error messages than assigned dynamic class creation:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `Literal[b"Foo"]`"
|
||||
# error: 6 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`"
|
||||
type(b"Foo", (), {})
|
||||
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter 2 (`bases`) of `type()`: Expected `tuple[type, ...]`, found `<class 'Base'>`"
|
||||
# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `<class 'Base'>`"
|
||||
type("Foo", Base, {})
|
||||
|
||||
# error: 14 [invalid-base] "Invalid class base with type `Literal[1]`"
|
||||
# error: 17 [invalid-base] "Invalid class base with type `Literal[2]`"
|
||||
# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`"
|
||||
type("Foo", (1, 2), {})
|
||||
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter 3 (`namespace`) of `type()`: Expected `dict[str, Any]`, found `dict[Unknown | bytes, Unknown | int]`"
|
||||
# error: 22 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[str | bytes, Any]`"
|
||||
type("Foo", (Base,), {b"attr": 1})
|
||||
```
|
||||
|
||||
@@ -592,19 +499,6 @@ class Y(C, B): ...
|
||||
Conflict = type("Conflict", (X, Y), {})
|
||||
```
|
||||
|
||||
## MRO error highlighting (snapshot)
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
This snapshot test documents the diagnostic highlighting range for dynamic class literals.
|
||||
Currently, the entire `type()` call expression is highlighted:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
Dup = type("Dup", (A, A), {}) # error: [duplicate-base]
|
||||
```
|
||||
|
||||
## Metaclass conflicts
|
||||
|
||||
Metaclass conflicts are detected and reported:
|
||||
@@ -619,129 +513,7 @@ class B(metaclass=Meta2): ...
|
||||
Bad = type("Bad", (A, B), {})
|
||||
```
|
||||
|
||||
## `__slots__` in namespace dictionary
|
||||
|
||||
Dynamic classes can define `__slots__` in the namespace dictionary. Non-empty `__slots__` makes the
|
||||
class a "disjoint base", which prevents it from being used alongside other disjoint bases in a class
|
||||
hierarchy:
|
||||
|
||||
```py
|
||||
# Dynamic class with non-empty __slots__
|
||||
Slotted = type("Slotted", (), {"__slots__": ("x", "y")})
|
||||
slotted = Slotted()
|
||||
reveal_type(slotted) # revealed: Slotted
|
||||
|
||||
# Classes with empty __slots__ are not disjoint bases
|
||||
EmptySlots = type("EmptySlots", (), {"__slots__": ()})
|
||||
|
||||
# Classes with no __slots__ are not disjoint bases
|
||||
NoSlots = type("NoSlots", (), {})
|
||||
|
||||
# String __slots__ are treated as a single slot (non-empty)
|
||||
StringSlots = type("StringSlots", (), {"__slots__": "x"})
|
||||
```
|
||||
|
||||
Dynamic classes with non-empty `__slots__` cannot coexist with other disjoint bases:
|
||||
|
||||
```py
|
||||
class RegularSlotted:
|
||||
__slots__ = ("a",)
|
||||
|
||||
DynSlotted = type("DynSlotted", (), {"__slots__": ("b",)})
|
||||
|
||||
# error: [instance-layout-conflict]
|
||||
class Conflict(
|
||||
RegularSlotted,
|
||||
DynSlotted,
|
||||
): ...
|
||||
```
|
||||
|
||||
Two dynamic classes with non-empty `__slots__` also conflict:
|
||||
|
||||
```py
|
||||
A = type("A", (), {"__slots__": ("x",)})
|
||||
B = type("B", (), {"__slots__": ("y",)})
|
||||
|
||||
# error: [instance-layout-conflict]
|
||||
class Conflict(
|
||||
A,
|
||||
B,
|
||||
): ...
|
||||
```
|
||||
|
||||
`instance-layout-conflict` errors are also emitted for classes that inherit from dynamic classes
|
||||
with disjoint bases:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class DisjointBase1:
|
||||
__slots__ = ("a",)
|
||||
|
||||
class DisjointBase2:
|
||||
__slots__ = ("b",)
|
||||
|
||||
def f(ns: dict[str, Any]):
|
||||
cls1 = type("cls1", (DisjointBase1,), ns)
|
||||
cls2 = type("cls2", (DisjointBase2,), ns)
|
||||
|
||||
# error: [instance-layout-conflict]
|
||||
cls3 = type("cls3", (cls1, cls2), {})
|
||||
|
||||
# error: [instance-layout-conflict]
|
||||
class Cls4(cls1, cls2): ...
|
||||
```
|
||||
|
||||
When the namespace dictionary is dynamic (not a literal), we can't determine if `__slots__` is
|
||||
defined, so no diagnostic is emitted:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class SlottedBase:
|
||||
__slots__ = ("a",)
|
||||
|
||||
def f(ns: dict[str, Any]):
|
||||
# The namespace might or might not contain __slots__, so no error is emitted
|
||||
Dynamic = type("Dynamic", (), ns)
|
||||
|
||||
# No error: we can't prove there's a conflict since ns might not have __slots__
|
||||
class MaybeConflict(SlottedBase, Dynamic): ...
|
||||
```
|
||||
|
||||
## `instance-layout-conflict` diagnostic snapshots
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
When the bases are a tuple literal, the diagnostic includes annotations for each conflicting base:
|
||||
|
||||
```py
|
||||
class A:
|
||||
__slots__ = ("x",)
|
||||
|
||||
class B:
|
||||
__slots__ = ("y",)
|
||||
|
||||
# error: [instance-layout-conflict]
|
||||
X = type("X", (A, B), {})
|
||||
```
|
||||
|
||||
When the bases are not a tuple literal (e.g., a variable), the diagnostic is emitted without
|
||||
per-base annotations:
|
||||
|
||||
```py
|
||||
class C:
|
||||
__slots__ = ("x",)
|
||||
|
||||
class D:
|
||||
__slots__ = ("y",)
|
||||
|
||||
bases: tuple[type[C], type[D]] = (C, D)
|
||||
# error: [instance-layout-conflict]
|
||||
Y = type("Y", bases, {})
|
||||
```
|
||||
|
||||
## Cyclic functional class definitions
|
||||
## Cyclic dynamic class definitions
|
||||
|
||||
Self-referential class definitions using `type()` are detected. The name being defined is referenced
|
||||
in the bases tuple before it's available:
|
||||
@@ -769,7 +541,7 @@ def make_classes(name1: str, name2: str):
|
||||
|
||||
def inner(x: cls1): ...
|
||||
|
||||
# error: [invalid-argument-type] "Argument to function `inner` is incorrect: Expected `mdtest_snippet.<locals of function 'make_classes'>.<unknown> @ src/mdtest_snippet.py:8:12`, found `mdtest_snippet.<locals of function 'make_classes'>.<unknown> @ src/mdtest_snippet.py:9:12`"
|
||||
# error: [invalid-argument-type] "Argument to function `inner` is incorrect: Expected `mdtest_snippet.<locals of function 'make_classes'>.<unknown> @ src/mdtest_snippet.py:8`, found `mdtest_snippet.<locals of function 'make_classes'>.<unknown> @ src/mdtest_snippet.py:9`"
|
||||
inner(cls2())
|
||||
```
|
||||
|
||||
@@ -858,51 +630,46 @@ def f(*args, **kwargs):
|
||||
A = type(*args, **kwargs)
|
||||
reveal_type(A) # revealed: type[Unknown]
|
||||
|
||||
# Has a string first arg, but unknown additional args from *args.
|
||||
# We return type[Unknown] since the overload is ambiguous.
|
||||
# Has a string first arg, but unknown additional args from *args
|
||||
B = type("B", *args, **kwargs)
|
||||
reveal_type(B) # revealed: type[Unknown]
|
||||
# TODO: `type[Unknown]` would cause fewer false positives
|
||||
reveal_type(B) # revealed: <class 'str'>
|
||||
|
||||
# Has string and tuple, but unknown additional args from *args.
|
||||
# Has string and tuple, but unknown additional args
|
||||
C = type("C", (), *args, **kwargs)
|
||||
reveal_type(C) # revealed: type[Unknown]
|
||||
# TODO: `type[Unknown]` would cause fewer false positives
|
||||
reveal_type(C) # revealed: type
|
||||
|
||||
# All three positional args provided, only **kwargs unknown.
|
||||
# All three positional args provided, only **kwargs unknown
|
||||
D = type("D", (), {}, **kwargs)
|
||||
reveal_type(D) # revealed: type[Unknown]
|
||||
# TODO: `type[Unknown]` would cause fewer false positives
|
||||
reveal_type(D) # revealed: type
|
||||
|
||||
# Three starred expressions - we can't know how they expand
|
||||
a = ("E",)
|
||||
b = ((),)
|
||||
c = ({},)
|
||||
E = type(*a, *b, *c)
|
||||
reveal_type(E) # revealed: type[Unknown]
|
||||
|
||||
# Non-string first argument with **kwargs - still ambiguous, return type[Unknown]
|
||||
F = type(123, **kwargs)
|
||||
reveal_type(F) # revealed: type[Unknown]
|
||||
# TODO: `type[Unknown]` would cause fewer false positives
|
||||
reveal_type(E) # revealed: type
|
||||
```
|
||||
|
||||
## Explicit type annotations
|
||||
|
||||
When an explicit type annotation is provided, the inferred type is checked against it:
|
||||
TODO: Annotated assignments with `type()` calls don't currently synthesize the specific class type.
|
||||
This will be fixed when we support all `type()` calls (including inline) via generic handling.
|
||||
|
||||
```py
|
||||
# The annotation `type` is compatible with the inferred class literal type
|
||||
T: type = type("T", (), {})
|
||||
reveal_type(T) # revealed: <class 'T'>
|
||||
|
||||
# The annotation `type[Base]` is compatible with the inferred type
|
||||
class Base: ...
|
||||
|
||||
# TODO: Should infer `<class 'T'>` instead of `type`
|
||||
T: type = type("T", (), {})
|
||||
reveal_type(T) # revealed: type
|
||||
|
||||
# TODO: Should infer `<class 'Derived'>` instead of `type[Base]}
|
||||
# error: [invalid-assignment] "Object of type `type` is not assignable to `type[Base]`"
|
||||
Derived: type[Base] = type("Derived", (Base,), {})
|
||||
reveal_type(Derived) # revealed: <class 'Derived'>
|
||||
|
||||
# Incompatible annotation produces an error
|
||||
class Unrelated: ...
|
||||
|
||||
# error: [invalid-assignment]
|
||||
Bad: type[Unrelated] = type("Bad", (Base,), {})
|
||||
reveal_type(Derived) # revealed: type[Base]
|
||||
```
|
||||
|
||||
## Special base classes
|
||||
|
||||
@@ -504,94 +504,3 @@ class HasOrderingMethod:
|
||||
ValidOrderedClass = total_ordering(HasOrderingMethod)
|
||||
reveal_type(ValidOrderedClass) # revealed: type[HasOrderingMethod]
|
||||
```
|
||||
|
||||
## Function call form with `type()`
|
||||
|
||||
When `total_ordering` is called on a class created with `type()`, the same validation is performed:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
|
||||
def lt_impl(self, other) -> bool:
|
||||
return True
|
||||
|
||||
# No error: the functional class defines `__lt__` in its namespace
|
||||
ValidFunctional = total_ordering(type("ValidFunctional", (), {"__lt__": lt_impl}))
|
||||
|
||||
InvalidFunctionalBase = type("InvalidFunctionalBase", (), {})
|
||||
# error: [invalid-total-ordering]
|
||||
InvalidFunctional = total_ordering(InvalidFunctionalBase)
|
||||
```
|
||||
|
||||
## Inherited from functional class
|
||||
|
||||
When a class inherits from a functional class that defines an ordering method, `@total_ordering`
|
||||
correctly detects it:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
|
||||
def lt_impl(self, other) -> bool:
|
||||
return True
|
||||
|
||||
def eq_impl(self, other) -> bool:
|
||||
return True
|
||||
|
||||
# Functional class with __lt__ method
|
||||
OrderedBase = type("OrderedBase", (), {"__lt__": lt_impl})
|
||||
|
||||
# A class inheriting from OrderedBase gets the ordering method
|
||||
@total_ordering
|
||||
class Ordered(OrderedBase):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
o1 = Ordered()
|
||||
o2 = Ordered()
|
||||
|
||||
# Inherited __lt__ is available
|
||||
reveal_type(o1 < o2) # revealed: bool
|
||||
|
||||
# @total_ordering synthesizes the other methods
|
||||
reveal_type(o1 <= o2) # revealed: bool
|
||||
reveal_type(o1 > o2) # revealed: bool
|
||||
reveal_type(o1 >= o2) # revealed: bool
|
||||
```
|
||||
|
||||
When the dynamic base class does not define any ordering method, `@total_ordering` emits an error:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
|
||||
# Dynamic class without ordering methods (invalid for @total_ordering)
|
||||
NoOrderBase = type("NoOrderBase", (), {})
|
||||
|
||||
@total_ordering # error: [invalid-total-ordering]
|
||||
class NoOrder(NoOrderBase):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return True
|
||||
```
|
||||
|
||||
## Dynamic namespace
|
||||
|
||||
When a `type()`-constructed class has a dynamic namespace, we assume it might provide an ordering
|
||||
method (since we can't know what's in the namespace). No error is emitted when such a class is
|
||||
passed to `@total_ordering`:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
from typing import Any
|
||||
|
||||
def f(ns: dict[str, Any]):
|
||||
# Dynamic class with dynamic namespace - might have ordering methods
|
||||
DynamicBase = type("DynamicBase", (), ns)
|
||||
|
||||
# No error: the dynamic namespace might contain __lt__ or another ordering method
|
||||
@total_ordering
|
||||
class Ordered(DynamicBase):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
# Also works when calling total_ordering as a function
|
||||
OrderedDirect = total_ordering(type("OrderedDirect", (), ns))
|
||||
```
|
||||
|
||||
@@ -86,7 +86,7 @@ class MyClass: ...
|
||||
def get_MyClass() -> MyClass:
|
||||
from . import make_MyClass
|
||||
|
||||
# error: [invalid-return-type] "Return type does not match returned value: expected `package.foo.MyClass @ src/package/foo.py:1:7`, found `package.foo.MyClass @ src/package/foo.pyi:1:7`"
|
||||
# error: [invalid-return-type] "Return type does not match returned value: expected `package.foo.MyClass @ src/package/foo.py:1`, found `package.foo.MyClass @ src/package/foo.pyi:1`"
|
||||
return make_MyClass()
|
||||
```
|
||||
|
||||
|
||||
@@ -127,3 +127,64 @@ class NotBoolable:
|
||||
while NotBoolable():
|
||||
...
|
||||
```
|
||||
|
||||
## Backwards control flow
|
||||
|
||||
```py
|
||||
i = 0
|
||||
reveal_type(i) # revealed: Literal[0]
|
||||
while i < 1_000_000:
|
||||
reveal_type(i) # revealed: int
|
||||
i += 1
|
||||
reveal_type(i) # revealed: int
|
||||
reveal_type(i) # revealed: int
|
||||
|
||||
# TODO: None of these should need to be raised to `int`. Loop control flow analysis should take the
|
||||
# loop condition into account.
|
||||
i = 0
|
||||
reveal_type(i) # revealed: Literal[0]
|
||||
while i < 2:
|
||||
# TODO: Should be Literal[0, 1].
|
||||
reveal_type(i) # revealed: int
|
||||
i += 1
|
||||
# TODO: Should be Literal[1, 2].
|
||||
reveal_type(i) # revealed: int
|
||||
# TODO: Should be Literal[2].
|
||||
reveal_type(i) # revealed: int
|
||||
```
|
||||
|
||||
```py
|
||||
def random() -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
i = 0
|
||||
while True:
|
||||
reveal_type(i) # revealed: Literal[0, 1, 2]
|
||||
if random():
|
||||
i = 1
|
||||
else:
|
||||
i = "break"
|
||||
break
|
||||
# To get here we must take the `i = 1` branch above.
|
||||
reveal_type(i) # revealed: Literal[1]
|
||||
if random():
|
||||
i = 2
|
||||
reveal_type(i) # revealed: Literal[1, 2]
|
||||
reveal_type(i) # revealed: Literal["break"]
|
||||
|
||||
i = 0
|
||||
while random():
|
||||
if random():
|
||||
reveal_type(i) # revealed: Literal[0, 1, 2, 3]
|
||||
i = 1
|
||||
reveal_type(i) # revealed: Literal[1]
|
||||
while random():
|
||||
if random():
|
||||
reveal_type(i) # revealed: Literal[1, 0, 2, 3]
|
||||
i = 2
|
||||
reveal_type(i) # revealed: Literal[2]
|
||||
if random():
|
||||
reveal_type(i) # revealed: Literal[1, 2, 0, 3]
|
||||
i = 3
|
||||
reveal_type(i) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
@@ -398,10 +398,10 @@ if returns_bool():
|
||||
else:
|
||||
class B(Y, X): ...
|
||||
|
||||
# revealed: (<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25:11'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>) | (<class 'mdtest_snippet.B @ src/mdtest_snippet.py:28:11'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>)
|
||||
# revealed: (<class 'B'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>) | (<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>)
|
||||
reveal_mro(B)
|
||||
|
||||
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25:11'> | <class 'mdtest_snippet.B @ src/mdtest_snippet.py:28:11'>`"
|
||||
# error: 12 [unsupported-base] "Unsupported class base with type `<class 'mdtest_snippet.B @ src/mdtest_snippet.py:25'> | <class 'mdtest_snippet.B @ src/mdtest_snippet.py:28'>`"
|
||||
class Z(A, B): ...
|
||||
|
||||
reveal_mro(Z) # revealed: (<class 'Z'>, Unknown, <class 'object'>)
|
||||
|
||||
@@ -12,30 +12,6 @@ def _(flag: bool):
|
||||
reveal_type(x) # revealed: None
|
||||
```
|
||||
|
||||
## `None != x` (reversed operands)
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
|
||||
if None != x:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
```
|
||||
|
||||
This also works for `==` with reversed operands:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
|
||||
if None == x:
|
||||
reveal_type(x) # revealed: None
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## `!=` for other singleton types
|
||||
|
||||
### Bool
|
||||
|
||||
@@ -121,31 +121,6 @@ def test(x: Literal["a", "b", "c"] | None | int = None):
|
||||
reveal_type(x) # revealed: Literal["a", "c"] | int
|
||||
```
|
||||
|
||||
## No narrowing for the right-hand side (currently)
|
||||
|
||||
No narrowing is done for the right-hand side currently, even if the right-hand side is a valid
|
||||
"target" (name/attribute/subscript) that could potentially be narrowed. We may change this in the
|
||||
future:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f(x: Literal["abc", "def"]):
|
||||
if "a" in x:
|
||||
# `x` could also be validly narrowed to `Literal["abc"]` here:
|
||||
reveal_type(x) # revealed: Literal["abc", "def"]
|
||||
else:
|
||||
# `x` could also be validly narrowed to `Literal["def"]` here:
|
||||
reveal_type(x) # revealed: Literal["abc", "def"]
|
||||
|
||||
if "a" not in x:
|
||||
# `x` could also be validly narrowed to `Literal["def"]` here:
|
||||
reveal_type(x) # revealed: Literal["abc", "def"]
|
||||
else:
|
||||
# `x` could also be validly narrowed to `Literal["abc"]` here:
|
||||
reveal_type(x) # revealed: Literal["abc", "def"]
|
||||
```
|
||||
|
||||
## bool
|
||||
|
||||
```py
|
||||
|
||||
@@ -16,32 +16,6 @@ def _(flag: bool):
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `None is not x` (reversed operands)
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = None if flag else 1
|
||||
|
||||
if None is not x:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
else:
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
This also works for other singleton types with reversed operands:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = True if flag else False
|
||||
|
||||
if False is not x:
|
||||
reveal_type(x) # revealed: Literal[True]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## `is not` for other singleton types
|
||||
|
||||
Boolean literals:
|
||||
|
||||
@@ -169,15 +169,12 @@ Narrowing does not occur in the same way if `type` is used to dynamically create
|
||||
|
||||
```py
|
||||
def _(x: str | int):
|
||||
# The following diagnostic is valid, since the three-argument form of `type`
|
||||
# can only be called with `str` as the first argument.
|
||||
# Inline type() calls fall back to regular type overload matching.
|
||||
# TODO: Once inline type() calls synthesize class types, this should narrow x to Never.
|
||||
#
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `str | int`"
|
||||
# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `str | int`"
|
||||
if type(x, (), {}) is str:
|
||||
# But we synthesize a new class object as the result of a three-argument call to `type`,
|
||||
# and we know that this synthesized class object is not the same object as the `str` class object,
|
||||
# so here the type is narrowed to `Never`!
|
||||
reveal_type(x) # revealed: Never
|
||||
reveal_type(x) # revealed: str | int
|
||||
else:
|
||||
reveal_type(x) # revealed: str | int
|
||||
```
|
||||
|
||||
@@ -339,7 +339,7 @@ class A: ...
|
||||
|
||||
def f(x: A):
|
||||
# TODO: no error
|
||||
# error: [invalid-assignment] "Object of type `mdtest_snippet.A @ src/mdtest_snippet.py:12:7 | mdtest_snippet.A @ src/mdtest_snippet.py:13:7` is not assignable to `mdtest_snippet.A @ src/mdtest_snippet.py:13:7`"
|
||||
# error: [invalid-assignment] "Object of type `mdtest_snippet.A @ src/mdtest_snippet.py:12 | mdtest_snippet.A @ src/mdtest_snippet.py:13` is not assignable to `mdtest_snippet.A @ src/mdtest_snippet.py:13`"
|
||||
x = A()
|
||||
```
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_as
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3:15'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:7:15'>`
|
||||
error[invalid-assignment]: Object of type `Literal[1]` is not assignable to attribute `attr` on type `<class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:3'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:7'>`
|
||||
--> src/mdtest_snippet.py:11:5
|
||||
|
|
||||
10 | # TODO: The error message here could be improved to explain why the assignment fails.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
|
||||
---
|
||||
mdtest name: attributes.md - Attributes - Diagnostic for function attribute accessed on `Callable` type
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md
|
||||
|
||||
@@ -67,7 +67,7 @@ warning[unsupported-base]: Unsupported class base
|
||||
25 | class C: ...
|
||||
26 |
|
||||
27 | class D(C): ... # error: [unsupported-base]
|
||||
| ^ Has type `<class 'mdtest_snippet.<locals of function 'f'>.C @ src/mdtest_snippet.py:23:15'> | <class 'mdtest_snippet.<locals of function 'f'>.C @ src/mdtest_snippet.py:25:15'>`
|
||||
| ^ Has type `<class 'mdtest_snippet.<locals of function 'f'>.C @ src/mdtest_snippet.py:23'> | <class 'mdtest_snippet.<locals of function 'f'>.C @ src/mdtest_snippet.py:25'>`
|
||||
|
|
||||
info: ty cannot resolve a consistent method resolution order (MRO) for class `D` due to this base
|
||||
info: Only class objects or `Any` are supported as class bases
|
||||
|
||||
@@ -33,7 +33,7 @@ error[invalid-super-argument]: Argument is not a valid class
|
||||
7 | else:
|
||||
8 | class A: ...
|
||||
9 | super(A, A()) # error: [invalid-super-argument]
|
||||
| ^^^^^^^^^^^^^ Argument has type `<class 'mdtest_snippet.<locals of function 'f'>.A @ src/mdtest_snippet.py:6:15'> | <class 'mdtest_snippet.<locals of function 'f'>.A @ src/mdtest_snippet.py:8:15'>`
|
||||
| ^^^^^^^^^^^^^ Argument has type `<class 'mdtest_snippet.<locals of function 'f'>.A @ src/mdtest_snippet.py:6'> | <class 'mdtest_snippet.<locals of function 'f'>.A @ src/mdtest_snippet.py:8'>`
|
||||
|
|
||||
info: rule `invalid-super-argument` is enabled by default
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
|
||||
---
|
||||
mdtest name: type.md - Calls to `type()` - MRO error highlighting (snapshot)
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/call/type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class A: ...
|
||||
2 |
|
||||
3 | Dup = type("Dup", (A, A), {}) # error: [duplicate-base]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[duplicate-base]: Duplicate base class <class 'A'> in class `Dup`
|
||||
--> src/mdtest_snippet.py:3:7
|
||||
|
|
||||
1 | class A: ...
|
||||
2 |
|
||||
3 | Dup = type("Dup", (A, A), {}) # error: [duplicate-base]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `duplicate-base` is enabled by default
|
||||
|
||||
```
|
||||
@@ -1,74 +0,0 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
|
||||
---
|
||||
mdtest name: type.md - Calls to `type()` - `instance-layout-conflict` diagnostic snapshots
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/call/type.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class A:
|
||||
2 | __slots__ = ("x",)
|
||||
3 |
|
||||
4 | class B:
|
||||
5 | __slots__ = ("y",)
|
||||
6 |
|
||||
7 | # error: [instance-layout-conflict]
|
||||
8 | X = type("X", (A, B), {})
|
||||
9 | class C:
|
||||
10 | __slots__ = ("x",)
|
||||
11 |
|
||||
12 | class D:
|
||||
13 | __slots__ = ("y",)
|
||||
14 |
|
||||
15 | bases: tuple[type[C], type[D]] = (C, D)
|
||||
16 | # error: [instance-layout-conflict]
|
||||
17 | Y = type("Y", bases, {})
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
|
||||
--> src/mdtest_snippet.py:8:5
|
||||
|
|
||||
7 | # error: [instance-layout-conflict]
|
||||
8 | X = type("X", (A, B), {})
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ Bases `A` and `B` cannot be combined in multiple inheritance
|
||||
9 | class C:
|
||||
10 | __slots__ = ("x",)
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
--> src/mdtest_snippet.py:8:16
|
||||
|
|
||||
7 | # error: [instance-layout-conflict]
|
||||
8 | X = type("X", (A, B), {})
|
||||
| - - `B` instances have a distinct memory layout because `B` defines non-empty `__slots__`
|
||||
| |
|
||||
| `A` instances have a distinct memory layout because `A` defines non-empty `__slots__`
|
||||
9 | class C:
|
||||
10 | __slots__ = ("x",)
|
||||
|
|
||||
info: rule `instance-layout-conflict` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[instance-layout-conflict]: Class will raise `TypeError` at runtime due to incompatible bases
|
||||
--> src/mdtest_snippet.py:17:5
|
||||
|
|
||||
15 | bases: tuple[type[C], type[D]] = (C, D)
|
||||
16 | # error: [instance-layout-conflict]
|
||||
17 | Y = type("Y", bases, {})
|
||||
| ^^^^^^^^^^^^^^^^^^^^ Bases `C` and `D` cannot be combined in multiple inheritance
|
||||
|
|
||||
info: Two classes cannot coexist in a class's MRO if their instances have incompatible memory layouts
|
||||
info: rule `instance-layout-conflict` is enabled by default
|
||||
|
||||
```
|
||||
@@ -229,6 +229,9 @@ pub(crate) struct SemanticIndex<'db> {
|
||||
/// Map from a standalone expression to its [`Expression`] ingredient.
|
||||
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
|
||||
|
||||
/// Map from loop-header definitions to their constituent definitions.
|
||||
loop_header_definitions: FxHashMap<Definition<'db>, Vec<Definition<'db>>>,
|
||||
|
||||
/// Map from nodes that create a scope to the scope they create.
|
||||
scopes_by_node: FxHashMap<NodeWithScopeKey, FileScopeId>,
|
||||
|
||||
@@ -319,6 +322,15 @@ impl<'db> SemanticIndex<'db> {
|
||||
self.scope_ids_by_scope.iter().copied()
|
||||
}
|
||||
|
||||
pub(crate) fn loop_header_definitions(
|
||||
&self,
|
||||
definition: Definition<'db>,
|
||||
) -> Option<&[Definition<'db>]> {
|
||||
self.loop_header_definitions
|
||||
.get(&definition)
|
||||
.map(|defs| defs.as_slice())
|
||||
}
|
||||
|
||||
pub(crate) fn symbol_is_global_in_scope(
|
||||
&self,
|
||||
symbol: ScopedSymbolId,
|
||||
|
||||
@@ -141,4 +141,10 @@ pub(crate) mod node_key {
|
||||
Self(NodeKey::from_node(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExpressionNodeKey> for NodeKey {
|
||||
fn from(value: ExpressionNodeKey) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,14 +19,15 @@ use ty_module_resolver::{ModuleName, resolve_module};
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::semantic_index::ast_ids::AstIdsBuilder;
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::{AstIdsBuilder, ScopedUseId};
|
||||
use crate::semantic_index::definition::{
|
||||
AnnotatedAssignmentDefinitionNodeRef, AssignmentDefinitionNodeRef,
|
||||
ComprehensionDefinitionNodeRef, Definition, DefinitionCategory, DefinitionNodeKey,
|
||||
DefinitionNodeRef, Definitions, ExceptHandlerDefinitionNodeRef, ForStmtDefinitionNodeRef,
|
||||
ImportDefinitionNodeRef, ImportFromDefinitionNodeRef, ImportFromSubmoduleDefinitionNodeRef,
|
||||
MatchPatternDefinitionNodeRef, StarImportDefinitionNodeRef, WithItemDefinitionNodeRef,
|
||||
ComprehensionDefinitionNodeRef, Definition, DefinitionCategory, DefinitionKind,
|
||||
DefinitionNodeKey, DefinitionNodeRef, Definitions, ExceptHandlerDefinitionNodeRef,
|
||||
ForStmtDefinitionNodeRef, ImportDefinitionNodeRef, ImportFromDefinitionNodeRef,
|
||||
ImportFromSubmoduleDefinitionNodeRef, LoopHeaderDefinitionKind, MatchPatternDefinitionNodeRef,
|
||||
StarImportDefinitionNodeRef, WithItemDefinitionNodeRef,
|
||||
};
|
||||
use crate::semantic_index::expression::{Expression, ExpressionKind};
|
||||
use crate::semantic_index::place::{PlaceExpr, PlaceTableBuilder, ScopedPlaceId};
|
||||
@@ -43,6 +44,7 @@ use crate::semantic_index::scope::{
|
||||
};
|
||||
use crate::semantic_index::scope::{Scope, ScopeId, ScopeKind, ScopeLaziness};
|
||||
use crate::semantic_index::symbol::{ScopedSymbolId, Symbol};
|
||||
use crate::semantic_index::use_def::Bindings;
|
||||
use crate::semantic_index::use_def::{
|
||||
EnclosingSnapshotKey, FlowSnapshot, ScopedEnclosingSnapshotId, UseDefMapBuilder,
|
||||
};
|
||||
@@ -53,22 +55,35 @@ use crate::{Db, Program};
|
||||
|
||||
mod except_handlers;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct LoopUse {
|
||||
place: ScopedPlaceId,
|
||||
use_id: ScopedUseId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Loop {
|
||||
/// Flow states at each `break` in the current loop.
|
||||
break_states: Vec<FlowSnapshot>,
|
||||
uses: Vec<LoopUse>,
|
||||
defined_places: FxHashSet<ScopedPlaceId>,
|
||||
}
|
||||
|
||||
impl Loop {
|
||||
fn push_break(&mut self, state: FlowSnapshot) {
|
||||
self.break_states.push(state);
|
||||
}
|
||||
|
||||
fn record_definition(&mut self, place: ScopedPlaceId) {
|
||||
self.defined_places.insert(place);
|
||||
}
|
||||
}
|
||||
|
||||
struct ScopeInfo {
|
||||
file_scope_id: FileScopeId,
|
||||
/// Current loop state; None if we are not currently visiting a loop
|
||||
current_loop: Option<Loop>,
|
||||
condition_place_uses: Option<FxHashSet<ScopedPlaceId>>,
|
||||
}
|
||||
|
||||
pub(super) struct SemanticIndexBuilder<'db, 'ast> {
|
||||
@@ -109,6 +124,7 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> {
|
||||
scopes_by_expression: ExpressionsScopeMapBuilder,
|
||||
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
|
||||
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
|
||||
loop_header_definitions: FxHashMap<Definition<'db>, Vec<Definition<'db>>>,
|
||||
imported_modules: FxHashSet<ModuleName>,
|
||||
seen_submodule_imports: FxHashSet<String>,
|
||||
/// Hashset of all [`FileScopeId`]s that correspond to [generator functions].
|
||||
@@ -147,6 +163,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
scopes_by_node: FxHashMap::default(),
|
||||
definitions_by_node: FxHashMap::default(),
|
||||
expressions_by_node: FxHashMap::default(),
|
||||
loop_header_definitions: FxHashMap::default(),
|
||||
|
||||
seen_submodule_imports: FxHashSet::default(),
|
||||
imported_modules: FxHashSet::default(),
|
||||
@@ -256,8 +273,17 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
|
||||
/// Pop a loop, replacing with the previous saved outer loop, if any.
|
||||
fn pop_loop(&mut self, outer_loop: Option<Loop>) -> Loop {
|
||||
std::mem::replace(&mut self.current_scope_info_mut().current_loop, outer_loop)
|
||||
.expect("pop_loop() should not be called without a prior push_loop()")
|
||||
let inner_loop = std::mem::take(&mut self.current_scope_info_mut().current_loop)
|
||||
.expect("pop_loop() should not be called without a prior push_loop()");
|
||||
let merged_outer = outer_loop.map(|mut outer| {
|
||||
outer.uses.extend(inner_loop.uses.iter().cloned());
|
||||
outer
|
||||
.defined_places
|
||||
.extend(inner_loop.defined_places.iter().copied());
|
||||
outer
|
||||
});
|
||||
self.current_scope_info_mut().current_loop = merged_outer;
|
||||
inner_loop
|
||||
}
|
||||
|
||||
fn current_loop_mut(&mut self) -> Option<&mut Loop> {
|
||||
@@ -308,6 +334,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
self.scope_stack.push(ScopeInfo {
|
||||
file_scope_id,
|
||||
current_loop: None,
|
||||
condition_place_uses: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -656,6 +683,9 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
place: ScopedPlaceId,
|
||||
definition_node: impl Into<DefinitionNodeRef<'ast, 'db>> + std::fmt::Debug + Copy,
|
||||
) -> Definition<'db> {
|
||||
if let Some(current_loop) = self.current_loop_mut() {
|
||||
current_loop.record_definition(place);
|
||||
}
|
||||
let (definition, num_definitions) = self.push_additional_definition(place, definition_node);
|
||||
debug_assert_eq!(
|
||||
num_definitions, 1,
|
||||
@@ -755,6 +785,40 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
(definition, num_definitions)
|
||||
}
|
||||
|
||||
fn add_loop_header_definition(
|
||||
&mut self,
|
||||
place: ScopedPlaceId,
|
||||
loop_node: &'ast ast::StmtWhile,
|
||||
definitions: Vec<Definition<'db>>,
|
||||
seed_definitions: Vec<Definition<'db>>,
|
||||
bindings: Bindings,
|
||||
seed_bindings: Bindings,
|
||||
) -> Definition<'db> {
|
||||
let kind = DefinitionKind::LoopHeader(LoopHeaderDefinitionKind::new(
|
||||
AstNodeRef::new(self.module, loop_node),
|
||||
definitions,
|
||||
seed_definitions,
|
||||
bindings,
|
||||
seed_bindings,
|
||||
));
|
||||
let is_reexported = kind.is_reexported();
|
||||
let definition = Definition::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
place,
|
||||
kind,
|
||||
is_reexported,
|
||||
);
|
||||
|
||||
self.add_entry_for_definition_key(DefinitionNodeKey::from_node_key(NodeKey::from_node(
|
||||
loop_node,
|
||||
)))
|
||||
.push(definition);
|
||||
|
||||
definition
|
||||
}
|
||||
|
||||
fn record_expression_narrowing_constraint(
|
||||
&mut self,
|
||||
predicate_node: &ast::Expr,
|
||||
@@ -1318,6 +1382,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
scopes: self.scopes,
|
||||
definitions_by_node: self.definitions_by_node,
|
||||
expressions_by_node: self.expressions_by_node,
|
||||
loop_header_definitions: self.loop_header_definitions,
|
||||
scope_ids_by_scope: self.scope_ids_by_scope,
|
||||
ast_ids,
|
||||
scopes_by_expression: self.scopes_by_expression.build(),
|
||||
@@ -1341,6 +1406,96 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> {
|
||||
self.source_text
|
||||
.get_or_init(|| source_text(self.db, self.file))
|
||||
}
|
||||
|
||||
fn record_use(&mut self, place_id: ScopedPlaceId, expr_node_key: ExpressionNodeKey) {
|
||||
if let ScopedPlaceId::Symbol(symbol_id) = place_id {
|
||||
self.mark_symbol_used(symbol_id);
|
||||
}
|
||||
let use_id = self.current_ast_ids().record_use(expr_node_key);
|
||||
self.current_use_def_map_mut()
|
||||
.record_use(place_id, use_id, expr_node_key.into());
|
||||
if let Some(condition_place_uses) = &mut self.current_scope_info_mut().condition_place_uses
|
||||
{
|
||||
condition_place_uses.insert(place_id);
|
||||
}
|
||||
if let Some(current_loop) = self.current_loop_mut() {
|
||||
current_loop.uses.push(LoopUse {
|
||||
place: place_id,
|
||||
use_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn create_loop_header_definitions(
|
||||
&mut self,
|
||||
loop_node: &'ast ast::StmtWhile,
|
||||
loop_state: &Loop,
|
||||
pre_loop: &FlowSnapshot,
|
||||
post_body: &FlowSnapshot,
|
||||
) {
|
||||
let mut used_places = FxHashSet::default();
|
||||
for loop_use in &loop_state.uses {
|
||||
used_places.insert(loop_use.place);
|
||||
}
|
||||
|
||||
let scope_id = self.current_scope();
|
||||
for place in loop_state.defined_places.iter() {
|
||||
if !used_places.contains(place) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pre_loop_binding_ids = pre_loop.binding_ids_for_place_excluding_unbound(*place);
|
||||
let seed_bindings = pre_loop.bindings_for_place(*place);
|
||||
let loop_bindings = self
|
||||
.current_use_def_map_mut()
|
||||
.merge_bindings(seed_bindings.clone(), post_body.bindings_for_place(*place));
|
||||
let mut seed_definitions = self
|
||||
.current_use_def_map()
|
||||
.definitions_for_place_in_snapshot(pre_loop, *place);
|
||||
let mut definitions = seed_definitions.clone();
|
||||
definitions.extend(
|
||||
self.current_use_def_map()
|
||||
.definitions_for_place_in_snapshot(post_body, *place),
|
||||
);
|
||||
definitions.sort();
|
||||
definitions.dedup();
|
||||
seed_definitions.sort();
|
||||
seed_definitions.dedup();
|
||||
|
||||
if definitions.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let header_definition = self.add_loop_header_definition(
|
||||
*place,
|
||||
loop_node,
|
||||
definitions.clone(),
|
||||
seed_definitions,
|
||||
loop_bindings,
|
||||
seed_bindings,
|
||||
);
|
||||
let header_definition_id = self.use_def_maps[scope_id]
|
||||
.register_definition_with_bindings(
|
||||
header_definition,
|
||||
pre_loop.bindings_for_place(*place),
|
||||
pre_loop.declarations_for_place(*place),
|
||||
);
|
||||
self.loop_header_definitions
|
||||
.insert(header_definition, definitions);
|
||||
|
||||
if pre_loop_binding_ids.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for loop_use in loop_state.uses.iter().filter(|use_| use_.place == *place) {
|
||||
self.current_use_def_map_mut().replace_use_bindings(
|
||||
loop_use.use_id,
|
||||
&pre_loop_binding_ids,
|
||||
header_definition_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
@@ -1924,41 +2079,66 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
|
||||
self.in_type_checking_block = is_outer_block_in_type_checking;
|
||||
}
|
||||
ast::Stmt::While(ast::StmtWhile {
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
range: _,
|
||||
node_index: _,
|
||||
}) => {
|
||||
ast::Stmt::While(stmt_while) => {
|
||||
let ast::StmtWhile {
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
range: _,
|
||||
node_index: _,
|
||||
} = stmt_while;
|
||||
self.current_scope_info_mut()
|
||||
.condition_place_uses
|
||||
.replace(FxHashSet::default());
|
||||
let outer_loop = self.push_loop();
|
||||
let has_outer_loop = outer_loop.is_some();
|
||||
self.visit_expr(test);
|
||||
let condition_place_uses = self
|
||||
.current_scope_info_mut()
|
||||
.condition_place_uses
|
||||
.take()
|
||||
.unwrap();
|
||||
|
||||
let pre_loop = self.flow_snapshot();
|
||||
let predicate = self.record_expression_narrowing_constraint(test);
|
||||
self.record_reachability_constraint(predicate);
|
||||
let predicate = self.build_predicate(test);
|
||||
let predicate_id = self.add_predicate(predicate);
|
||||
self.record_narrowing_constraint_id(predicate_id);
|
||||
self.record_reachability_constraint_id(predicate_id);
|
||||
|
||||
let outer_loop = self.push_loop();
|
||||
self.visit_body(body);
|
||||
let this_loop = self.pop_loop(outer_loop);
|
||||
let post_body = self.flow_snapshot();
|
||||
if !has_outer_loop {
|
||||
self.create_loop_header_definitions(
|
||||
stmt_while, &this_loop, &pre_loop, &post_body,
|
||||
);
|
||||
}
|
||||
|
||||
// We execute the `else` branch once the condition evaluates to false. This could
|
||||
// happen without ever executing the body, if the condition is false the first time
|
||||
// it's tested. Or it could happen if a _later_ evaluation of the condition yields
|
||||
// false. So we merge in the pre-loop state here into the post-body state:
|
||||
|
||||
self.flow_merge(pre_loop);
|
||||
self.flow_merge(pre_loop.clone());
|
||||
|
||||
// The `else` branch can only be reached if the loop condition *can* be false. To
|
||||
// model this correctly, we need a second copy of the while condition constraint,
|
||||
// since the first and later evaluations might produce different results. We would
|
||||
// otherwise simplify `predicate AND ~predicate` to `False`.
|
||||
let later_predicate_id = self.current_use_def_map_mut().add_predicate(predicate);
|
||||
let later_reachability_constraint = self
|
||||
.current_reachability_constraints_mut()
|
||||
.add_atom(later_predicate_id);
|
||||
self.record_negated_reachability_constraint(later_reachability_constraint);
|
||||
|
||||
self.record_negated_narrowing_constraint(predicate);
|
||||
let condition_depends_on_loop = condition_place_uses
|
||||
.iter()
|
||||
.any(|place| pre_loop.has_new_bindings_for_place(&post_body, *place));
|
||||
if condition_depends_on_loop {
|
||||
self.record_ambiguous_reachability();
|
||||
} else {
|
||||
let later_predicate_id =
|
||||
self.current_use_def_map_mut().add_predicate(predicate);
|
||||
let later_reachability_constraint = self
|
||||
.current_reachability_constraints_mut()
|
||||
.add_atom(later_predicate_id);
|
||||
self.record_negated_reachability_constraint(later_reachability_constraint);
|
||||
self.record_negated_narrowing_constraint(predicate);
|
||||
}
|
||||
|
||||
self.visit_body(orelse);
|
||||
|
||||
@@ -2469,12 +2649,7 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
let place_id = self.add_place(place_expr);
|
||||
|
||||
if is_use {
|
||||
if let ScopedPlaceId::Symbol(symbol_id) = place_id {
|
||||
self.mark_symbol_used(symbol_id);
|
||||
}
|
||||
let use_id = self.current_ast_ids().record_use(expr);
|
||||
self.current_use_def_map_mut()
|
||||
.record_use(place_id, use_id, node_key);
|
||||
self.record_use(place_id, expr.into());
|
||||
}
|
||||
|
||||
if is_definition {
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::node_key::NodeKey;
|
||||
use crate::semantic_index::place::ScopedPlaceId;
|
||||
use crate::semantic_index::scope::{FileScopeId, ScopeId};
|
||||
use crate::semantic_index::symbol::ScopedSymbolId;
|
||||
use crate::semantic_index::use_def::Bindings;
|
||||
use crate::unpack::{Unpack, UnpackPosition};
|
||||
|
||||
/// A definition of a place.
|
||||
@@ -753,6 +754,7 @@ pub enum DefinitionKind<'db> {
|
||||
TypeVar(AstNodeRef<ast::TypeParamTypeVar>),
|
||||
ParamSpec(AstNodeRef<ast::TypeParamParamSpec>),
|
||||
TypeVarTuple(AstNodeRef<ast::TypeParamTypeVarTuple>),
|
||||
LoopHeader(LoopHeaderDefinitionKind<'db>),
|
||||
}
|
||||
|
||||
impl DefinitionKind<'_> {
|
||||
@@ -835,6 +837,7 @@ impl DefinitionKind<'_> {
|
||||
DefinitionKind::TypeVarTuple(type_var_tuple) => {
|
||||
type_var_tuple.node(module).name.range()
|
||||
}
|
||||
DefinitionKind::LoopHeader(loop_header) => loop_header.node(module).range(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -880,6 +883,7 @@ impl DefinitionKind<'_> {
|
||||
DefinitionKind::TypeVar(type_var) => type_var.node(module).range(),
|
||||
DefinitionKind::ParamSpec(param_spec) => param_spec.node(module).range(),
|
||||
DefinitionKind::TypeVarTuple(type_var_tuple) => type_var_tuple.node(module).range(),
|
||||
DefinitionKind::LoopHeader(loop_header) => loop_header.node(module).range(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -935,19 +939,8 @@ impl DefinitionKind<'_> {
|
||||
| DefinitionKind::WithItem(_)
|
||||
| DefinitionKind::MatchPattern(_)
|
||||
| DefinitionKind::ImportFromSubmodule(_)
|
||||
| DefinitionKind::ExceptHandler(_) => DefinitionCategory::Binding,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the value expression for assignment-based definitions.
|
||||
///
|
||||
/// Returns `Some` for `Assignment` and `AnnotatedAssignment` (if it has a value),
|
||||
/// `None` for all other definition kinds.
|
||||
pub(crate) fn value<'ast>(&self, module: &'ast ParsedModuleRef) -> Option<&'ast ast::Expr> {
|
||||
match self {
|
||||
DefinitionKind::Assignment(assignment) => Some(assignment.value(module)),
|
||||
DefinitionKind::AnnotatedAssignment(assignment) => assignment.value(module),
|
||||
_ => None,
|
||||
| DefinitionKind::ExceptHandler(_)
|
||||
| DefinitionKind::LoopHeader(_) => DefinitionCategory::Binding,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1223,9 +1216,62 @@ impl ExceptHandlerDefinitionKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, get_size2::GetSize)]
|
||||
pub struct LoopHeaderDefinitionKind<'db> {
|
||||
node: AstNodeRef<ast::StmtWhile>,
|
||||
definitions: Vec<Definition<'db>>,
|
||||
seed_definitions: Vec<Definition<'db>>,
|
||||
bindings: Bindings,
|
||||
seed_bindings: Bindings,
|
||||
}
|
||||
|
||||
impl<'db> LoopHeaderDefinitionKind<'db> {
|
||||
pub(crate) fn new(
|
||||
node: AstNodeRef<ast::StmtWhile>,
|
||||
definitions: Vec<Definition<'db>>,
|
||||
seed_definitions: Vec<Definition<'db>>,
|
||||
bindings: Bindings,
|
||||
seed_bindings: Bindings,
|
||||
) -> Self {
|
||||
Self {
|
||||
node,
|
||||
definitions,
|
||||
seed_definitions,
|
||||
bindings,
|
||||
seed_bindings,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn node<'ast>(&self, module: &'ast ParsedModuleRef) -> &'ast ast::StmtWhile {
|
||||
self.node.node(module)
|
||||
}
|
||||
|
||||
pub(crate) fn definitions(&self) -> &[Definition<'db>] {
|
||||
&self.definitions
|
||||
}
|
||||
|
||||
pub(crate) fn seed_definitions(&self) -> &[Definition<'db>] {
|
||||
&self.seed_definitions
|
||||
}
|
||||
|
||||
pub(crate) fn bindings(&self) -> &Bindings {
|
||||
&self.bindings
|
||||
}
|
||||
|
||||
pub(crate) fn seed_bindings(&self) -> &Bindings {
|
||||
&self.seed_bindings
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, salsa::Update, get_size2::GetSize)]
|
||||
pub(crate) struct DefinitionNodeKey(NodeKey);
|
||||
|
||||
impl DefinitionNodeKey {
|
||||
pub(crate) fn from_node_key(node_key: NodeKey) -> Self {
|
||||
Self(node_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::Alias> for DefinitionNodeKey {
|
||||
fn from(node: &ast::Alias) -> Self {
|
||||
Self(NodeKey::from_node(node))
|
||||
@@ -1292,6 +1338,12 @@ impl From<&ast::StmtAugAssign> for DefinitionNodeKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::StmtWhile> for DefinitionNodeKey {
|
||||
fn from(node: &ast::StmtWhile) -> Self {
|
||||
Self(NodeKey::from_node(node))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::Parameter> for DefinitionNodeKey {
|
||||
fn from(node: &ast::Parameter) -> Self {
|
||||
Self(NodeKey::from_node(node))
|
||||
|
||||
@@ -111,6 +111,13 @@ impl NarrowingConstraintsBuilder {
|
||||
) -> ScopedNarrowingConstraint {
|
||||
self.lists.intersect(a, b)
|
||||
}
|
||||
|
||||
pub(crate) fn iter_predicates(
|
||||
&self,
|
||||
set: ScopedNarrowingConstraint,
|
||||
) -> NarrowingConstraintsIterator<'_> {
|
||||
self.lists.iter_set_reverse(set).copied()
|
||||
}
|
||||
}
|
||||
|
||||
// Iteration
|
||||
@@ -142,12 +149,5 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
impl NarrowingConstraintsBuilder {
|
||||
pub(crate) fn iter_predicates(
|
||||
&self,
|
||||
set: ScopedNarrowingConstraint,
|
||||
) -> NarrowingConstraintsIterator<'_> {
|
||||
self.lists.iter_set_reverse(set).copied()
|
||||
}
|
||||
}
|
||||
// Test-only impl removed; use the main impl above.
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::ops::Range;
|
||||
|
||||
use ruff_db::{files::File, parsed::ParsedModuleRef};
|
||||
use ruff_index::newtype_index;
|
||||
use ruff_python_ast::{self as ast, NodeIndex};
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::{
|
||||
Db,
|
||||
@@ -463,27 +463,6 @@ impl NodeWithScopeKind {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the anchor node index for this scope, or `None` for the module scope.
|
||||
///
|
||||
/// This is used to compute relative node indices for expressions within the scope,
|
||||
/// providing a stable anchor that only changes when the scope-introducing node changes.
|
||||
pub(crate) fn node_index(&self) -> Option<NodeIndex> {
|
||||
match self {
|
||||
Self::Module => None,
|
||||
Self::Class(class) => Some(class.index()),
|
||||
Self::ClassTypeParameters(class) => Some(class.index()),
|
||||
Self::Function(function) => Some(function.index()),
|
||||
Self::FunctionTypeParameters(function) => Some(function.index()),
|
||||
Self::TypeAlias(type_alias) => Some(type_alias.index()),
|
||||
Self::TypeAliasTypeParameters(type_alias) => Some(type_alias.index()),
|
||||
Self::Lambda(lambda) => Some(lambda.index()),
|
||||
Self::ListComprehension(comp) => Some(comp.index()),
|
||||
Self::SetComprehension(comp) => Some(comp.index()),
|
||||
Self::DictComprehension(comp) => Some(comp.index()),
|
||||
Self::GeneratorExpression(generator) => Some(generator.index()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
|
||||
@@ -261,8 +261,9 @@ use crate::semantic_index::reachability_constraints::{
|
||||
};
|
||||
use crate::semantic_index::scope::{FileScopeId, ScopeKind, ScopeLaziness};
|
||||
use crate::semantic_index::symbol::ScopedSymbolId;
|
||||
pub(crate) use crate::semantic_index::use_def::place_state::Bindings;
|
||||
use crate::semantic_index::use_def::place_state::{
|
||||
Bindings, Declarations, EnclosingSnapshot, LiveBindingsIterator, LiveDeclaration,
|
||||
Declarations, EnclosingSnapshot, LiveBindingsIterator, LiveDeclaration,
|
||||
LiveDeclarationsIterator, PlaceState, PreviousDefinitions, ScopedDefinitionId,
|
||||
};
|
||||
use crate::semantic_index::{EnclosingSnapshotResult, SemanticIndex};
|
||||
@@ -364,6 +365,13 @@ impl<'db> UseDefMap<'db> {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn bindings_from_snapshot<'a>(
|
||||
&'a self,
|
||||
bindings: &'a Bindings,
|
||||
) -> BindingWithConstraintsIterator<'a, 'db> {
|
||||
self.bindings_iterator(bindings, BoundnessAnalysis::BasedOnUnboundVisibility)
|
||||
}
|
||||
|
||||
pub(crate) fn applicable_constraints(
|
||||
&self,
|
||||
constraint_key: ConstraintKey,
|
||||
@@ -825,6 +833,52 @@ pub(super) struct FlowSnapshot {
|
||||
reachability: ScopedReachabilityConstraintId,
|
||||
}
|
||||
|
||||
impl FlowSnapshot {
|
||||
pub(super) fn has_new_bindings_for_place(
|
||||
&self,
|
||||
other: &FlowSnapshot,
|
||||
place: ScopedPlaceId,
|
||||
) -> bool {
|
||||
let (self_ids, other_ids) = match place {
|
||||
ScopedPlaceId::Symbol(symbol) => (
|
||||
self.symbol_states[symbol].bindings().binding_ids(),
|
||||
other.symbol_states[symbol].bindings().binding_ids(),
|
||||
),
|
||||
ScopedPlaceId::Member(member) => (
|
||||
self.member_states[member].bindings().binding_ids(),
|
||||
other.member_states[member].bindings().binding_ids(),
|
||||
),
|
||||
};
|
||||
other_ids.iter().any(|id| !self_ids.contains(id))
|
||||
}
|
||||
|
||||
pub(super) fn bindings_for_place(&self, place: ScopedPlaceId) -> Bindings {
|
||||
match place {
|
||||
ScopedPlaceId::Symbol(symbol) => self.symbol_states[symbol].bindings().clone(),
|
||||
ScopedPlaceId::Member(member) => self.member_states[member].bindings().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn binding_ids_for_place_excluding_unbound(
|
||||
&self,
|
||||
place: ScopedPlaceId,
|
||||
) -> Vec<ScopedDefinitionId> {
|
||||
let mut ids = match place {
|
||||
ScopedPlaceId::Symbol(symbol) => self.symbol_states[symbol].bindings().binding_ids(),
|
||||
ScopedPlaceId::Member(member) => self.member_states[member].bindings().binding_ids(),
|
||||
};
|
||||
ids.retain(|id| *id != ScopedDefinitionId::UNBOUND);
|
||||
ids
|
||||
}
|
||||
|
||||
pub(super) fn declarations_for_place(&self, place: ScopedPlaceId) -> Declarations {
|
||||
match place {
|
||||
ScopedPlaceId::Symbol(symbol) => self.symbol_states[symbol].declarations().clone(),
|
||||
ScopedPlaceId::Member(member) => self.member_states[member].declarations().clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A snapshot of the state of a single symbol (e.g. `obj`) and all of its associated members
|
||||
/// (e.g. `obj.attr`, `obj["key"]`).
|
||||
pub(super) struct SingleSymbolSnapshot {
|
||||
@@ -1248,6 +1302,107 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
self.record_node_reachability(node_key);
|
||||
}
|
||||
|
||||
pub(super) fn merge_use_with_snapshot(
|
||||
&mut self,
|
||||
use_id: ScopedUseId,
|
||||
snapshot: &FlowSnapshot,
|
||||
place: ScopedPlaceId,
|
||||
predicate: Option<ScopedPredicateId>,
|
||||
) {
|
||||
let mut bindings = self.bindings_by_use[use_id].clone();
|
||||
let mut backedge_bindings = snapshot.bindings_for_place(place);
|
||||
if let Some(predicate) = predicate {
|
||||
if predicate != ScopedPredicateId::ALWAYS_TRUE
|
||||
&& predicate != ScopedPredicateId::ALWAYS_FALSE
|
||||
{
|
||||
backedge_bindings
|
||||
.record_narrowing_constraint(&mut self.narrowing_constraints, predicate.into());
|
||||
}
|
||||
}
|
||||
bindings.merge(
|
||||
backedge_bindings,
|
||||
&mut self.narrowing_constraints,
|
||||
&mut self.reachability_constraints,
|
||||
);
|
||||
self.bindings_by_use[use_id] = bindings;
|
||||
}
|
||||
|
||||
pub(super) fn register_definition(
|
||||
&mut self,
|
||||
definition: Definition<'db>,
|
||||
) -> ScopedDefinitionId {
|
||||
self.all_definitions
|
||||
.push(DefinitionState::Defined(definition))
|
||||
}
|
||||
|
||||
pub(super) fn register_definition_with_bindings(
|
||||
&mut self,
|
||||
definition: Definition<'db>,
|
||||
bindings: Bindings,
|
||||
declarations: Declarations,
|
||||
) -> ScopedDefinitionId {
|
||||
let def_id = self
|
||||
.all_definitions
|
||||
.push(DefinitionState::Defined(definition));
|
||||
self.bindings_by_definition.insert(definition, bindings);
|
||||
self.declarations_by_binding
|
||||
.insert(definition, declarations);
|
||||
def_id
|
||||
}
|
||||
|
||||
pub(super) fn replace_use_with_definition(
|
||||
&mut self,
|
||||
use_id: ScopedUseId,
|
||||
definition_id: ScopedDefinitionId,
|
||||
) {
|
||||
self.bindings_by_use[use_id].replace_definition(definition_id);
|
||||
}
|
||||
|
||||
pub(super) fn replace_use_bindings(
|
||||
&mut self,
|
||||
use_id: ScopedUseId,
|
||||
from: &[ScopedDefinitionId],
|
||||
definition_id: ScopedDefinitionId,
|
||||
) {
|
||||
self.bindings_by_use[use_id].replace_definitions(
|
||||
from,
|
||||
definition_id,
|
||||
&mut self.narrowing_constraints,
|
||||
&mut self.reachability_constraints,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn merge_bindings(&mut self, mut bindings: Bindings, other: Bindings) -> Bindings {
|
||||
bindings.merge(
|
||||
other,
|
||||
&mut self.narrowing_constraints,
|
||||
&mut self.reachability_constraints,
|
||||
);
|
||||
bindings
|
||||
}
|
||||
|
||||
pub(super) fn definitions_for_place_in_snapshot(
|
||||
&self,
|
||||
snapshot: &FlowSnapshot,
|
||||
place: ScopedPlaceId,
|
||||
) -> Vec<Definition<'db>> {
|
||||
let binding_ids = match place {
|
||||
ScopedPlaceId::Symbol(symbol) => {
|
||||
snapshot.symbol_states[symbol].bindings().binding_ids()
|
||||
}
|
||||
ScopedPlaceId::Member(member) => {
|
||||
snapshot.member_states[member].bindings().binding_ids()
|
||||
}
|
||||
};
|
||||
binding_ids
|
||||
.into_iter()
|
||||
.filter_map(|binding_id| match self.all_definitions[binding_id] {
|
||||
DefinitionState::Defined(definition) => Some(definition),
|
||||
DefinitionState::Undefined | DefinitionState::Deleted => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn record_node_reachability(&mut self, node_key: NodeKey) {
|
||||
self.node_reachability.insert(node_key, self.reachability);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ use crate::semantic_index::reachability_constraints::{
|
||||
/// A newtype-index for a definition in a particular scope.
|
||||
#[newtype_index]
|
||||
#[derive(Ord, PartialOrd, get_size2::GetSize)]
|
||||
pub(super) struct ScopedDefinitionId;
|
||||
pub(crate) struct ScopedDefinitionId;
|
||||
|
||||
impl ScopedDefinitionId {
|
||||
/// A special ID that is used to describe an implicit start-of-scope state. When
|
||||
@@ -74,7 +74,7 @@ impl ScopedDefinitionId {
|
||||
/// Live declarations for a single place at some point in control flow, with their
|
||||
/// corresponding reachability constraints.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
pub(super) struct Declarations {
|
||||
pub(crate) struct Declarations {
|
||||
/// A list of live declarations for this place, sorted by their `ScopedDefinitionId`
|
||||
live_declarations: SmallVec<[LiveDeclaration; 2]>,
|
||||
}
|
||||
@@ -206,7 +206,7 @@ impl EnclosingSnapshot {
|
||||
/// Live bindings for a single place at some point in control flow. Each live binding comes
|
||||
/// with a set of narrowing constraints and a reachability constraint.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
pub(super) struct Bindings {
|
||||
pub(crate) struct Bindings {
|
||||
/// The narrowing constraint applicable to the "unbound" binding, if we need access to it even
|
||||
/// when it's not visible. This happens in class scopes, where local name bindings are not visible
|
||||
/// to nested scopes, but we still need to know what narrowing constraints were applied to the
|
||||
@@ -222,12 +222,109 @@ impl Bindings {
|
||||
.unwrap_or(self.live_bindings[0].narrowing_constraint)
|
||||
}
|
||||
|
||||
pub(super) fn has_defined_bindings(&self) -> bool {
|
||||
self.live_bindings
|
||||
.iter()
|
||||
.any(|binding| !binding.binding.is_unbound())
|
||||
}
|
||||
|
||||
pub(super) fn from_single(
|
||||
binding: ScopedDefinitionId,
|
||||
narrowing_constraint: ScopedNarrowingConstraint,
|
||||
reachability_constraint: ScopedReachabilityConstraintId,
|
||||
) -> Self {
|
||||
Self {
|
||||
unbound_narrowing_constraint: None,
|
||||
live_bindings: smallvec![LiveBinding {
|
||||
binding,
|
||||
narrowing_constraint,
|
||||
reachability_constraint,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn representative_narrowing_constraint(&self) -> ScopedNarrowingConstraint {
|
||||
self.live_bindings
|
||||
.first()
|
||||
.map(|binding| binding.narrowing_constraint)
|
||||
.unwrap_or_else(ScopedNarrowingConstraint::empty)
|
||||
}
|
||||
|
||||
pub(super) fn retain_bindings(
|
||||
&mut self,
|
||||
mut predicate: impl FnMut(ScopedDefinitionId) -> bool,
|
||||
) {
|
||||
self.live_bindings
|
||||
.retain(|binding| predicate(binding.binding));
|
||||
}
|
||||
|
||||
pub(super) fn binding_ids(&self) -> Vec<ScopedDefinitionId> {
|
||||
self.live_bindings
|
||||
.iter()
|
||||
.map(|binding| binding.binding)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn finish(&mut self, reachability_constraints: &mut ReachabilityConstraintsBuilder) {
|
||||
self.live_bindings.shrink_to_fit();
|
||||
for binding in &self.live_bindings {
|
||||
reachability_constraints.mark_used(binding.reachability_constraint);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn replace_definition(&mut self, binding: ScopedDefinitionId) {
|
||||
for live_binding in &mut self.live_bindings {
|
||||
live_binding.binding = binding;
|
||||
}
|
||||
self.live_bindings
|
||||
.sort_by(|left, right| left.binding.cmp(&right.binding));
|
||||
}
|
||||
|
||||
pub(super) fn replace_definitions(
|
||||
&mut self,
|
||||
from: &[ScopedDefinitionId],
|
||||
replacement: ScopedDefinitionId,
|
||||
narrowing_constraints: &mut NarrowingConstraintsBuilder,
|
||||
reachability_constraints: &mut ReachabilityConstraintsBuilder,
|
||||
) {
|
||||
if from.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut changed = false;
|
||||
for live_binding in &mut self.live_bindings {
|
||||
if from.contains(&live_binding.binding) {
|
||||
live_binding.binding = replacement;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !changed {
|
||||
return;
|
||||
}
|
||||
|
||||
self.live_bindings
|
||||
.sort_by(|left, right| left.binding.cmp(&right.binding));
|
||||
|
||||
let mut merged: SmallVec<[LiveBinding; 2]> = SmallVec::new();
|
||||
for binding in std::mem::take(&mut self.live_bindings) {
|
||||
match merged.last_mut() {
|
||||
Some(last) if last.binding == binding.binding => {
|
||||
last.narrowing_constraint = narrowing_constraints.intersect_constraints(
|
||||
last.narrowing_constraint,
|
||||
binding.narrowing_constraint,
|
||||
);
|
||||
last.reachability_constraint = reachability_constraints.add_or_constraint(
|
||||
last.reachability_constraint,
|
||||
binding.reachability_constraint,
|
||||
);
|
||||
}
|
||||
_ => merged.push(binding),
|
||||
}
|
||||
}
|
||||
|
||||
self.live_bindings = merged;
|
||||
}
|
||||
}
|
||||
|
||||
/// One of the live bindings for a single place at some point in control flow.
|
||||
@@ -291,6 +388,26 @@ impl Bindings {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn add_narrowing_constraint_from(
|
||||
&mut self,
|
||||
narrowing_constraints: &mut NarrowingConstraintsBuilder,
|
||||
constraint: ScopedNarrowingConstraint,
|
||||
) {
|
||||
let predicates: Vec<_> = narrowing_constraints.iter_predicates(constraint).collect();
|
||||
let mut unbound_constraint = self.unbound_narrowing_constraint;
|
||||
for predicate in predicates {
|
||||
if let Some(existing) = unbound_constraint {
|
||||
unbound_constraint =
|
||||
Some(narrowing_constraints.add_predicate_to_constraint(existing, predicate));
|
||||
}
|
||||
for binding in &mut self.live_bindings {
|
||||
binding.narrowing_constraint = narrowing_constraints
|
||||
.add_predicate_to_constraint(binding.narrowing_constraint, predicate);
|
||||
}
|
||||
}
|
||||
self.unbound_narrowing_constraint = unbound_constraint;
|
||||
}
|
||||
|
||||
/// Add given reachability constraint to all live bindings.
|
||||
pub(super) fn record_reachability_constraint(
|
||||
&mut self,
|
||||
|
||||
@@ -18,6 +18,8 @@ use ruff_db::parsed::parsed_module;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use rustc_hash::FxHashMap;
|
||||
use salsa::plumbing::{AsId, Id};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use ty_module_resolver::{KnownModule, Module, ModuleName, resolve_module};
|
||||
|
||||
@@ -64,7 +66,7 @@ use crate::types::generics::{
|
||||
ApplySpecialization, InferableTypeVars, Specialization, SpecializationBuilder, bind_typevar,
|
||||
typing_self, walk_generic_context,
|
||||
};
|
||||
use crate::types::mro::{Mro, MroIterator, StaticMroError};
|
||||
use crate::types::mro::{Mro, MroError, MroIterator};
|
||||
pub(crate) use crate::types::narrow::{NarrowingConstraint, infer_narrowing_constraint};
|
||||
use crate::types::newtype::NewType;
|
||||
pub(crate) use crate::types::signatures::{Parameter, Parameters};
|
||||
@@ -158,8 +160,59 @@ pub fn check_types(db: &dyn Db, file: File) -> Vec<Diagnostic> {
|
||||
diagnostics
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static LOOP_HEADER_OVERRIDE: RefCell<FxHashMap<Id, Type<'static>>> =
|
||||
RefCell::new(FxHashMap::default());
|
||||
}
|
||||
|
||||
fn loop_header_override<'db>(definition: Definition<'db>) -> Option<Type<'db>> {
|
||||
let id = definition.as_id();
|
||||
LOOP_HEADER_OVERRIDE.with(|cell| cell.borrow().get(&id).copied().map(restore_type_lifetime))
|
||||
}
|
||||
|
||||
pub(crate) fn with_loop_header_override<'db, R>(
|
||||
definition: Definition<'db>,
|
||||
ty: Type<'db>,
|
||||
f: impl FnOnce() -> R,
|
||||
) -> R {
|
||||
let id = definition.as_id();
|
||||
LOOP_HEADER_OVERRIDE.with(|cell| {
|
||||
let mut overrides = cell.borrow_mut();
|
||||
let previous = overrides.insert(id, erase_type_lifetime(ty));
|
||||
drop(overrides);
|
||||
|
||||
let result = f();
|
||||
|
||||
let mut overrides = cell.borrow_mut();
|
||||
match previous {
|
||||
Some(previous) => {
|
||||
overrides.insert(id, previous);
|
||||
}
|
||||
None => {
|
||||
overrides.remove(&id);
|
||||
}
|
||||
}
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
fn erase_type_lifetime<'db>(ty: Type<'db>) -> Type<'static> {
|
||||
// SAFETY: `Type` is a copyable, db-backed handle; we only use this within
|
||||
// a single thread to provide a temporary loop-header override.
|
||||
unsafe { std::mem::transmute::<Type<'db>, Type<'static>>(ty) }
|
||||
}
|
||||
|
||||
fn restore_type_lifetime<'db>(ty: Type<'static>) -> Type<'db> {
|
||||
// SAFETY: This is the inverse of `erase_type_lifetime` and is only used
|
||||
// within the dynamic scope of a loop-header override.
|
||||
unsafe { std::mem::transmute::<Type<'static>, Type<'db>>(ty) }
|
||||
}
|
||||
|
||||
/// Infer the type of a binding.
|
||||
pub(crate) fn binding_type<'db>(db: &'db dyn Db, definition: Definition<'db>) -> Type<'db> {
|
||||
if let Some(override_type) = loop_header_override(definition) {
|
||||
return override_type;
|
||||
}
|
||||
let inference = infer_definition_types(db, definition);
|
||||
inference.binding_type(definition)
|
||||
}
|
||||
@@ -2645,15 +2698,8 @@ impl<'db> Type<'db> {
|
||||
return if instance.is_none(db) && callable.is_function_like(db) {
|
||||
Some((self, AttributeKind::NormalOrNonDataDescriptor))
|
||||
} else {
|
||||
// For classmethod-like callables, bind to the owner class. For function-like callables, bind to the instance.
|
||||
let self_type = if callable.is_classmethod_like(db) && instance.is_none(db) {
|
||||
owner.to_instance(db).unwrap_or(owner)
|
||||
} else {
|
||||
instance
|
||||
};
|
||||
|
||||
Some((
|
||||
Type::Callable(callable.bind_self(db, Some(self_type))),
|
||||
Type::Callable(callable.bind_self(db, Some(instance))),
|
||||
AttributeKind::NormalOrNonDataDescriptor,
|
||||
))
|
||||
};
|
||||
@@ -6536,9 +6582,9 @@ impl<'db> Type<'db> {
|
||||
Some(TypeDefinition::Function(function.definition(db)))
|
||||
}
|
||||
Self::ModuleLiteral(module) => Some(TypeDefinition::Module(module.module(db))),
|
||||
Self::ClassLiteral(class_literal) => class_literal.type_definition(db),
|
||||
Self::ClassLiteral(class_literal) => Some(class_literal.type_definition(db)),
|
||||
Self::GenericAlias(alias) => Some(TypeDefinition::StaticClass(alias.definition(db))),
|
||||
Self::NominalInstance(instance) => instance.class(db).type_definition(db),
|
||||
Self::NominalInstance(instance) => Some(instance.class(db).type_definition(db)),
|
||||
Self::KnownInstance(instance) => match instance {
|
||||
KnownInstanceType::TypeVar(var) => {
|
||||
Some(TypeDefinition::TypeVar(var.definition(db)?))
|
||||
@@ -6552,7 +6598,7 @@ impl<'db> Type<'db> {
|
||||
|
||||
Self::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
|
||||
SubclassOfInner::Dynamic(_) => None,
|
||||
SubclassOfInner::Class(class) => class.type_definition(db),
|
||||
SubclassOfInner::Class(class) => Some(class.type_definition(db)),
|
||||
SubclassOfInner::TypeVar(bound_typevar) => {
|
||||
Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?))
|
||||
}
|
||||
@@ -6582,7 +6628,7 @@ impl<'db> Type<'db> {
|
||||
Self::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?)),
|
||||
|
||||
Self::ProtocolInstance(protocol) => match protocol.inner {
|
||||
Protocol::FromClass(class) => class.type_definition(db),
|
||||
Protocol::FromClass(class) => Some(class.type_definition(db)),
|
||||
Protocol::Synthesized(_) => None,
|
||||
},
|
||||
|
||||
@@ -11799,7 +11845,9 @@ impl<'db> UnionType<'db> {
|
||||
elements
|
||||
.into_iter()
|
||||
.fold(
|
||||
UnionBuilder::new(db).cycle_recovery(true),
|
||||
UnionBuilder::new(db)
|
||||
.cycle_recovery(true)
|
||||
.recursively_defined(RecursivelyDefined::Yes),
|
||||
|builder, element| builder.add(element.into()),
|
||||
)
|
||||
.build()
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::sync::{LazyLock, Mutex};
|
||||
|
||||
use super::TypeVarVariance;
|
||||
use super::{
|
||||
BoundTypeVarInstance, MemberLookupPolicy, Mro, MroIterator, SpecialFormType, StaticMroError,
|
||||
BoundTypeVarInstance, MemberLookupPolicy, Mro, MroError, MroIterator, SpecialFormType,
|
||||
SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase,
|
||||
function::FunctionType,
|
||||
};
|
||||
@@ -60,7 +60,7 @@ use crate::{
|
||||
attribute_assignments,
|
||||
definition::{DefinitionKind, TargetKind},
|
||||
place_table,
|
||||
scope::ScopeId,
|
||||
scope::{FileScopeId, ScopeId},
|
||||
semantic_index, use_def_map,
|
||||
},
|
||||
types::{
|
||||
@@ -74,7 +74,7 @@ use ruff_db::diagnostic::Span;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::{self as ast, NodeIndex, PythonVersion};
|
||||
use ruff_python_ast::{self as ast, PythonVersion};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use rustc_hash::FxHashSet;
|
||||
use ty_module_resolver::{KnownModule, file_to_module};
|
||||
@@ -128,8 +128,8 @@ fn try_mro_cycle_initial<'db>(
|
||||
_id: salsa::Id,
|
||||
self_: StaticClassLiteral<'db>,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> Result<Mro<'db>, StaticMroError<'db>> {
|
||||
Err(StaticMroError::cycle(
|
||||
) -> Result<Mro<'db>, MroError<'db>> {
|
||||
Err(MroError::cycle(
|
||||
db,
|
||||
self_.apply_optional_specialization(db, specialization),
|
||||
))
|
||||
@@ -613,7 +613,7 @@ impl<'db> ClassLiteral<'db> {
|
||||
pub(crate) fn file(self, db: &dyn Db) -> File {
|
||||
match self {
|
||||
Self::Static(class) => class.file(db),
|
||||
Self::Dynamic(class) => class.scope(db).file(db),
|
||||
Self::Dynamic(class) => class.file(db),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,17 +643,13 @@ impl<'db> ClassLiteral<'db> {
|
||||
/// Returns `true` if this class defines any ordering method (`__lt__`, `__le__`, `__gt__`,
|
||||
/// `__ge__`) in its own body (not inherited). Used by `@total_ordering` to determine if
|
||||
/// synthesis is valid.
|
||||
///
|
||||
/// For dynamic classes, this checks if any ordering methods are provided in the namespace
|
||||
/// dictionary:
|
||||
/// ```python
|
||||
/// X = type("X", (), {"__lt__": lambda self, other: True})
|
||||
/// ```
|
||||
// TODO: A dynamic class could provide ordering methods in the namespace dictionary:
|
||||
// ```python
|
||||
// >>> X = type("X", (), {"__lt__": lambda self, other: True})
|
||||
// ```
|
||||
pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
Self::Static(class) => class.has_own_ordering_method(db),
|
||||
Self::Dynamic(class) => class.has_own_ordering_method(db),
|
||||
}
|
||||
self.as_static()
|
||||
.is_some_and(|class| class.has_own_ordering_method(db))
|
||||
}
|
||||
|
||||
/// Returns the static class definition if this is one.
|
||||
@@ -672,10 +668,10 @@ impl<'db> ClassLiteral<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the definition of this class, if available.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
|
||||
/// Returns the definition of this class.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
|
||||
match self {
|
||||
Self::Static(class) => Some(class.definition(db)),
|
||||
Self::Static(class) => class.definition(db),
|
||||
Self::Dynamic(class) => class.definition(db),
|
||||
}
|
||||
}
|
||||
@@ -683,11 +679,11 @@ impl<'db> ClassLiteral<'db> {
|
||||
/// Returns the type definition for this class.
|
||||
///
|
||||
/// For static classes, returns `TypeDefinition::StaticClass`.
|
||||
/// For dynamic classes, returns `TypeDefinition::DynamicClass` if a definition is available.
|
||||
pub(crate) fn type_definition(self, db: &'db dyn Db) -> Option<TypeDefinition<'db>> {
|
||||
/// For dynamic classes, returns `TypeDefinition::DynamicClass`.
|
||||
pub(crate) fn type_definition(self, db: &'db dyn Db) -> TypeDefinition<'db> {
|
||||
match self {
|
||||
Self::Static(class) => Some(TypeDefinition::StaticClass(class.definition(db))),
|
||||
Self::Dynamic(class) => class.definition(db).map(TypeDefinition::DynamicClass),
|
||||
Self::Static(class) => TypeDefinition::StaticClass(class.definition(db)),
|
||||
Self::Dynamic(class) => TypeDefinition::DynamicClass(class.definition(db)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,27 +704,20 @@ impl<'db> ClassLiteral<'db> {
|
||||
}
|
||||
|
||||
/// Returns whether this class is a disjoint base.
|
||||
///
|
||||
/// A class is considered a disjoint base if:
|
||||
/// - It has the `@disjoint_base` decorator (static classes only), or
|
||||
/// - It defines non-empty `__slots__`
|
||||
///
|
||||
/// For dynamic classes created via `type()`, we check if `__slots__` is provided
|
||||
/// in the namespace dictionary:
|
||||
/// ```python
|
||||
/// >>> X = type("X", (), {"__slots__": ("a",)})
|
||||
/// >>> class Foo(int, X): ...
|
||||
/// ...
|
||||
/// Traceback (most recent call last):
|
||||
/// File "<python-input-4>", line 1, in <module>
|
||||
/// class Foo(int, X): ...
|
||||
/// TypeError: multiple bases have instance lay-out conflict
|
||||
/// ```
|
||||
// TODO: A dynamic class could provide __slots__ in the namespace dictionary, which would make
|
||||
// it a disjoint base:
|
||||
// ```python
|
||||
// >>> X = type("X", (), {"__slots__": ("a",)})
|
||||
// >>> class Foo(int, X): ...
|
||||
// ...
|
||||
// Traceback (most recent call last):
|
||||
// File "<python-input-4>", line 1, in <module>
|
||||
// class Foo(int, X): ...
|
||||
// TypeError: multiple bases have instance lay-out conflict
|
||||
// ```
|
||||
pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option<DisjointBase<'db>> {
|
||||
match self {
|
||||
Self::Static(class) => class.as_disjoint_base(db),
|
||||
Self::Dynamic(class) => class.as_disjoint_base(db),
|
||||
}
|
||||
self.as_static()
|
||||
.and_then(|class| class.as_disjoint_base(db))
|
||||
}
|
||||
|
||||
/// Returns a non-generic instance of this class.
|
||||
@@ -952,13 +941,13 @@ impl<'db> ClassType<'db> {
|
||||
self.class_literal(db).known(db)
|
||||
}
|
||||
|
||||
/// Returns the definition for this class, if available.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
|
||||
/// Returns the definition for this class.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
|
||||
self.class_literal(db).definition(db)
|
||||
}
|
||||
|
||||
/// Returns the type definition for this class.
|
||||
pub(crate) fn type_definition(self, db: &'db dyn Db) -> Option<TypeDefinition<'db>> {
|
||||
pub(crate) fn type_definition(self, db: &'db dyn Db) -> TypeDefinition<'db> {
|
||||
self.class_literal(db).type_definition(db)
|
||||
}
|
||||
|
||||
@@ -1334,12 +1323,8 @@ impl<'db> ClassType<'db> {
|
||||
Signature::new(parameters, return_annotation)
|
||||
}
|
||||
|
||||
let (class_literal, specialization) = match self {
|
||||
Self::NonGeneric(ClassLiteral::Dynamic(dynamic)) => {
|
||||
return dynamic.own_class_member(db, name);
|
||||
}
|
||||
Self::NonGeneric(ClassLiteral::Static(class)) => (class, None),
|
||||
Self::Generic(generic) => (generic.origin(db), Some(generic.specialization(db))),
|
||||
let Some((class_literal, specialization)) = self.static_class_literal(db) else {
|
||||
return Member::unbound();
|
||||
};
|
||||
|
||||
let fallback_member_lookup = || {
|
||||
@@ -1652,21 +1637,12 @@ impl<'db> ClassType<'db> {
|
||||
/// A helper function for `instance_member` that looks up the `name` attribute only on
|
||||
/// this class, not on its superclasses.
|
||||
pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> {
|
||||
match self {
|
||||
Self::NonGeneric(ClassLiteral::Dynamic(dynamic)) => {
|
||||
dynamic.own_instance_member(db, name)
|
||||
}
|
||||
Self::NonGeneric(ClassLiteral::Static(class_literal)) => {
|
||||
class_literal.own_instance_member(db, name)
|
||||
}
|
||||
Self::Generic(generic) => {
|
||||
let specialization = generic.specialization(db);
|
||||
generic
|
||||
.origin(db)
|
||||
.own_instance_member(db, name)
|
||||
.map_type(|ty| ty.apply_optional_specialization(db, Some(specialization)))
|
||||
}
|
||||
}
|
||||
let Some((class_literal, specialization)) = self.static_class_literal(db) else {
|
||||
return Member::unbound();
|
||||
};
|
||||
class_literal
|
||||
.own_instance_member(db, name)
|
||||
.map_type(|ty| ty.apply_optional_specialization(db, specialization))
|
||||
}
|
||||
|
||||
/// Return a callable type (or union of callable types) that represents the callable
|
||||
@@ -2135,27 +2111,16 @@ impl<'db> StaticClassLiteral<'db> {
|
||||
let Some(base_class) = base.into_class() else {
|
||||
continue;
|
||||
};
|
||||
match base_class.class_literal(db) {
|
||||
ClassLiteral::Static(base_literal) => {
|
||||
if base_literal.is_known(db, KnownClass::Object) {
|
||||
continue;
|
||||
}
|
||||
let member = class_member(db, base_literal.body_scope(db), name);
|
||||
if let Some(ty) = member.ignore_possibly_undefined() {
|
||||
let base_specialization = base_class
|
||||
.static_class_literal(db)
|
||||
.and_then(|(_, spec)| spec);
|
||||
return Some(ty.apply_optional_specialization(db, base_specialization));
|
||||
}
|
||||
}
|
||||
ClassLiteral::Dynamic(dynamic) => {
|
||||
// Dynamic classes (created with `type()`) can also define ordering methods
|
||||
// in their namespace dict.
|
||||
let member = dynamic.own_class_member(db, name);
|
||||
if let Some(ty) = member.ignore_possibly_undefined() {
|
||||
return Some(ty);
|
||||
}
|
||||
}
|
||||
let Some((base_literal, base_specialization)) = base_class.static_class_literal(db)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if base_literal.is_known(db, KnownClass::Object) {
|
||||
continue;
|
||||
}
|
||||
let member = class_member(db, base_literal.body_scope(db), name);
|
||||
if let Some(ty) = member.ignore_possibly_undefined() {
|
||||
return Some(ty.apply_optional_specialization(db, base_specialization));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2411,9 +2376,7 @@ impl<'db> StaticClassLiteral<'db> {
|
||||
{
|
||||
Some(DisjointBase::due_to_decorator(self))
|
||||
} else if SlotsKind::from(db, self) == SlotsKind::NotEmpty {
|
||||
Some(DisjointBase::due_to_dunder_slots(ClassLiteral::Static(
|
||||
self,
|
||||
)))
|
||||
Some(DisjointBase::due_to_dunder_slots(self))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -2535,7 +2498,7 @@ impl<'db> StaticClassLiteral<'db> {
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> Result<Mro<'db>, StaticMroError<'db>> {
|
||||
) -> Result<Mro<'db>, MroError<'db>> {
|
||||
tracing::trace!("StaticClassLiteral::try_mro: {}", self.name(db));
|
||||
Mro::of_static_class(db, self, specialization)
|
||||
}
|
||||
@@ -2642,7 +2605,7 @@ impl<'db> StaticClassLiteral<'db> {
|
||||
return Ok((SubclassOfType::subclass_of_unknown(), None));
|
||||
}
|
||||
|
||||
if self.try_mro(db, None).is_err_and(StaticMroError::is_cycle) {
|
||||
if self.try_mro(db, None).is_err_and(MroError::is_cycle) {
|
||||
return Ok((SubclassOfType::subclass_of_unknown(), None));
|
||||
}
|
||||
|
||||
@@ -4702,13 +4665,27 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
|
||||
/// - name: "Foo"
|
||||
/// - bases: [Base]
|
||||
///
|
||||
/// This is called "dynamic" because the class is created dynamically at runtime
|
||||
/// via a function call rather than a class statement.
|
||||
/// # Limitations
|
||||
///
|
||||
/// TODO: Attributes from the namespace dict (third argument to `type()`) are not tracked.
|
||||
/// This matches Pyright's behavior. For example:
|
||||
///
|
||||
/// ```python
|
||||
/// Foo = type("Foo", (), {"attr": 42})
|
||||
/// Foo().attr # Error: no attribute 'attr'
|
||||
/// ```
|
||||
///
|
||||
/// Supporting namespace dict attributes would require parsing dict literals and tracking
|
||||
/// the attribute types, similar to how TypedDict handles its fields.
|
||||
///
|
||||
/// # Salsa interning
|
||||
///
|
||||
/// This is a salsa interned struct. Two different `type()` calls always produce
|
||||
/// distinct `DynamicClassLiteral` instances, even if they have the same name and bases:
|
||||
/// Each `type()` call is uniquely identified by its [`Definition`], which provides
|
||||
/// stable identity without depending on AST node indices that can change when code
|
||||
/// is inserted above the call site.
|
||||
///
|
||||
/// Two different `type()` calls always produce distinct `DynamicClassLiteral`
|
||||
/// instances, even if they have the same name and bases:
|
||||
///
|
||||
/// ```python
|
||||
/// Foo1 = type("Foo", (Base,), {})
|
||||
@@ -4716,11 +4693,9 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
|
||||
/// # Foo1 and Foo2 are distinct types
|
||||
/// ```
|
||||
///
|
||||
/// The `anchor` field provides stable identity:
|
||||
/// - For assigned `type()` calls, the `Definition` uniquely identifies the class.
|
||||
/// - For dangling `type()` calls, a relative node offset anchored to the enclosing scope
|
||||
/// provides stable identity that only changes when the scope itself changes.
|
||||
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
|
||||
/// Note: Only assigned `type()` calls are currently supported (e.g., `Foo = type(...)`).
|
||||
/// Inline calls like `process(type(...))` fall back to normal call handling.
|
||||
#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)]
|
||||
#[derive(PartialOrd, Ord)]
|
||||
pub struct DynamicClassLiteral<'db> {
|
||||
/// The name of the class (from the first argument to `type()`).
|
||||
@@ -4731,105 +4706,46 @@ pub struct DynamicClassLiteral<'db> {
|
||||
#[returns(deref)]
|
||||
pub bases: Box<[ClassBase<'db>]>,
|
||||
|
||||
/// The scope containing the `type()` call.
|
||||
pub scope: ScopeId<'db>,
|
||||
|
||||
/// The anchor for this dynamic class, providing stable identity.
|
||||
///
|
||||
/// - `Definition`: The `type()` call is assigned to a variable. The definition
|
||||
/// uniquely identifies this class and can be used to find the `type()` call.
|
||||
/// - `ScopeOffset`: The `type()` call is "dangling" (not assigned). The offset
|
||||
/// is relative to the enclosing scope's anchor node index.
|
||||
pub anchor: DynamicClassAnchor<'db>,
|
||||
|
||||
/// The class members from the namespace dict (third argument to `type()`).
|
||||
/// Each entry is a (name, type) pair extracted from the dict literal.
|
||||
#[returns(deref)]
|
||||
pub members: Box<[(Name, Type<'db>)]>,
|
||||
|
||||
/// Whether the namespace dict (third argument) is dynamic (not a literal dict,
|
||||
/// or contains non-string-literal keys). When true, attribute lookups on this
|
||||
/// class and its instances return `Unknown` instead of failing.
|
||||
pub has_dynamic_namespace: bool,
|
||||
/// The definition where this class is created.
|
||||
pub definition: Definition<'db>,
|
||||
|
||||
/// Dataclass parameters if this class has been wrapped with `@dataclass` decorator
|
||||
/// or passed to `dataclass()` as a function.
|
||||
pub dataclass_params: Option<DataclassParams<'db>>,
|
||||
}
|
||||
|
||||
/// Anchor for identifying a dynamic class literal.
|
||||
///
|
||||
/// This enum provides stable identity for `DynamicClassLiteral`:
|
||||
/// - For assigned calls, the `Definition` uniquely identifies the class.
|
||||
/// - For dangling calls, a relative offset provides stable identity.
|
||||
#[derive(
|
||||
Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, salsa::Update, get_size2::GetSize,
|
||||
)]
|
||||
pub enum DynamicClassAnchor<'db> {
|
||||
/// The `type()` call is assigned to a variable.
|
||||
///
|
||||
/// The `Definition` uniquely identifies this class. The `type()` call expression
|
||||
/// is the `value` of the assignment, so we can get its range from the definition.
|
||||
Definition(Definition<'db>),
|
||||
|
||||
/// The `type()` call is "dangling" (not assigned to a variable).
|
||||
///
|
||||
/// The offset is relative to the enclosing scope's anchor node index.
|
||||
/// For module scope, this is equivalent to an absolute index (anchor is 0).
|
||||
ScopeOffset(u32),
|
||||
}
|
||||
|
||||
impl get_size2::GetSize for DynamicClassLiteral<'_> {}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl<'db> DynamicClassLiteral<'db> {
|
||||
/// Returns the definition where this class is created, if it was assigned to a variable.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
|
||||
match self.anchor(db) {
|
||||
DynamicClassAnchor::Definition(definition) => Some(definition),
|
||||
DynamicClassAnchor::ScopeOffset(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a [`Span`] with the range of the `type()` call expression.
|
||||
///
|
||||
/// See [`Self::header_range`] for more details.
|
||||
pub(super) fn header_span(self, db: &'db dyn Db) -> Span {
|
||||
Span::from(self.scope(db).file(db)).with_range(self.header_range(db))
|
||||
Span::from(self.file(db)).with_range(self.header_range(db))
|
||||
}
|
||||
|
||||
/// Returns the range of the `type()` call expression that created this class.
|
||||
pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange {
|
||||
let scope = self.scope(db);
|
||||
let file = scope.file(db);
|
||||
let definition = self.definition(db);
|
||||
let file = definition.file(db);
|
||||
let module = parsed_module(db, file).load(db);
|
||||
|
||||
match self.anchor(db) {
|
||||
DynamicClassAnchor::Definition(definition) => {
|
||||
// For definitions, get the range from the definition's value.
|
||||
// The `type()` call is the value of the assignment.
|
||||
definition
|
||||
.kind(db)
|
||||
.value(&module)
|
||||
.expect("DynamicClassAnchor::Definition should only be used for assignments")
|
||||
.range()
|
||||
}
|
||||
DynamicClassAnchor::ScopeOffset(offset) => {
|
||||
// For dangling `type()` calls, compute the absolute index from the offset.
|
||||
let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0));
|
||||
let anchor_u32 = scope_anchor
|
||||
.as_u32()
|
||||
.expect("anchor should not be NodeIndex::NONE");
|
||||
let absolute_index = NodeIndex::from(anchor_u32 + offset);
|
||||
// Dynamic classes are only created from regular assignments (e.g., `Foo = type(...)`).
|
||||
let DefinitionKind::Assignment(assignment) = definition.kind(db) else {
|
||||
unreachable!("DynamicClassLiteral should only be created from Assignment definitions");
|
||||
};
|
||||
assignment.value(&module).range()
|
||||
}
|
||||
|
||||
// Get the node and return its range.
|
||||
let node: &ast::ExprCall = module
|
||||
.get_by_index(absolute_index)
|
||||
.try_into()
|
||||
.expect("scope offset should point to ExprCall");
|
||||
node.range()
|
||||
}
|
||||
}
|
||||
/// Returns the file containing the `type()` call.
|
||||
pub(crate) fn file(self, db: &'db dyn Db) -> File {
|
||||
self.definition(db).file(db)
|
||||
}
|
||||
|
||||
/// Returns the scope containing the `type()` call.
|
||||
pub(crate) fn file_scope(self, db: &'db dyn Db) -> FileScopeId {
|
||||
self.definition(db).file_scope(db)
|
||||
}
|
||||
|
||||
/// Get the metaclass of this dynamic class.
|
||||
@@ -4984,29 +4900,6 @@ impl<'db> DynamicClassLiteral<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up a class member defined directly on this class (not inherited).
|
||||
///
|
||||
/// Returns [`Member::unbound`] if the member is not found in the namespace dict,
|
||||
/// unless the namespace is dynamic, in which case returns `Unknown`.
|
||||
pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Member<'db> {
|
||||
// If the namespace is dynamic (not a literal dict) and the name isn't in `self.members`,
|
||||
// return Unknown since we can't know what attributes might be defined.
|
||||
self.members(db)
|
||||
.iter()
|
||||
.find_map(|(member_name, ty)| (name == member_name).then_some(*ty))
|
||||
.or_else(|| self.has_dynamic_namespace(db).then(Type::unknown))
|
||||
.map(Member::definitely_declared)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Look up an instance member defined directly on this class (not inherited).
|
||||
///
|
||||
/// For dynamic classes, instance members are the same as class members
|
||||
/// since they come from the namespace dict.
|
||||
pub(super) fn own_instance_member(self, db: &'db dyn Db, name: &str) -> Member<'db> {
|
||||
self.own_class_member(db, name)
|
||||
}
|
||||
|
||||
/// Try to compute the MRO for this dynamic class.
|
||||
///
|
||||
/// Returns `Ok(Mro)` if successful, or `Err(DynamicMroError)` if there's
|
||||
@@ -5016,50 +4909,6 @@ impl<'db> DynamicClassLiteral<'db> {
|
||||
Mro::of_dynamic_class(db, self)
|
||||
}
|
||||
|
||||
/// Return `Some()` if this dynamic class is known to be a [`DisjointBase`].
|
||||
///
|
||||
/// A dynamic class is a disjoint base if `__slots__` is defined in the namespace
|
||||
/// dictionary and is non-empty. Example:
|
||||
/// ```python
|
||||
/// X = type("X", (), {"__slots__": ("a",)})
|
||||
/// ```
|
||||
pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option<DisjointBase<'db>> {
|
||||
// Check if __slots__ is in the members
|
||||
for (name, ty) in self.members(db) {
|
||||
if name.as_str() == "__slots__" {
|
||||
// Check if the slots are non-empty
|
||||
let is_non_empty = match ty {
|
||||
// __slots__ = ("a", "b")
|
||||
Type::NominalInstance(nominal) => nominal.tuple_spec(db).is_some_and(|spec| {
|
||||
spec.len().into_fixed_length().is_some_and(|len| len > 0)
|
||||
}),
|
||||
// __slots__ = "abc" # Same as ("abc",)
|
||||
Type::StringLiteral(_) => true,
|
||||
// Other types are considered dynamic/unknown
|
||||
_ => false,
|
||||
};
|
||||
if is_non_empty {
|
||||
return Some(DisjointBase::due_to_dunder_slots(ClassLiteral::Dynamic(
|
||||
self,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns `true` if this dynamic class defines any ordering method (`__lt__`, `__le__`,
|
||||
/// `__gt__`, `__ge__`) in its namespace dictionary. Used by `@total_ordering` to determine
|
||||
/// if synthesis is valid.
|
||||
///
|
||||
/// If the namespace is dynamic, returns `true` since we can't know if ordering methods exist.
|
||||
pub(crate) fn has_own_ordering_method(self, db: &'db dyn Db) -> bool {
|
||||
const ORDERING_METHODS: &[&str] = &["__lt__", "__le__", "__gt__", "__ge__"];
|
||||
ORDERING_METHODS
|
||||
.iter()
|
||||
.any(|name| !self.own_class_member(db, name).is_undefined())
|
||||
}
|
||||
|
||||
/// Returns a new [`DynamicClassLiteral`] with the given dataclass params, preserving all other fields.
|
||||
pub(crate) fn with_dataclass_params(
|
||||
self,
|
||||
@@ -5070,10 +4919,7 @@ impl<'db> DynamicClassLiteral<'db> {
|
||||
db,
|
||||
self.name(db).clone(),
|
||||
self.bases(db),
|
||||
self.scope(db),
|
||||
self.anchor(db),
|
||||
self.members(db),
|
||||
self.has_dynamic_namespace(db),
|
||||
self.definition(db),
|
||||
dataclass_params,
|
||||
)
|
||||
}
|
||||
@@ -5365,8 +5211,7 @@ impl<'db> QualifiedClassName<'db> {
|
||||
}
|
||||
ClassLiteral::Dynamic(class) => {
|
||||
// Dynamic classes don't have a body scope; start from the enclosing scope.
|
||||
let scope = class.scope(self.db);
|
||||
(scope.file(self.db), scope.file_scope_id(self.db), 0)
|
||||
(class.file(self.db), class.file_scope(self.db), 0)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5451,7 +5296,7 @@ impl InheritanceCycle {
|
||||
/// [PEP 800]: https://peps.python.org/pep-0800/
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
||||
pub(super) struct DisjointBase<'db> {
|
||||
pub(super) class: ClassLiteral<'db>,
|
||||
pub(super) class: StaticClassLiteral<'db>,
|
||||
pub(super) kind: DisjointBaseKind,
|
||||
}
|
||||
|
||||
@@ -5460,14 +5305,14 @@ impl<'db> DisjointBase<'db> {
|
||||
/// because it has the `@disjoint_base` decorator on its definition
|
||||
fn due_to_decorator(class: StaticClassLiteral<'db>) -> Self {
|
||||
Self {
|
||||
class: ClassLiteral::Static(class),
|
||||
class,
|
||||
kind: DisjointBaseKind::DisjointBaseDecorator,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`DisjointBase`] instance where we know the class is a disjoint base
|
||||
/// because of its `__slots__` definition.
|
||||
fn due_to_dunder_slots(class: ClassLiteral<'db>) -> Self {
|
||||
fn due_to_dunder_slots(class: StaticClassLiteral<'db>) -> Self {
|
||||
Self {
|
||||
class,
|
||||
kind: DisjointBaseKind::DefinesSlots,
|
||||
@@ -5479,12 +5324,10 @@ impl<'db> DisjointBase<'db> {
|
||||
self == other
|
||||
|| self
|
||||
.class
|
||||
.default_specialization(db)
|
||||
.is_subclass_of(db, other.class.default_specialization(db))
|
||||
.is_subclass_of(db, None, other.class.default_specialization(db))
|
||||
|| other
|
||||
.class
|
||||
.default_specialization(db)
|
||||
.is_subclass_of(db, self.class.default_specialization(db))
|
||||
.is_subclass_of(db, None, self.class.default_specialization(db))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::Db;
|
||||
use crate::types::class::CodeGeneratorKind;
|
||||
use crate::types::generics::{ApplySpecialization, Specialization};
|
||||
use crate::types::mro::MroIterator;
|
||||
use crate::{Db, DisplaySettings};
|
||||
|
||||
use crate::types::tuple::TupleType;
|
||||
use crate::types::{
|
||||
ApplyTypeMappingVisitor, ClassLiteral, ClassType, DynamicType, KnownClass, KnownInstanceType,
|
||||
MaterializationKind, NormalizedVisitor, SpecialFormType, StaticMroError, Type, TypeContext,
|
||||
MaterializationKind, MroError, NormalizedVisitor, SpecialFormType, Type, TypeContext,
|
||||
TypeMapping, todo_type,
|
||||
};
|
||||
|
||||
@@ -381,7 +381,7 @@ impl<'db> ClassBase<'db> {
|
||||
};
|
||||
class_literal
|
||||
.try_mro(db, specialization)
|
||||
.is_err_and(StaticMroError::is_cycle)
|
||||
.is_err_and(MroError::is_cycle)
|
||||
}
|
||||
ClassBase::Dynamic(_)
|
||||
| ClassBase::Generic
|
||||
@@ -408,27 +408,16 @@ impl<'db> ClassBase<'db> {
|
||||
}
|
||||
|
||||
pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display {
|
||||
self.display_with(db, DisplaySettings::default())
|
||||
}
|
||||
|
||||
pub(super) fn display_with(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
display_settings: DisplaySettings<'db>,
|
||||
) -> impl std::fmt::Display {
|
||||
struct ClassBaseDisplay<'db> {
|
||||
db: &'db dyn Db,
|
||||
base: ClassBase<'db>,
|
||||
settings: DisplaySettings<'db>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ClassBaseDisplay<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.base {
|
||||
ClassBase::Dynamic(dynamic) => dynamic.fmt(f),
|
||||
ClassBase::Class(class) => Type::from(class)
|
||||
.display_with(self.db, self.settings.clone())
|
||||
.fmt(f),
|
||||
ClassBase::Class(class) => Type::from(class).display(self.db).fmt(f),
|
||||
ClassBase::Protocol => f.write_str("typing.Protocol"),
|
||||
ClassBase::Generic => f.write_str("typing.Generic"),
|
||||
ClassBase::TypedDict => f.write_str("typing.TypedDict"),
|
||||
@@ -436,11 +425,7 @@ impl<'db> ClassBase<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
ClassBaseDisplay {
|
||||
db,
|
||||
base: self,
|
||||
settings: display_settings,
|
||||
}
|
||||
ClassBaseDisplay { db, base: self }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3009,15 +3009,16 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast:
|
||||
|
||||
pub(crate) fn report_instance_layout_conflict(
|
||||
context: &InferContext,
|
||||
header_range: TextRange,
|
||||
base_nodes: Option<&[ast::Expr]>,
|
||||
class: StaticClassLiteral,
|
||||
node: &ast::StmtClassDef,
|
||||
disjoint_bases: &IncompatibleBases,
|
||||
) {
|
||||
debug_assert!(disjoint_bases.len() > 1);
|
||||
|
||||
let db = context.db();
|
||||
|
||||
let Some(builder) = context.report_lint(&INSTANCE_LAYOUT_CONFLICT, header_range) else {
|
||||
let Some(builder) = context.report_lint(&INSTANCE_LAYOUT_CONFLICT, class.header_range(db))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -3041,14 +3042,9 @@ pub(crate) fn report_instance_layout_conflict(
|
||||
originating_base,
|
||||
} = disjoint_base_info;
|
||||
|
||||
// Get the span for this base from the AST (if available)
|
||||
let Some(base_node) = base_nodes.and_then(|nodes| nodes.get(*node_index)) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let span = context.span(base_node);
|
||||
let span = context.span(&node.bases()[*node_index]);
|
||||
let mut annotation = Annotation::secondary(span.clone());
|
||||
if *originating_base == disjoint_base.class {
|
||||
if originating_base.as_static() == Some(disjoint_base.class) {
|
||||
match disjoint_base.kind {
|
||||
DisjointBaseKind::DefinesSlots => {
|
||||
annotation = annotation.message(format_args!(
|
||||
@@ -3172,10 +3168,11 @@ impl<'db> IncompatibleBases<'db> {
|
||||
.keys()
|
||||
.filter(|other_base| other_base != disjoint_base)
|
||||
.all(|other_base| {
|
||||
!disjoint_base
|
||||
.class
|
||||
.default_specialization(db)
|
||||
.is_subclass_of(db, other_base.class.default_specialization(db))
|
||||
!disjoint_base.class.is_subclass_of(
|
||||
db,
|
||||
None,
|
||||
other_base.class.default_specialization(db),
|
||||
)
|
||||
})
|
||||
})
|
||||
.map(|(base, info)| (*base, *info))
|
||||
|
||||
@@ -7,10 +7,9 @@ use std::fmt::{self, Display, Formatter, Write};
|
||||
use std::rc::Rc;
|
||||
|
||||
use ruff_db::files::FilePath;
|
||||
use ruff_db::source::{line_index, source_text};
|
||||
use ruff_db::source::line_index;
|
||||
use ruff_python_ast::str::{Quote, TripleQuotes};
|
||||
use ruff_python_literal::escape::AsciiEscape;
|
||||
use ruff_source_file::LineColumn;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
@@ -582,10 +581,9 @@ impl<'db> FmtDetailed<'db> for ClassDisplay<'db> {
|
||||
FilePath::Vendored(_) | FilePath::SystemVirtual(_) => Cow::Borrowed(path),
|
||||
};
|
||||
let line_index = line_index(self.db, file);
|
||||
let LineColumn { line, column } =
|
||||
line_index.line_column(class_offset, &source_text(self.db, file));
|
||||
let line_number = line_index.line_index(class_offset);
|
||||
f.set_invalid_type_annotation();
|
||||
write!(f, " @ {path}:{line}:{column}")?;
|
||||
write!(f, " @ {path}:{line_number}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1818,19 +1818,10 @@ impl KnownFunction {
|
||||
let mut diag = builder.into_diagnostic("Revealed MRO");
|
||||
let span = context.span(&call_expression.arguments.args[0]);
|
||||
let mut message = String::new();
|
||||
let display_settings = DisplaySettings::from_possibly_ambiguous_types(
|
||||
db,
|
||||
classes
|
||||
.iter()
|
||||
.flat_map(|class| class.iter_mro(db))
|
||||
.filter_map(ClassBase::into_class),
|
||||
);
|
||||
for (i, class) in classes.iter().enumerate() {
|
||||
message.push('(');
|
||||
for class in class.iter_mro(db) {
|
||||
message.push_str(
|
||||
&class.display_with(db, display_settings.clone()).to_string(),
|
||||
);
|
||||
message.push_str(&class.display(db).to_string());
|
||||
// Omit the comma for the last element (which is always `object`)
|
||||
if class
|
||||
.into_class()
|
||||
|
||||
@@ -96,7 +96,7 @@ pub(crate) fn typing_self<'db>(
|
||||
let identity = TypeVarIdentity::new(
|
||||
db,
|
||||
ast::name::Name::new_static("Self"),
|
||||
class.definition(db),
|
||||
Some(class.definition(db)),
|
||||
TypeVarKind::TypingSelf,
|
||||
);
|
||||
let bounds = TypeVarBoundOrConstraints::UpperBound(Type::instance(
|
||||
|
||||
@@ -169,9 +169,9 @@ pub fn definitions_for_name<'db>(
|
||||
// instead of `int` (hover only shows the docstring of the first definition).
|
||||
.rev()
|
||||
.filter_map(|ty| ty.as_nominal_instance())
|
||||
.filter_map(|instance| {
|
||||
let definition = instance.class_literal(db).definition(db)?;
|
||||
Some(ResolvedDefinition::Definition(definition))
|
||||
.map(|instance| {
|
||||
let definition = instance.class_literal(db).definition(db);
|
||||
ResolvedDefinition::Definition(definition)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
@@ -1377,7 +1377,8 @@ mod resolve_definition {
|
||||
| DefinitionKind::ExceptHandler(_)
|
||||
| DefinitionKind::TypeVar(_)
|
||||
| DefinitionKind::ParamSpec(_)
|
||||
| DefinitionKind::TypeVarTuple(_) => {
|
||||
| DefinitionKind::TypeVarTuple(_)
|
||||
| DefinitionKind::LoopHeader(_) => {
|
||||
// Not yet implemented
|
||||
return Err(());
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId};
|
||||
use crate::semantic_index::definition::{
|
||||
AnnotatedAssignmentDefinitionKind, AssignmentDefinitionKind, ComprehensionDefinitionKind,
|
||||
Definition, DefinitionKind, DefinitionNodeKey, DefinitionState, ExceptHandlerDefinitionKind,
|
||||
ForStmtDefinitionKind, TargetKind, WithItemDefinitionKind,
|
||||
ForStmtDefinitionKind, LoopHeaderDefinitionKind, TargetKind, WithItemDefinitionKind,
|
||||
};
|
||||
use crate::semantic_index::expression::{Expression, ExpressionKind};
|
||||
use crate::semantic_index::narrowing_constraints::ConstraintKey;
|
||||
@@ -55,8 +55,8 @@ use crate::subscript::{PyIndex, PySlice};
|
||||
use crate::types::call::bind::{CallableDescription, MatchingOverloadIndex};
|
||||
use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind};
|
||||
use crate::types::class::{
|
||||
ClassLiteral, CodeGeneratorKind, DynamicClassAnchor, DynamicClassLiteral,
|
||||
DynamicMetaclassConflict, FieldKind, MetaclassErrorKind, MethodDecorator,
|
||||
ClassLiteral, CodeGeneratorKind, DynamicClassLiteral, DynamicMetaclassConflict, FieldKind,
|
||||
MetaclassErrorKind, MethodDecorator,
|
||||
};
|
||||
use crate::types::context::{InNoTypeCheck, InferContext};
|
||||
use crate::types::cyclic::CycleDetector;
|
||||
@@ -69,11 +69,11 @@ use crate::types::diagnostic::{
|
||||
INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC,
|
||||
INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
|
||||
INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases,
|
||||
NO_MATCHING_OVERLOAD, NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE,
|
||||
POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS,
|
||||
TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
|
||||
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR,
|
||||
USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions,
|
||||
NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL,
|
||||
POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL,
|
||||
UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE,
|
||||
UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
|
||||
hint_if_stdlib_attribute_exists_on_other_versions,
|
||||
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
|
||||
report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance,
|
||||
report_cannot_delete_typed_dict_key, report_cannot_pop_required_field_on_typed_dict,
|
||||
@@ -102,7 +102,7 @@ use crate::types::generics::{
|
||||
};
|
||||
use crate::types::infer::nearest_enclosing_function;
|
||||
use crate::types::instance::SliceLiteral;
|
||||
use crate::types::mro::{DynamicMroErrorKind, StaticMroErrorKind};
|
||||
use crate::types::mro::{DynamicMroErrorKind, MroErrorKind};
|
||||
use crate::types::newtype::NewType;
|
||||
use crate::types::subclass_of::SubclassOfInner;
|
||||
use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleType};
|
||||
@@ -121,7 +121,7 @@ use crate::types::{
|
||||
TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation,
|
||||
TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance,
|
||||
TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, binding_type, infer_scope_types,
|
||||
todo_type,
|
||||
todo_type, with_loop_header_override,
|
||||
};
|
||||
use crate::types::{CallableTypes, overrides};
|
||||
use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
|
||||
@@ -797,13 +797,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
// (4) Check that the class's MRO is resolvable
|
||||
match class.try_mro(self.db(), None) {
|
||||
Err(mro_error) => match mro_error.reason() {
|
||||
StaticMroErrorKind::DuplicateBases(duplicates) => {
|
||||
MroErrorKind::DuplicateBases(duplicates) => {
|
||||
let base_nodes = class_node.bases();
|
||||
for duplicate in duplicates {
|
||||
report_duplicate_bases(&self.context, class, duplicate, base_nodes);
|
||||
}
|
||||
}
|
||||
StaticMroErrorKind::InvalidBases(bases) => {
|
||||
MroErrorKind::InvalidBases(bases) => {
|
||||
let base_nodes = class_node.bases();
|
||||
for (index, base_ty) in bases {
|
||||
report_invalid_or_unsupported_base(
|
||||
@@ -814,7 +814,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
);
|
||||
}
|
||||
}
|
||||
StaticMroErrorKind::UnresolvableMro { bases_list } => {
|
||||
MroErrorKind::UnresolvableMro { bases_list } => {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INCONSISTENT_MRO, class_node)
|
||||
{
|
||||
@@ -829,7 +829,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
));
|
||||
}
|
||||
}
|
||||
StaticMroErrorKind::Pep695ClassWithGenericInheritance => {
|
||||
MroErrorKind::Pep695ClassWithGenericInheritance => {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_GENERIC_CLASS, class_node)
|
||||
{
|
||||
@@ -839,7 +839,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
);
|
||||
}
|
||||
}
|
||||
StaticMroErrorKind::InheritanceCycle => {
|
||||
MroErrorKind::InheritanceCycle => {
|
||||
if let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&CYCLIC_CLASS_DEFINITION, class_node)
|
||||
@@ -857,8 +857,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
if disjoint_bases.len() > 1 {
|
||||
report_instance_layout_conflict(
|
||||
&self.context,
|
||||
class.header_range(self.db()),
|
||||
Some(class_node.bases()),
|
||||
class,
|
||||
class_node,
|
||||
&disjoint_bases,
|
||||
);
|
||||
}
|
||||
@@ -1501,6 +1501,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
DefinitionKind::TypeVarTuple(node) => {
|
||||
self.infer_typevartuple_definition(node.node(self.module()), definition);
|
||||
}
|
||||
DefinitionKind::LoopHeader(loop_header) => {
|
||||
self.infer_loop_header_definition(&loop_header, definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1530,6 +1533,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
DefinitionKind::Assignment(assignment) => {
|
||||
self.infer_assignment_deferred(assignment.value(self.module()));
|
||||
}
|
||||
DefinitionKind::LoopHeader(_) => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -5384,6 +5388,66 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
add.insert(self, target_ty);
|
||||
}
|
||||
|
||||
fn infer_loop_header_definition(
|
||||
&mut self,
|
||||
loop_header: &LoopHeaderDefinitionKind<'db>,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
const MAX_LOOP_HEADER_ITERATIONS: usize = 8;
|
||||
let max_iterations = MAX_LOOP_HEADER_ITERATIONS;
|
||||
let should_widen = true;
|
||||
|
||||
let add = self.add_binding(loop_header.node(self.module()).into(), definition);
|
||||
let mut seed_union = UnionBuilder::new(self.db());
|
||||
for seed_definition in loop_header.seed_definitions() {
|
||||
seed_union = seed_union.add(binding_type(self.db(), *seed_definition));
|
||||
}
|
||||
let mut current = if seed_union.is_empty() {
|
||||
Type::unknown()
|
||||
} else {
|
||||
seed_union.build()
|
||||
};
|
||||
|
||||
let mut changed = false;
|
||||
for _ in 0..max_iterations {
|
||||
let previous_multi = self.multi_inference_state;
|
||||
self.multi_inference_state = MultiInferenceState::Overwrite;
|
||||
let next = with_loop_header_override(definition, current, || {
|
||||
let mut union = UnionBuilder::new(self.db());
|
||||
for definition in loop_header.definitions() {
|
||||
let inferred = match definition.kind(self.db()) {
|
||||
DefinitionKind::Assignment(assignment) => self
|
||||
.infer_assignment_definition_impl(
|
||||
&assignment,
|
||||
*definition,
|
||||
TypeContext::default(),
|
||||
),
|
||||
DefinitionKind::AugmentedAssignment(augmented_assignment) => {
|
||||
self.infer_augment_assignment(augmented_assignment.node(self.module()))
|
||||
}
|
||||
_ => binding_type(self.db(), *definition),
|
||||
};
|
||||
union = union.add(inferred);
|
||||
}
|
||||
union.build()
|
||||
});
|
||||
self.multi_inference_state = previous_multi;
|
||||
|
||||
if next == current {
|
||||
changed = false;
|
||||
break;
|
||||
}
|
||||
changed = true;
|
||||
current = next;
|
||||
}
|
||||
|
||||
if should_widen && changed {
|
||||
current = KnownClass::Int.to_instance(self.db());
|
||||
}
|
||||
|
||||
add.insert(self, current);
|
||||
}
|
||||
|
||||
fn infer_assignment_definition_impl(
|
||||
&mut self,
|
||||
assignment: &AssignmentDefinitionKind<'db>,
|
||||
@@ -5448,9 +5512,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
// Try to extract the dynamic class with definition.
|
||||
// This returns `None` if it's not a three-arg call to `type()`,
|
||||
// signalling that we must fall back to normal call inference.
|
||||
self.infer_dynamic_type_expression(call_expr, Some(definition))
|
||||
self.infer_dynamic_type_expression(call_expr, definition)
|
||||
.unwrap_or_else(|| {
|
||||
self.infer_type_call_fallback(call_expr, callable_type, tcx)
|
||||
self.infer_call_expression_impl(call_expr, callable_type, tcx)
|
||||
})
|
||||
}
|
||||
Some(_) | None => {
|
||||
@@ -6056,67 +6120,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fallback for `type()` calls when `infer_dynamic_type_expression` returns `None`.
|
||||
///
|
||||
/// This handles ambiguous `type()` calls that have variadic arguments (`*args`, `**kwargs`)
|
||||
/// or invalid keyword arguments. In these cases, we return `type[Unknown]` instead of
|
||||
/// going through normal overload resolution, which could give misleading results
|
||||
/// (e.g., matching the single-argument overload when there might be more arguments).
|
||||
fn infer_type_call_fallback(
|
||||
&mut self,
|
||||
call_expr: &ast::ExprCall,
|
||||
callable_type: Type<'db>,
|
||||
tcx: TypeContext<'db>,
|
||||
) -> Type<'db> {
|
||||
self.try_type_call_fallback(call_expr)
|
||||
.unwrap_or_else(|| self.infer_call_expression_impl(call_expr, callable_type, tcx))
|
||||
}
|
||||
|
||||
/// Try to handle an ambiguous `type()` call with variadic arguments or invalid kwargs.
|
||||
///
|
||||
/// Returns `Some(type[Unknown])` if this is an ambiguous `type()` call that should
|
||||
/// not go through normal overload resolution. Returns `None` if normal call
|
||||
/// inference should proceed.
|
||||
fn try_type_call_fallback(&mut self, call_expr: &ast::ExprCall) -> Option<Type<'db>> {
|
||||
let arguments = &call_expr.arguments;
|
||||
|
||||
// Check for variadic arguments that make the overload ambiguous.
|
||||
let has_starred_args = arguments.args.iter().any(ast::Expr::is_starred_expr);
|
||||
let has_kwargs_unpack = arguments.keywords.iter().any(|kw| kw.arg.is_none());
|
||||
|
||||
// If we have variadic arguments, we can't determine which overload of `type()`
|
||||
// is being called. Return type[Unknown].
|
||||
if has_starred_args || has_kwargs_unpack {
|
||||
for arg in &arguments.args {
|
||||
self.infer_expression(arg, TypeContext::default());
|
||||
}
|
||||
for kw in &arguments.keywords {
|
||||
self.infer_expression(&kw.value, TypeContext::default());
|
||||
}
|
||||
return Some(SubclassOfType::subclass_of_unknown());
|
||||
}
|
||||
|
||||
// If we have explicit keyword arguments (not **kwargs unpack), this is an invalid
|
||||
// type() call. Return type[Unknown] instead of Unknown to reduce false positives.
|
||||
if !arguments.keywords.is_empty() && !has_kwargs_unpack && arguments.args.len() == 3 {
|
||||
// Infer all argument types to ensure they're stored.
|
||||
for arg in &arguments.args {
|
||||
self.infer_expression(arg, TypeContext::default());
|
||||
}
|
||||
for kw in &arguments.keywords {
|
||||
self.infer_expression(&kw.value, TypeContext::default());
|
||||
}
|
||||
|
||||
if let Some(builder) = self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) {
|
||||
builder.into_diagnostic("No overload of class `type` matches arguments");
|
||||
}
|
||||
|
||||
return Some(SubclassOfType::subclass_of_unknown());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Try to infer a 3-argument `type(name, bases, dict)` call expression, capturing the definition.
|
||||
///
|
||||
/// This is called when we detect a `type()` call in assignment context and want to
|
||||
@@ -6128,7 +6131,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
fn infer_dynamic_type_expression(
|
||||
&mut self,
|
||||
call_expr: &ast::ExprCall,
|
||||
definition: Option<Definition<'db>>,
|
||||
definition: Definition<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
let db = self.db();
|
||||
|
||||
@@ -6156,59 +6159,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
// Infer the argument types.
|
||||
let name_type = self.infer_expression(name_arg, TypeContext::default());
|
||||
let bases_type = self.infer_expression(bases_arg, TypeContext::default());
|
||||
|
||||
// Extract members from the namespace dict (third argument).
|
||||
// Infer the whole dict first to avoid double-inferring individual values.
|
||||
let namespace_type = self.infer_expression(namespace_arg, TypeContext::default());
|
||||
let (members, has_dynamic_namespace): (Box<[(ast::name::Name, Type<'db>)]>, bool) =
|
||||
if let ast::Expr::Dict(dict) = namespace_arg {
|
||||
// Check if all keys are string literal types. If any key is not a string literal
|
||||
// type or is missing (spread), the namespace is considered dynamic.
|
||||
let all_keys_are_string_literals = dict.items.iter().all(|item| {
|
||||
item.key
|
||||
.as_ref()
|
||||
.is_some_and(|k| matches!(self.expression_type(k), Type::StringLiteral(_)))
|
||||
});
|
||||
let members = dict
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
// Only extract items with string literal keys.
|
||||
let key_expr = item.key.as_ref()?;
|
||||
let key_name = match self.expression_type(key_expr) {
|
||||
Type::StringLiteral(s) => ast::name::Name::new(s.value(db)),
|
||||
_ => return None,
|
||||
};
|
||||
// Get the already-inferred type from when we inferred the dict above.
|
||||
let value_ty = self.expression_type(&item.value);
|
||||
Some((key_name, value_ty))
|
||||
})
|
||||
.collect();
|
||||
(members, !all_keys_are_string_literals)
|
||||
} else if let Type::TypedDict(typed_dict) = namespace_type {
|
||||
// Namespace is a TypedDict instance. Extract known keys as members.
|
||||
// TypedDicts are "open" (can have additional string keys), so this
|
||||
// is still a dynamic namespace for unknown attributes.
|
||||
let members: Box<[(ast::name::Name, Type<'db>)]> = typed_dict
|
||||
.items(db)
|
||||
.iter()
|
||||
.map(|(name, field)| (name.clone(), field.declared_ty))
|
||||
.collect();
|
||||
(members, true)
|
||||
} else {
|
||||
// Namespace is not a dict literal, so it's dynamic.
|
||||
(Box::new([]), true)
|
||||
};
|
||||
|
||||
if !matches!(namespace_type, Type::TypedDict(_))
|
||||
&& !namespace_type.is_assignable_to(
|
||||
db,
|
||||
KnownClass::Dict
|
||||
.to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]),
|
||||
)
|
||||
&& let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&INVALID_ARGUMENT_TYPE, namespace_arg)
|
||||
if !namespace_type.is_assignable_to(
|
||||
db,
|
||||
KnownClass::Dict
|
||||
.to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()]),
|
||||
) && let Some(builder) = self
|
||||
.context
|
||||
.report_lint(&INVALID_ARGUMENT_TYPE, namespace_arg)
|
||||
{
|
||||
let mut diagnostic = builder
|
||||
.into_diagnostic("Invalid argument to parameter 3 (`namespace`) of `type()`");
|
||||
@@ -6235,42 +6194,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
ast::name::Name::new_static("<unknown>")
|
||||
};
|
||||
|
||||
let (bases, mut disjoint_bases) =
|
||||
self.extract_dynamic_type_bases(bases_arg, bases_type, &name);
|
||||
let bases = self.extract_dynamic_type_bases(bases_arg, bases_type, &name);
|
||||
|
||||
let scope = self.scope();
|
||||
|
||||
// Create the anchor for identifying this dynamic class.
|
||||
// - For assigned `type()` calls, the Definition uniquely identifies the class.
|
||||
// - For dangling calls, compute a relative offset from the scope's node index.
|
||||
let anchor = if let Some(def) = definition {
|
||||
DynamicClassAnchor::Definition(def)
|
||||
} else {
|
||||
let call_node_index = call_expr.node_index().load();
|
||||
let scope_anchor = scope.node(db).node_index().unwrap_or(NodeIndex::from(0));
|
||||
let anchor_u32 = scope_anchor
|
||||
.as_u32()
|
||||
.expect("scope anchor should not be NodeIndex::NONE");
|
||||
let call_u32 = call_node_index
|
||||
.as_u32()
|
||||
.expect("call node should not be NodeIndex::NONE");
|
||||
DynamicClassAnchor::ScopeOffset(call_u32 - anchor_u32)
|
||||
};
|
||||
|
||||
let dynamic_class = DynamicClassLiteral::new(
|
||||
db,
|
||||
name,
|
||||
bases,
|
||||
scope,
|
||||
anchor,
|
||||
members,
|
||||
has_dynamic_namespace,
|
||||
None,
|
||||
);
|
||||
let dynamic_class = DynamicClassLiteral::new(db, name, bases, definition, None);
|
||||
|
||||
// Check for MRO errors.
|
||||
match dynamic_class.try_mro(db) {
|
||||
Err(error) => match error.reason() {
|
||||
if let Err(error) = dynamic_class.try_mro(db) {
|
||||
match error.reason() {
|
||||
DynamicMroErrorKind::DuplicateBases(duplicates) => {
|
||||
if let Some(builder) = self.context.report_lint(&DUPLICATE_BASE, call_expr) {
|
||||
builder.into_diagnostic(format_args!(
|
||||
@@ -6298,18 +6228,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
));
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(_) => {
|
||||
// MRO succeeded, check for instance-layout-conflict.
|
||||
disjoint_bases.remove_redundant_entries(db);
|
||||
if disjoint_bases.len() > 1 {
|
||||
report_instance_layout_conflict(
|
||||
&self.context,
|
||||
dynamic_class.header_range(db),
|
||||
bases_arg.as_tuple_expr().map(|tuple| tuple.elts.as_slice()),
|
||||
&disjoint_bases,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6337,15 +6255,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
|
||||
/// Extract base classes from the second argument of a `type()` call.
|
||||
///
|
||||
/// Returns the extracted bases and any disjoint bases found (for instance-layout-conflict
|
||||
/// checking). If any bases were invalid, diagnostics are emitted and the dynamic class is
|
||||
/// inferred as inheriting from `Unknown`.
|
||||
/// If any bases were invalid, diagnostics are emitted and the dynamic
|
||||
/// class is inferred as inheriting from `Unknown`.
|
||||
fn extract_dynamic_type_bases(
|
||||
&mut self,
|
||||
bases_node: &ast::Expr,
|
||||
bases_type: Type<'db>,
|
||||
name: &ast::name::Name,
|
||||
) -> (Box<[ClassBase<'db>]>, IncompatibleBases<'db>) {
|
||||
) -> Box<[ClassBase<'db>]> {
|
||||
let db = self.db();
|
||||
|
||||
// Get AST nodes for base expressions (for diagnostics).
|
||||
@@ -6356,9 +6273,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
let placeholder_class: ClassLiteral<'db> =
|
||||
KnownClass::Object.try_to_class_literal(db).unwrap().into();
|
||||
|
||||
let mut disjoint_bases = IncompatibleBases::default();
|
||||
|
||||
let bases = bases_type
|
||||
bases_type
|
||||
.tuple_instance_spec(db)
|
||||
.as_deref()
|
||||
.and_then(|spec| spec.as_fixed_length())
|
||||
@@ -6373,16 +6288,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
if let Some(class_base) =
|
||||
ClassBase::try_from_type(db, *base, placeholder_class)
|
||||
{
|
||||
// Collect disjoint bases for instance-layout-conflict checking.
|
||||
if let ClassBase::Class(base_class) = class_base {
|
||||
if let Some(disjoint_base) = base_class.nearest_disjoint_base(db) {
|
||||
disjoint_bases.insert(
|
||||
disjoint_base,
|
||||
idx,
|
||||
base_class.class_literal(db),
|
||||
);
|
||||
}
|
||||
}
|
||||
return class_base;
|
||||
}
|
||||
|
||||
@@ -6415,18 +6320,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
"Only class objects or `Any` are supported as class bases",
|
||||
);
|
||||
}
|
||||
} else if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_BASE, diagnostic_node)
|
||||
{
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
"Invalid class base with type `{}`",
|
||||
base.display(db)
|
||||
));
|
||||
if bases_tuple_elts.is_none() {
|
||||
diagnostic.info(format_args!(
|
||||
"Element {} of the tuple is invalid",
|
||||
idx + 1
|
||||
} else {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_BASE, diagnostic_node)
|
||||
{
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
"Invalid class base with type `{}`",
|
||||
base.display(db)
|
||||
));
|
||||
if bases_tuple_elts.is_none() {
|
||||
diagnostic.info(format_args!(
|
||||
"Element {} of the tuple is invalid",
|
||||
idx + 1
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6449,9 +6356,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
));
|
||||
}
|
||||
Box::from([ClassBase::unknown()])
|
||||
});
|
||||
|
||||
(bases, disjoint_bases)
|
||||
})
|
||||
}
|
||||
|
||||
fn infer_annotated_assignment_statement(&mut self, assignment: &ast::StmtAnnAssign) {
|
||||
@@ -9374,21 +9279,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
return Type::TypedDict(typed_dict);
|
||||
}
|
||||
|
||||
// Handle 3-argument `type(name, bases, dict)`.
|
||||
if let Type::ClassLiteral(class) = callable_type
|
||||
&& class.is_known(self.db(), KnownClass::Type)
|
||||
{
|
||||
if let Some(dynamic_type) = self.infer_dynamic_type_expression(call_expression, None) {
|
||||
return dynamic_type;
|
||||
}
|
||||
|
||||
// Fallback for ambiguous `type()` calls (with `*args`, `**kwargs`, or invalid kwargs).
|
||||
let result = self.try_type_call_fallback(call_expression);
|
||||
if let Some(ty) = result {
|
||||
return ty;
|
||||
}
|
||||
}
|
||||
|
||||
// We don't call `Type::try_call`, because we want to perform type inference on the
|
||||
// arguments after matching them to parameters, but before checking that the argument types
|
||||
// are assignable to any parameter annotations.
|
||||
|
||||
@@ -53,7 +53,7 @@ impl<'db> Mro<'db> {
|
||||
db: &'db dyn Db,
|
||||
class_literal: StaticClassLiteral<'db>,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> Result<Self, StaticMroError<'db>> {
|
||||
) -> Result<Self, MroError<'db>> {
|
||||
/// Possibly add `Generic` to the resolved bases list.
|
||||
///
|
||||
/// This function is called in two cases:
|
||||
@@ -141,15 +141,10 @@ impl<'db> Mro<'db> {
|
||||
{
|
||||
ClassBase::try_from_type(db, *single_base, ClassLiteral::Static(class_literal))
|
||||
.map_or_else(
|
||||
|| {
|
||||
Err(StaticMroErrorKind::InvalidBases(Box::from([(
|
||||
0,
|
||||
*single_base,
|
||||
)])))
|
||||
},
|
||||
|| Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))),
|
||||
|single_base| {
|
||||
if single_base.has_cyclic_mro(db) {
|
||||
Err(StaticMroErrorKind::InheritanceCycle)
|
||||
Err(MroErrorKind::InheritanceCycle)
|
||||
} else {
|
||||
Ok(std::iter::once(ClassBase::Class(class))
|
||||
.chain(single_base.mro(db, specialization))
|
||||
@@ -193,10 +188,8 @@ impl<'db> Mro<'db> {
|
||||
}
|
||||
|
||||
if !invalid_bases.is_empty() {
|
||||
return Err(
|
||||
StaticMroErrorKind::InvalidBases(invalid_bases.into_boxed_slice())
|
||||
.into_mro_error(db, class),
|
||||
);
|
||||
return Err(MroErrorKind::InvalidBases(invalid_bases.into_boxed_slice())
|
||||
.into_mro_error(db, class));
|
||||
}
|
||||
|
||||
// `Generic` is implicitly added to the bases list of a class that has PEP-695 type parameters
|
||||
@@ -208,7 +201,7 @@ impl<'db> Mro<'db> {
|
||||
let mut seqs = vec![VecDeque::from([ClassBase::Class(class)])];
|
||||
for base in &resolved_bases {
|
||||
if base.has_cyclic_mro(db) {
|
||||
return Err(StaticMroErrorKind::InheritanceCycle.into_mro_error(db, class));
|
||||
return Err(MroErrorKind::InheritanceCycle.into_mro_error(db, class));
|
||||
}
|
||||
seqs.push(base.mro(db, specialization).collect());
|
||||
}
|
||||
@@ -236,8 +229,9 @@ impl<'db> Mro<'db> {
|
||||
)
|
||||
})
|
||||
{
|
||||
return Err(StaticMroErrorKind::Pep695ClassWithGenericInheritance
|
||||
.into_mro_error(db, class));
|
||||
return Err(
|
||||
MroErrorKind::Pep695ClassWithGenericInheritance.into_mro_error(db, class)
|
||||
);
|
||||
}
|
||||
|
||||
let mut duplicate_dynamic_bases = false;
|
||||
@@ -297,14 +291,14 @@ impl<'db> Mro<'db> {
|
||||
if duplicate_dynamic_bases {
|
||||
Ok(Mro::from_error(db, class))
|
||||
} else {
|
||||
Err(StaticMroErrorKind::UnresolvableMro {
|
||||
Err(MroErrorKind::UnresolvableMro {
|
||||
bases_list: original_bases.iter().copied().collect(),
|
||||
}
|
||||
.into_mro_error(db, class))
|
||||
}
|
||||
} else {
|
||||
Err(
|
||||
StaticMroErrorKind::DuplicateBases(duplicate_bases.into_boxed_slice())
|
||||
MroErrorKind::DuplicateBases(duplicate_bases.into_boxed_slice())
|
||||
.into_mro_error(db, class),
|
||||
)
|
||||
}
|
||||
@@ -543,23 +537,23 @@ impl<'db> Iterator for MroIterator<'db> {
|
||||
impl std::iter::FusedIterator for MroIterator<'_> {}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
pub(super) struct StaticMroError<'db> {
|
||||
kind: StaticMroErrorKind<'db>,
|
||||
pub(super) struct MroError<'db> {
|
||||
kind: MroErrorKind<'db>,
|
||||
fallback_mro: Mro<'db>,
|
||||
}
|
||||
|
||||
impl<'db> StaticMroError<'db> {
|
||||
impl<'db> MroError<'db> {
|
||||
/// Construct an MRO error of kind `InheritanceCycle`.
|
||||
pub(super) fn cycle(db: &'db dyn Db, class: ClassType<'db>) -> Self {
|
||||
StaticMroErrorKind::InheritanceCycle.into_mro_error(db, class)
|
||||
MroErrorKind::InheritanceCycle.into_mro_error(db, class)
|
||||
}
|
||||
|
||||
pub(super) fn is_cycle(&self) -> bool {
|
||||
matches!(self.kind, StaticMroErrorKind::InheritanceCycle)
|
||||
matches!(self.kind, MroErrorKind::InheritanceCycle)
|
||||
}
|
||||
|
||||
/// Return an [`StaticMroErrorKind`] variant describing why we could not resolve the MRO for this class.
|
||||
pub(super) fn reason(&self) -> &StaticMroErrorKind<'db> {
|
||||
/// Return an [`MroErrorKind`] variant describing why we could not resolve the MRO for this class.
|
||||
pub(super) fn reason(&self) -> &MroErrorKind<'db> {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
@@ -570,9 +564,9 @@ impl<'db> StaticMroError<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible ways in which attempting to resolve the MRO of a statically-defined class might fail.
|
||||
/// Possible ways in which attempting to resolve the MRO of a class might fail.
|
||||
#[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
pub(super) enum StaticMroErrorKind<'db> {
|
||||
pub(super) enum MroErrorKind<'db> {
|
||||
/// The class inherits from one or more invalid bases.
|
||||
///
|
||||
/// To avoid excessive complexity in our implementation,
|
||||
@@ -602,13 +596,9 @@ pub(super) enum StaticMroErrorKind<'db> {
|
||||
UnresolvableMro { bases_list: Box<[Type<'db>]> },
|
||||
}
|
||||
|
||||
impl<'db> StaticMroErrorKind<'db> {
|
||||
pub(super) fn into_mro_error(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
class: ClassType<'db>,
|
||||
) -> StaticMroError<'db> {
|
||||
StaticMroError {
|
||||
impl<'db> MroErrorKind<'db> {
|
||||
pub(super) fn into_mro_error(self, db: &'db dyn Db, class: ClassType<'db>) -> MroError<'db> {
|
||||
MroError {
|
||||
kind: self,
|
||||
fallback_mro: Mro::from_error(db, class),
|
||||
}
|
||||
@@ -670,7 +660,7 @@ fn c3_merge(mut sequences: Vec<VecDeque<ClassBase>>) -> Option<Mro> {
|
||||
|
||||
/// Error for dynamic class MRO computation with fallback MRO.
|
||||
///
|
||||
/// Separate from [`StaticMroError`] because dynamic classes can only have a subset of MRO errors.
|
||||
/// Separate from [`MroError`] because dynamic classes can only have a subset of MRO errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize, salsa::Update)]
|
||||
pub(crate) struct DynamicMroError<'db> {
|
||||
kind: DynamicMroErrorKind<'db>,
|
||||
|
||||
@@ -1250,29 +1250,6 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
|
||||
);
|
||||
}
|
||||
}
|
||||
// For symmetric operators (==, !=, is, is not), if left is not a narrowable target,
|
||||
// try to narrow the right operand instead by swapping the operands.
|
||||
// E.g., `None != x` should narrow `x` the same way as `x != None`.
|
||||
_ if matches!(
|
||||
op,
|
||||
ast::CmpOp::Eq | ast::CmpOp::NotEq | ast::CmpOp::Is | ast::CmpOp::IsNot
|
||||
) && matches!(
|
||||
right,
|
||||
ast::Expr::Name(_)
|
||||
| ast::Expr::Attribute(_)
|
||||
| ast::Expr::Subscript(_)
|
||||
| ast::Expr::Named(_)
|
||||
) =>
|
||||
{
|
||||
if let Some(right_place) = place_expr(right)
|
||||
// Swap lhs_ty and rhs_ty since we're narrowing the right operand
|
||||
&& let Some(ty) =
|
||||
self.evaluate_expr_compare_op(rhs_ty, lhs_ty, *op, is_positive)
|
||||
{
|
||||
let place = self.expect_place(&right_place);
|
||||
constraints.insert(place, NarrowingConstraint::regular(ty));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,14 +303,14 @@ impl<'db> TypedDictType<'db> {
|
||||
|
||||
pub fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
|
||||
match self {
|
||||
TypedDictType::Class(defining_class) => defining_class.definition(db),
|
||||
TypedDictType::Class(defining_class) => Some(defining_class.definition(db)),
|
||||
TypedDictType::Synthesized(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn type_definition(self, db: &'db dyn Db) -> Option<TypeDefinition<'db>> {
|
||||
match self {
|
||||
TypedDictType::Class(defining_class) => defining_class.type_definition(db),
|
||||
TypedDictType::Class(defining_class) => Some(defining_class.type_definition(db)),
|
||||
TypedDictType::Synthesized(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use self::schedule::spawn_main_loop;
|
||||
use crate::PositionEncoding;
|
||||
use crate::capabilities::{ResolvedClientCapabilities, server_capabilities};
|
||||
use crate::session::{ClientName, InitializationOptions, Session, warn_about_unknown_options};
|
||||
use crate::session::{InitializationOptions, Session, warn_about_unknown_options};
|
||||
use anyhow::Context;
|
||||
use lsp_server::Connection;
|
||||
use lsp_types::{ClientCapabilities, InitializeParams, MessageType, Url};
|
||||
@@ -47,7 +47,6 @@ impl Server {
|
||||
initialization_options,
|
||||
capabilities: client_capabilities,
|
||||
workspace_folders,
|
||||
client_info,
|
||||
..
|
||||
} = serde_json::from_value(init_value)
|
||||
.context("Failed to deserialize initialization parameters")?;
|
||||
@@ -66,7 +65,6 @@ impl Server {
|
||||
tracing::error!("Failed to deserialize initialization options: {error}");
|
||||
}
|
||||
|
||||
tracing::debug!("Client info: {client_info:#?}");
|
||||
tracing::debug!("Initialization options: {initialization_options:#?}");
|
||||
|
||||
let resolved_client_capabilities = ResolvedClientCapabilities::new(&client_capabilities);
|
||||
@@ -157,7 +155,6 @@ impl Server {
|
||||
workspace_urls,
|
||||
initialization_options,
|
||||
native_system,
|
||||
ClientName::from(client_info),
|
||||
in_test,
|
||||
)?,
|
||||
})
|
||||
|
||||
@@ -119,12 +119,9 @@ pub(super) fn request(req: server::Request) -> Task {
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::error!("Encountered error when routing request with ID {id}: {err}");
|
||||
|
||||
Task::sync(move |session, client| {
|
||||
Task::sync(move |_session, client| {
|
||||
if matches!(err.code, ErrorCode::InternalError) {
|
||||
client.show_error_message(format!(
|
||||
"ty failed to handle a request from the editor. {}",
|
||||
session.client_name().log_guidance()
|
||||
));
|
||||
client.show_error_message("ty failed to handle a request from the editor. Check the logs for more details.");
|
||||
}
|
||||
|
||||
respond_silent_error(
|
||||
@@ -178,12 +175,11 @@ pub(super) fn notification(notif: server::Notification) -> Task {
|
||||
}
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::error!("Encountered error when routing notification: {err}");
|
||||
Task::sync(move |session, client| {
|
||||
Task::sync(move |_session, client| {
|
||||
if matches!(err.code, ErrorCode::InternalError) {
|
||||
client.show_error_message(format!(
|
||||
"ty failed to handle a notification from the editor. {}",
|
||||
session.client_name().log_guidance()
|
||||
));
|
||||
client.show_error_message(
|
||||
"ty failed to handle a notification from the editor. Check the logs for more details."
|
||||
);
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -197,7 +193,7 @@ where
|
||||
Ok(Task::sync(move |session, client: &Client| {
|
||||
let _span = tracing::debug_span!("request", %id, method = R::METHOD).entered();
|
||||
let result = R::run(session, client, params);
|
||||
respond::<R>(&id, result, client, session.client_name().log_guidance());
|
||||
respond::<R>(&id, result, client);
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -221,7 +217,6 @@ where
|
||||
// SAFETY: The `snapshot` is safe to move across the unwind boundary because it is not used
|
||||
// after unwinding.
|
||||
let snapshot = AssertUnwindSafe(session.snapshot_session());
|
||||
let log_guidance = snapshot.0.client_name().log_guidance();
|
||||
|
||||
Box::new(move |client| {
|
||||
let _span = tracing::debug_span!("request", %id, method = R::METHOD).entered();
|
||||
@@ -243,7 +238,7 @@ where
|
||||
let snapshot = snapshot;
|
||||
R::handle_request(&id, snapshot.0, client, params);
|
||||
}) {
|
||||
panic_response::<R>(&id, client, &error, retry, log_guidance);
|
||||
panic_response::<R>(&id, client, &error, retry);
|
||||
}
|
||||
})
|
||||
}))
|
||||
@@ -289,7 +284,6 @@ where
|
||||
|
||||
let path = document.notebook_or_file_path();
|
||||
let db = session.project_db(path).clone();
|
||||
let log_guidance = document.client_name().log_guidance();
|
||||
|
||||
Box::new(move |client| {
|
||||
let _span = tracing::debug_span!("request", %id, method = R::METHOD).entered();
|
||||
@@ -312,7 +306,7 @@ where
|
||||
R::handle_request(&id, &db, document, client, params);
|
||||
});
|
||||
}) {
|
||||
panic_response::<R>(&id, client, &error, retry, log_guidance);
|
||||
panic_response::<R>(&id, client, &error, retry);
|
||||
}
|
||||
})
|
||||
}))
|
||||
@@ -323,7 +317,6 @@ fn panic_response<R>(
|
||||
client: &Client,
|
||||
error: &PanicError,
|
||||
request: Option<lsp_server::Request>,
|
||||
log_guidance: &str,
|
||||
) where
|
||||
R: traits::RetriableRequestHandler,
|
||||
{
|
||||
@@ -353,7 +346,6 @@ fn panic_response<R>(
|
||||
error: anyhow!("request handler {error}"),
|
||||
}),
|
||||
client,
|
||||
log_guidance,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -366,10 +358,7 @@ fn sync_notification_task<N: traits::SyncNotificationHandler>(
|
||||
let _span = tracing::debug_span!("notification", method = N::METHOD).entered();
|
||||
if let Err(err) = N::run(session, client, params) {
|
||||
tracing::error!("An error occurred while running {id}: {err}");
|
||||
client.show_error_message(format!(
|
||||
"ty encountered a problem. {}",
|
||||
session.client_name().log_guidance()
|
||||
));
|
||||
client.show_error_message("ty encountered a problem. Check the logs for more details.");
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -401,8 +390,6 @@ where
|
||||
return Box::new(|_| {});
|
||||
};
|
||||
|
||||
let log_guidance = snapshot.client_name().log_guidance();
|
||||
|
||||
Box::new(move |client| {
|
||||
let _span = tracing::debug_span!("notification", method = N::METHOD).entered();
|
||||
|
||||
@@ -412,14 +399,18 @@ where
|
||||
Ok(result) => result,
|
||||
Err(panic) => {
|
||||
tracing::error!("An error occurred while running {id}: {panic}");
|
||||
client.show_error_message(format!("ty encountered a panic. {log_guidance}"));
|
||||
client.show_error_message(
|
||||
"ty encountered a panic. Check the logs for more details.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = result {
|
||||
tracing::error!("An error occurred while running {id}: {err}");
|
||||
client.show_error_message(format!("ty encountered a problem. {log_guidance}"));
|
||||
client.show_error_message(
|
||||
"ty encountered a problem. Check the logs for more details.",
|
||||
);
|
||||
}
|
||||
})
|
||||
}))
|
||||
@@ -458,13 +449,12 @@ fn respond<Req>(
|
||||
id: &RequestId,
|
||||
result: Result<<<Req as RequestHandler>::RequestType as Request>::Result>,
|
||||
client: &Client,
|
||||
log_guidance: &str,
|
||||
) where
|
||||
Req: RequestHandler,
|
||||
{
|
||||
if let Err(err) = &result {
|
||||
tracing::error!("An error occurred with request ID {id}: {err}");
|
||||
client.show_error_message(format!("ty encountered a problem. {log_guidance}"));
|
||||
client.show_error_message("ty encountered a problem. Check the logs for more details.");
|
||||
}
|
||||
client.respond(id, result);
|
||||
}
|
||||
|
||||
@@ -116,10 +116,7 @@ pub(super) trait BackgroundDocumentRequestHandler: RetriableRequestHandler {
|
||||
|
||||
if let Err(err) = &result {
|
||||
tracing::error!("An error occurred with request ID {id}: {err}");
|
||||
client.show_error_message(format!(
|
||||
"ty encountered a problem. {}",
|
||||
snapshot.client_name().log_guidance()
|
||||
));
|
||||
client.show_error_message("ty encountered a problem. Check the logs for more details.");
|
||||
}
|
||||
|
||||
client.respond(id, result);
|
||||
@@ -156,10 +153,7 @@ pub(super) trait BackgroundRequestHandler: RetriableRequestHandler {
|
||||
|
||||
if let Err(err) = &result {
|
||||
tracing::error!("An error occurred with request ID {id}: {err}");
|
||||
client.show_error_message(format!(
|
||||
"ty encountered a problem. {}",
|
||||
snapshot.client_name().log_guidance()
|
||||
));
|
||||
client.show_error_message("ty encountered a problem. Check the logs for more details.");
|
||||
}
|
||||
|
||||
client.respond(id, result);
|
||||
|
||||
@@ -13,7 +13,7 @@ use lsp_types::request::{
|
||||
WorkspaceDiagnosticRequest,
|
||||
};
|
||||
use lsp_types::{
|
||||
ClientInfo, DiagnosticRegistrationOptions, DiagnosticServerCapabilities,
|
||||
DiagnosticRegistrationOptions, DiagnosticServerCapabilities,
|
||||
DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, Registration, RegistrationParams,
|
||||
TextDocumentContentChangeEvent, Unregistration, UnregistrationParams, Url,
|
||||
};
|
||||
@@ -106,9 +106,6 @@ pub(crate) struct Session {
|
||||
/// Registrations is a set of LSP methods that have been dynamically registered with the
|
||||
/// client.
|
||||
registrations: HashSet<String>,
|
||||
|
||||
/// The name of the client (editor) that connected to this server.
|
||||
client_name: ClientName,
|
||||
}
|
||||
|
||||
/// LSP State for a Project
|
||||
@@ -144,7 +141,6 @@ impl Session {
|
||||
workspace_urls: Vec<Url>,
|
||||
initialization_options: InitializationOptions,
|
||||
native_system: Arc<dyn System + 'static + Send + Sync + RefUnwindSafe>,
|
||||
client_name: ClientName,
|
||||
in_test: bool,
|
||||
) -> crate::Result<Self> {
|
||||
let index = Arc::new(Index::new());
|
||||
@@ -172,7 +168,6 @@ impl Session {
|
||||
suspended_workspace_diagnostics_request: None,
|
||||
revision: 0,
|
||||
registrations: HashSet::new(),
|
||||
client_name,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -537,8 +532,8 @@ impl Session {
|
||||
);
|
||||
|
||||
client.show_error_message(format!(
|
||||
"Failed to load project for workspace {url}. {}",
|
||||
self.client_name.log_guidance(),
|
||||
"Failed to load project for workspace {url}. \
|
||||
Please refer to the logs for more details.",
|
||||
));
|
||||
|
||||
let db_with_default_settings = ProjectMetadata::from_options(
|
||||
@@ -824,7 +819,6 @@ impl Session {
|
||||
.unwrap_or_else(|| Arc::new(WorkspaceSettings::default())),
|
||||
position_encoding: self.position_encoding,
|
||||
document: document_handle,
|
||||
client_name: self.client_name,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -843,7 +837,6 @@ impl Session {
|
||||
in_test: self.in_test,
|
||||
resolved_client_capabilities: self.resolved_client_capabilities,
|
||||
revision: self.revision,
|
||||
client_name: self.client_name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -983,10 +976,6 @@ impl Session {
|
||||
pub(crate) fn position_encoding(&self) -> PositionEncoding {
|
||||
self.position_encoding
|
||||
}
|
||||
|
||||
pub(crate) fn client_name(&self) -> ClientName {
|
||||
self.client_name
|
||||
}
|
||||
}
|
||||
|
||||
/// A guard that holds the only reference to the index and allows modifying it.
|
||||
@@ -1036,7 +1025,6 @@ pub(crate) struct DocumentSnapshot {
|
||||
workspace_settings: Arc<WorkspaceSettings>,
|
||||
position_encoding: PositionEncoding,
|
||||
document: DocumentHandle,
|
||||
client_name: ClientName,
|
||||
}
|
||||
|
||||
impl DocumentSnapshot {
|
||||
@@ -1083,10 +1071,6 @@ impl DocumentSnapshot {
|
||||
pub(crate) fn notebook_or_file_path(&self) -> &AnySystemPath {
|
||||
self.document.notebook_or_file_path()
|
||||
}
|
||||
|
||||
pub(crate) fn client_name(&self) -> ClientName {
|
||||
self.client_name
|
||||
}
|
||||
}
|
||||
|
||||
/// An immutable snapshot of the current state of [`Session`].
|
||||
@@ -1097,7 +1081,6 @@ pub(crate) struct SessionSnapshot {
|
||||
resolved_client_capabilities: ResolvedClientCapabilities,
|
||||
in_test: bool,
|
||||
revision: u64,
|
||||
client_name: ClientName,
|
||||
|
||||
/// IMPORTANT: It's important that the databases come last, or at least,
|
||||
/// after any `Arc` that we try to extract or mutate in-place using `Arc::into_inner`
|
||||
@@ -1139,42 +1122,6 @@ impl SessionSnapshot {
|
||||
pub(crate) fn revision(&self) -> u64 {
|
||||
self.revision
|
||||
}
|
||||
|
||||
pub(crate) fn client_name(&self) -> ClientName {
|
||||
self.client_name
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the client (editor) that's connected to the language server.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) enum ClientName {
|
||||
Zed,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl From<Option<ClientInfo>> for ClientName {
|
||||
fn from(info: Option<ClientInfo>) -> Self {
|
||||
match info {
|
||||
Some(info) if matches!(info.name.as_str(), "Zed") => ClientName::Zed,
|
||||
_ => ClientName::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientName {
|
||||
/// Returns editor-specific guidance for finding logs.
|
||||
///
|
||||
/// Different editors have different ways to access language server logs, so we provide tailored
|
||||
/// instructions based on the connected client.
|
||||
pub(crate) fn log_guidance(self) -> &'static str {
|
||||
match self {
|
||||
ClientName::Zed => {
|
||||
"Please refer to the logs for more details \
|
||||
(command palette: `dev: open language server logs`)."
|
||||
}
|
||||
ClientName::Other => "Please refer to the logs for more details.",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
||||
@@ -231,11 +231,11 @@ fn discard_todo_metadata(ty: &str) -> Cow<'_, str> {
|
||||
/// Normalize paths in diagnostics to Unix paths before comparing them against
|
||||
/// the expected type. Doing otherwise means that it's hard to write cross-platform
|
||||
/// tests, since in some edge cases the display of a type can include a path to the
|
||||
/// file in which the type was defined (e.g. `foo.bar.A @ src/foo/bar.py:10:5` on Unix,
|
||||
/// but `foo.bar.A @ src\foo\bar.py:10:5` on Windows).
|
||||
/// file in which the type was defined (e.g. `foo.bar.A @ src/foo/bar.py:10` on Unix,
|
||||
/// but `foo.bar.A @ src\foo\bar.py:10` on Windows).
|
||||
fn normalize_paths(ty: &str) -> Cow<'_, str> {
|
||||
static PATH_IN_CLASS_DISPLAY_REGEX: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"( @ )([^\.]+?)(\.pyi?:\d)").unwrap());
|
||||
LazyLock::new(|| regex::Regex::new(r"( @ )(.+)(\.pyi?:\d)").unwrap());
|
||||
|
||||
fn normalize_path_captures(path_captures: ®ex::Captures) -> String {
|
||||
let normalized_path = std::path::Path::new(&path_captures[2])
|
||||
@@ -360,8 +360,6 @@ impl Matcher {
|
||||
return false;
|
||||
};
|
||||
|
||||
let primary_annotation = normalize_paths(primary_annotation);
|
||||
|
||||
// reveal_type, reveal_protocol_interface
|
||||
if matches!(
|
||||
primary_message,
|
||||
|
||||
Reference in New Issue
Block a user