Compare commits
11 Commits
jack/loop-
...
claude/ana
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47f29b9250 | ||
|
|
a697050a83 | ||
|
|
2f64ef9c72 | ||
|
|
fde7d72fbb | ||
|
|
d13b5db066 | ||
|
|
c7b41060f4 | ||
|
|
3878701265 | ||
|
|
6e89e0abff | ||
|
|
cb31883c5f | ||
|
|
6d8f2864c3 | ||
|
|
990d0a8999 |
@@ -101,7 +101,7 @@ pub(crate) fn private_member_access(checker: &Checker, expr: &Expr) {
|
||||
}
|
||||
}
|
||||
|
||||
// Allow some documented private methods, like `os._exit()`.
|
||||
// Allow some public functions whose names start with an underscore, like `os._exit()`.
|
||||
if let Some(qualified_name) = semantic.resolve_qualified_name(expr) {
|
||||
if matches!(qualified_name.segments(), ["os", "_exit"]) {
|
||||
return;
|
||||
|
||||
@@ -3586,7 +3586,7 @@ quux.<CURSOR>
|
||||
__init_subclass__ :: bound method type[Quux].__init_subclass__() -> None
|
||||
__module__ :: str
|
||||
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
|
||||
__new__ :: def __new__(cls) -> Self@__new__
|
||||
__new__ :: def __new__[Self](cls) -> Self
|
||||
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: bound method Quux.__repr__() -> str
|
||||
@@ -3667,19 +3667,19 @@ C.<CURSOR>
|
||||
__mro__ :: tuple[type, ...]
|
||||
__name__ :: str
|
||||
__ne__ :: def __ne__(self, value: object, /) -> bool
|
||||
__new__ :: def __new__(cls) -> Self@__new__
|
||||
__or__ :: bound method <class 'C'>.__or__[Self](value: Any, /) -> UnionType | Self@__or__
|
||||
__new__ :: def __new__[Self](cls) -> Self
|
||||
__or__ :: bound method <class 'C'>.__or__[Self](value: Any, /) -> UnionType | Self
|
||||
__prepare__ :: bound method <class 'Meta'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
|
||||
__qualname__ :: str
|
||||
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: def __repr__(self) -> str
|
||||
__ror__ :: bound method <class 'C'>.__ror__[Self](value: Any, /) -> UnionType | Self@__ror__
|
||||
__ror__ :: bound method <class 'C'>.__ror__[Self](value: Any, /) -> UnionType | Self
|
||||
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
|
||||
__sizeof__ :: def __sizeof__(self) -> int
|
||||
__str__ :: def __str__(self) -> str
|
||||
__subclasscheck__ :: bound method <class 'C'>.__subclasscheck__(subclass: type, /) -> bool
|
||||
__subclasses__ :: bound method <class 'C'>.__subclasses__[Self]() -> list[Self@__subclasses__]
|
||||
__subclasses__ :: bound method <class 'C'>.__subclasses__[Self]() -> list[Self]
|
||||
__subclasshook__ :: bound method <class 'C'>.__subclasshook__(subclass: type, /) -> bool
|
||||
__text_signature__ :: str | None
|
||||
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
||||
@@ -3737,18 +3737,18 @@ Meta.<CURSOR>
|
||||
__mro__ :: tuple[type, ...]
|
||||
__name__ :: str
|
||||
__ne__ :: def __ne__(self, value: object, /) -> bool
|
||||
__or__ :: def __or__[Self](self: Self@__or__, value: Any, /) -> UnionType | Self@__or__
|
||||
__or__ :: def __or__[Self](self: Self, value: Any, /) -> UnionType | Self
|
||||
__prepare__ :: bound method <class 'Meta'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
|
||||
__qualname__ :: str
|
||||
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: def __repr__(self) -> str
|
||||
__ror__ :: def __ror__[Self](self: Self@__ror__, value: Any, /) -> UnionType | Self@__ror__
|
||||
__ror__ :: def __ror__[Self](self: Self, value: Any, /) -> UnionType | Self
|
||||
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
|
||||
__sizeof__ :: def __sizeof__(self) -> int
|
||||
__str__ :: def __str__(self) -> str
|
||||
__subclasscheck__ :: def __subclasscheck__(self, subclass: type, /) -> bool
|
||||
__subclasses__ :: def __subclasses__[Self](self: Self@__subclasses__) -> list[Self@__subclasses__]
|
||||
__subclasses__ :: def __subclasses__[Self](self: Self) -> list[Self]
|
||||
__subclasshook__ :: bound method <class 'Meta'>.__subclasshook__(subclass: type, /) -> bool
|
||||
__text_signature__ :: str | None
|
||||
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
||||
@@ -3866,19 +3866,19 @@ Quux.<CURSOR>
|
||||
__mro__ :: tuple[type, ...]
|
||||
__name__ :: str
|
||||
__ne__ :: def __ne__(self, value: object, /) -> bool
|
||||
__new__ :: def __new__(cls) -> Self@__new__
|
||||
__or__ :: bound method <class 'Quux'>.__or__[Self](value: Any, /) -> UnionType | Self@__or__
|
||||
__new__ :: def __new__[Self](cls) -> Self
|
||||
__or__ :: bound method <class 'Quux'>.__or__[Self](value: Any, /) -> UnionType | Self
|
||||
__prepare__ :: bound method <class 'type'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
|
||||
__qualname__ :: str
|
||||
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
|
||||
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||
__repr__ :: def __repr__(self) -> str
|
||||
__ror__ :: bound method <class 'Quux'>.__ror__[Self](value: Any, /) -> UnionType | Self@__ror__
|
||||
__ror__ :: bound method <class 'Quux'>.__ror__[Self](value: Any, /) -> UnionType | Self
|
||||
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
|
||||
__sizeof__ :: def __sizeof__(self) -> int
|
||||
__str__ :: def __str__(self) -> str
|
||||
__subclasscheck__ :: bound method <class 'Quux'>.__subclasscheck__(subclass: type, /) -> bool
|
||||
__subclasses__ :: bound method <class 'Quux'>.__subclasses__[Self]() -> list[Self@__subclasses__]
|
||||
__subclasses__ :: bound method <class 'Quux'>.__subclasses__[Self]() -> list[Self]
|
||||
__subclasshook__ :: bound method <class 'Quux'>.__subclasshook__(subclass: type, /) -> bool
|
||||
__text_signature__ :: str | None
|
||||
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
||||
@@ -3919,8 +3919,8 @@ Answer.<CURSOR>
|
||||
__bool__ :: bound method <class 'Answer'>.__bool__() -> Literal[True]
|
||||
__class__ :: <class 'EnumMeta'>
|
||||
__contains__ :: bound method <class 'Answer'>.__contains__(value: object) -> bool
|
||||
__copy__ :: def __copy__(self) -> Self@__copy__
|
||||
__deepcopy__ :: def __deepcopy__(self, memo: Any) -> Self@__deepcopy__
|
||||
__copy__ :: def __copy__[Self](self) -> Self
|
||||
__deepcopy__ :: def __deepcopy__[Self](self, memo: Any) -> Self
|
||||
__delattr__ :: def __delattr__(self, name: str, /) -> None
|
||||
__dict__ :: dict[str, Any]
|
||||
__dictoffset__ :: int
|
||||
@@ -3930,34 +3930,34 @@ Answer.<CURSOR>
|
||||
__flags__ :: int
|
||||
__format__ :: def __format__(self, format_spec: str) -> str
|
||||
__getattribute__ :: def __getattribute__(self, name: str, /) -> Any
|
||||
__getitem__ :: bound method <class 'Answer'>.__getitem__[_EnumMemberT](name: str) -> _EnumMemberT@__getitem__
|
||||
__getitem__ :: bound method <class 'Answer'>.__getitem__[_EnumMemberT](name: str) -> _EnumMemberT
|
||||
__getstate__ :: def __getstate__(self) -> object
|
||||
__hash__ :: def __hash__(self) -> int
|
||||
__init__ :: def __init__(self) -> None
|
||||
__init_subclass__ :: bound method <class 'Answer'>.__init_subclass__() -> None
|
||||
__instancecheck__ :: bound method <class 'Answer'>.__instancecheck__(instance: Any, /) -> bool
|
||||
__itemsize__ :: int
|
||||
__iter__ :: bound method <class 'Answer'>.__iter__[_EnumMemberT]() -> Iterator[_EnumMemberT@__iter__]
|
||||
__iter__ :: bound method <class 'Answer'>.__iter__[_EnumMemberT]() -> Iterator[_EnumMemberT]
|
||||
__len__ :: bound method <class 'Answer'>.__len__() -> int
|
||||
__members__ :: MappingProxyType[str, Answer]
|
||||
__module__ :: str
|
||||
__mro__ :: tuple[type, ...]
|
||||
__name__ :: str
|
||||
__ne__ :: def __ne__(self, value: object, /) -> bool
|
||||
__new__ :: def __new__(cls, value: object) -> Self@__new__
|
||||
__or__ :: bound method <class 'Answer'>.__or__[Self](value: Any, /) -> UnionType | Self@__or__
|
||||
__new__ :: def __new__[Self](cls, value: object) -> Self
|
||||
__or__ :: bound method <class 'Answer'>.__or__[Self](value: Any, /) -> UnionType | Self
|
||||
__order__ :: str
|
||||
__prepare__ :: bound method <class 'EnumMeta'>.__prepare__(cls: str, bases: tuple[type, ...], **kwds: Any) -> _EnumDict
|
||||
__qualname__ :: str
|
||||
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
|
||||
__repr__ :: def __repr__(self) -> str
|
||||
__reversed__ :: bound method <class 'Answer'>.__reversed__[_EnumMemberT]() -> Iterator[_EnumMemberT@__reversed__]
|
||||
__ror__ :: bound method <class 'Answer'>.__ror__[Self](value: Any, /) -> UnionType | Self@__ror__
|
||||
__reversed__ :: bound method <class 'Answer'>.__reversed__[_EnumMemberT]() -> Iterator[_EnumMemberT]
|
||||
__ror__ :: bound method <class 'Answer'>.__ror__[Self](value: Any, /) -> UnionType | Self
|
||||
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
|
||||
__sizeof__ :: def __sizeof__(self) -> int
|
||||
__str__ :: def __str__(self) -> str
|
||||
__subclasscheck__ :: bound method <class 'Answer'>.__subclasscheck__(subclass: type, /) -> bool
|
||||
__subclasses__ :: bound method <class 'Answer'>.__subclasses__[Self]() -> list[Self@__subclasses__]
|
||||
__subclasses__ :: bound method <class 'Answer'>.__subclasses__[Self]() -> list[Self]
|
||||
__subclasshook__ :: bound method <class 'Answer'>.__subclasshook__(subclass: type, /) -> bool
|
||||
__text_signature__ :: str | None
|
||||
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
||||
@@ -3999,7 +3999,7 @@ quux.<CURSOR>
|
||||
index :: bound method Quux.index(value: Any, start: SupportsIndex = 0, stop: SupportsIndex = ..., /) -> int
|
||||
x :: int
|
||||
y :: str
|
||||
__add__ :: Overload[(value: tuple[int | str, ...], /) -> tuple[int | str, ...], (value: tuple[_T@__add__, ...], /) -> tuple[int | str | _T@__add__, ...]]
|
||||
__add__ :: Overload[(value: tuple[int | str, ...], /) -> tuple[int | str, ...], [_T](value: tuple[_T, ...], /) -> tuple[int | str | _T, ...]]
|
||||
__annotations__ :: dict[str, Any]
|
||||
__class__ :: type[Quux]
|
||||
__class_getitem__ :: bound method type[Quux].__class_getitem__(item: Any, /) -> GenericAlias
|
||||
|
||||
@@ -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'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:8'>`"
|
||||
# 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'>`"
|
||||
C1.y = "problematic"
|
||||
|
||||
class C2:
|
||||
|
||||
@@ -464,10 +464,51 @@ 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:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
class Base:
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
@@ -480,6 +521,130 @@ class Derived(Base):
|
||||
reveal_type(Derived.custom_attribute) # revealed: int
|
||||
```
|
||||
|
||||
Subclasses must be constructed with arguments matching the required arguments of the base
|
||||
`__init_subclass__` method.
|
||||
|
||||
```py
|
||||
class Empty: ...
|
||||
|
||||
class RequiresArg:
|
||||
def __init_subclass__(cls, arg: int): ...
|
||||
|
||||
class NoArg:
|
||||
def __init_subclass__(cls): ...
|
||||
|
||||
# Single-base definitions
|
||||
class MissingArg(RequiresArg): ... # error: [missing-argument]
|
||||
class InvalidType(RequiresArg, arg="foo"): ... # error: [invalid-argument-type]
|
||||
class Valid(RequiresArg, arg=1): ...
|
||||
|
||||
# error: [missing-argument]
|
||||
# error: [unknown-argument]
|
||||
class IncorrectArg(RequiresArg, not_arg="foo"): ...
|
||||
```
|
||||
|
||||
For multiple inheritance, the first resolved `__init_subclass__` method is used.
|
||||
|
||||
```py
|
||||
class Empty: ...
|
||||
|
||||
class RequiresArg:
|
||||
def __init_subclass__(cls, arg: int): ...
|
||||
|
||||
class NoArg:
|
||||
def __init_subclass__(cls): ...
|
||||
|
||||
class Valid(NoArg, RequiresArg): ...
|
||||
class MissingArg(RequiresArg, NoArg): ... # error: [missing-argument]
|
||||
class InvalidType(RequiresArg, NoArg, arg="foo"): ... # error: [invalid-argument-type]
|
||||
class Valid(RequiresArg, NoArg, arg=1): ...
|
||||
|
||||
# Ensure base class without __init_subclass__ is ignored
|
||||
class Valid(Empty, NoArg): ...
|
||||
class Valid(Empty, RequiresArg, NoArg, arg=1): ...
|
||||
class MissingArg(Empty, RequiresArg): ... # error: [missing-argument]
|
||||
class MissingArg(Empty, RequiresArg, NoArg): ... # error: [missing-argument]
|
||||
class InvalidType(Empty, RequiresArg, NoArg, arg="foo"): ... # error: [invalid-argument-type]
|
||||
|
||||
# Multiple inheritance with args
|
||||
class Base(Empty, RequiresArg, NoArg, arg=1): ...
|
||||
class Valid(Base, arg=1): ...
|
||||
class MissingArg(Base): ... # error: [missing-argument]
|
||||
class InvalidType(Base, arg="foo"): ... # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
Keyword splats are allowed if their type can be determined:
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
class RequiresKwarg:
|
||||
def __init_subclass__(cls, arg: int): ...
|
||||
|
||||
class WrongArg(TypedDict):
|
||||
kwarg: int
|
||||
|
||||
class InvalidType(TypedDict):
|
||||
arg: str
|
||||
|
||||
wrong_arg: WrongArg = {"kwarg": 5}
|
||||
|
||||
# error: [missing-argument]
|
||||
# error: [unknown-argument]
|
||||
class MissingArg(RequiresKwarg, **wrong_arg): ...
|
||||
|
||||
invalid_type: InvalidType = {"arg": "foo"}
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
class InvalidType(RequiresKwarg, **invalid_type): ...
|
||||
```
|
||||
|
||||
So are generics:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar, Literal, overload
|
||||
|
||||
class Base[T]:
|
||||
def __init_subclass__(cls, arg: T): ...
|
||||
|
||||
class Valid(Base[int], arg=1): ...
|
||||
class InvalidType(Base[int], arg="x"): ... # error: [invalid-argument-type]
|
||||
|
||||
# Old generic syntax
|
||||
T = TypeVar("T")
|
||||
|
||||
class Base(Generic[T]):
|
||||
def __init_subclass__(cls, arg: T) -> None: ...
|
||||
|
||||
class Valid(Base[int], arg=1): ...
|
||||
class InvalidType(Base[int], arg="x"): ... # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
So are overloads:
|
||||
|
||||
```py
|
||||
class Base:
|
||||
@overload
|
||||
def __init_subclass__(cls, mode: Literal["a"], arg: int) -> None: ...
|
||||
@overload
|
||||
def __init_subclass__(cls, mode: Literal["b"], arg: str) -> None: ...
|
||||
def __init_subclass__(cls, mode: str, arg: int | str) -> None: ...
|
||||
|
||||
class Valid(Base, mode="a", arg=5): ...
|
||||
class Valid(Base, mode="b", arg="foo"): ...
|
||||
class InvalidType(Base, mode="b", arg=5): ... # error: [no-matching-overload]
|
||||
```
|
||||
|
||||
The `metaclass` keyword is ignored, as it has special meaning and is not passed to
|
||||
`__init_subclass__` at runtime.
|
||||
|
||||
```py
|
||||
class Base:
|
||||
def __init_subclass__(cls, arg: int): ...
|
||||
|
||||
class Valid(Base, arg=5, metaclass=object): ...
|
||||
```
|
||||
|
||||
## `@staticmethod`
|
||||
|
||||
### Basic
|
||||
@@ -621,17 +786,17 @@ argument:
|
||||
```py
|
||||
from typing_extensions import Self
|
||||
|
||||
reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||
reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
reveal_type(object.__new__) # revealed: def __new__[Self](cls) -> Self
|
||||
reveal_type(object().__new__) # revealed: def __new__[Self](cls) -> Self
|
||||
# revealed: Overload[[Self](cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self, [Self](cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self]
|
||||
reveal_type(int.__new__)
|
||||
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||
# revealed: Overload[[Self](cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self, [Self](cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self]
|
||||
reveal_type((42).__new__)
|
||||
|
||||
class X:
|
||||
def __init__(self, val: int): ...
|
||||
def make_another(self) -> Self:
|
||||
reveal_type(self.__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||
reveal_type(self.__new__) # revealed: def __new__[Self](cls) -> Self
|
||||
return self.__new__(type(self))
|
||||
```
|
||||
|
||||
|
||||
@@ -77,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`, found `mdtest_snippet.Foo @ src/mdtest_snippet.py:6`"
|
||||
# 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`"
|
||||
takes_foo1(foo2)
|
||||
# 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`"
|
||||
# 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`"
|
||||
takes_foo2(foo1)
|
||||
```
|
||||
|
||||
@@ -115,17 +115,116 @@ reveal_type(bar.base_attr) # revealed: int
|
||||
reveal_type(bar.mixin_attr) # revealed: str
|
||||
```
|
||||
|
||||
Attributes from the namespace dict (third argument) are not tracked. Like Pyright, we error when
|
||||
attempting to access them:
|
||||
Attributes from the namespace dict (third argument) are tracked:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
Foo = type("Foo", (Base,), {"custom_attr": 42})
|
||||
foo = Foo()
|
||||
|
||||
# error: [unresolved-attribute] "Object of type `Foo` has no attribute `custom_attr`"
|
||||
reveal_type(foo.custom_attr) # revealed: Unknown
|
||||
# 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
|
||||
```
|
||||
|
||||
## Inheritance from dynamic classes
|
||||
@@ -513,7 +612,129 @@ class B(metaclass=Meta2): ...
|
||||
Bad = type("Bad", (A, B), {})
|
||||
```
|
||||
|
||||
## Cyclic dynamic class definitions
|
||||
## `__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
|
||||
|
||||
Self-referential class definitions using `type()` are detected. The name being defined is referenced
|
||||
in the bases tuple before it's available:
|
||||
@@ -541,7 +762,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`, found `mdtest_snippet.<locals of function 'make_classes'>.<unknown> @ src/mdtest_snippet.py:9`"
|
||||
# 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`"
|
||||
inner(cls2())
|
||||
```
|
||||
|
||||
|
||||
@@ -1208,7 +1208,7 @@ def uses_dataclass[T](x: T) -> ChildOfParentDataclass[T]:
|
||||
# revealed: (self: ParentDataclass[Unknown], value: Unknown) -> None
|
||||
reveal_type(ParentDataclass.__init__)
|
||||
|
||||
# revealed: (self: ParentDataclass[T@ChildOfParentDataclass], value: T@ChildOfParentDataclass) -> None
|
||||
# revealed: [T](self: ParentDataclass[T], value: T) -> None
|
||||
reveal_type(ChildOfParentDataclass.__init__)
|
||||
|
||||
result_int = uses_dataclass(42)
|
||||
|
||||
@@ -504,3 +504,94 @@ 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`, found `package.foo.MyClass @ src/package/foo.pyi:1`"
|
||||
# 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`"
|
||||
return make_MyClass()
|
||||
```
|
||||
|
||||
|
||||
@@ -817,7 +817,7 @@ class WithOverloadedMethod(Generic[T]):
|
||||
def method(self, x: S | T) -> S | T:
|
||||
return x
|
||||
|
||||
# revealed: Overload[(self, x: int) -> int, (self, x: S@method) -> S@method | int]
|
||||
# revealed: Overload[(self, x: int) -> int, [S](self, x: S) -> S | int]
|
||||
reveal_type(WithOverloadedMethod[int].method)
|
||||
```
|
||||
|
||||
|
||||
@@ -702,7 +702,7 @@ class WithOverloadedMethod[T]:
|
||||
def method[S](self, x: S | T) -> S | T:
|
||||
return x
|
||||
|
||||
# revealed: Overload[(self, x: int) -> int, (self, x: S@method) -> S@method | int]
|
||||
# revealed: Overload[(self, x: int) -> int, [S](self, x: S) -> S | int]
|
||||
reveal_type(WithOverloadedMethod[int].method)
|
||||
```
|
||||
|
||||
|
||||
@@ -867,7 +867,7 @@ class ClassWithOverloadedInit[T]:
|
||||
# overload. We would then also have to determine that R must be equal to the return type of **P's
|
||||
# solution.
|
||||
|
||||
# revealed: Overload[(x: int) -> ClassWithOverloadedInit[int], (x: str) -> ClassWithOverloadedInit[str]]
|
||||
# revealed: Overload[[T](x: int) -> ClassWithOverloadedInit[int], [T](x: str) -> ClassWithOverloadedInit[str]]
|
||||
reveal_type(into_callable(ClassWithOverloadedInit))
|
||||
# TODO: revealed: Overload[(x: int) -> ClassWithOverloadedInit[int], (x: str) -> ClassWithOverloadedInit[str]]
|
||||
# revealed: Overload[(x: int) -> ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str], (x: str) -> ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str]]
|
||||
@@ -888,7 +888,7 @@ class GenericClass[T]:
|
||||
def _(x: list[str]):
|
||||
# TODO: This fails because we are not propagating GenericClass's generic context into the
|
||||
# Callable that we create for it.
|
||||
# revealed: (x: list[T@GenericClass], y: list[T@GenericClass]) -> GenericClass[T@GenericClass]
|
||||
# revealed: [T](x: list[T], y: list[T]) -> GenericClass[T]
|
||||
reveal_type(into_callable(GenericClass))
|
||||
# revealed: ty_extensions.GenericContext[T@GenericClass]
|
||||
reveal_type(generic_context(into_callable(GenericClass)))
|
||||
|
||||
@@ -153,7 +153,7 @@ already solved and specialized when the class was specialized:
|
||||
from ty_extensions import generic_context
|
||||
|
||||
legacy.m("string", None) # error: [invalid-argument-type]
|
||||
reveal_type(legacy.m) # revealed: bound method Legacy[int].m[S](x: int, y: S@m) -> S@m
|
||||
reveal_type(legacy.m) # revealed: bound method Legacy[int].m[S](x: int, y: S) -> S
|
||||
# revealed: ty_extensions.GenericContext[T@Legacy]
|
||||
reveal_type(generic_context(Legacy))
|
||||
# revealed: ty_extensions.GenericContext[Self@m, S@m]
|
||||
@@ -344,4 +344,27 @@ class C[T]:
|
||||
ok2: Inner[T]
|
||||
```
|
||||
|
||||
## Mixed-scope type parameters
|
||||
|
||||
Methods can have type parameters that are scoped to the method itself, while also referring to type
|
||||
parameters from the enclosing class.
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from ty_extensions import into_callable
|
||||
|
||||
T = TypeVar("T")
|
||||
S = TypeVar("S")
|
||||
|
||||
class Foo(Generic[T]):
|
||||
def bar(self, x: T, y: S) -> tuple[T, S]:
|
||||
raise NotImplementedError
|
||||
|
||||
def f(x: type[Foo[T]]) -> T:
|
||||
# revealed: [S](self, x: T@f, y: S) -> tuple[T@f, S]
|
||||
reveal_type(into_callable(x.bar))
|
||||
raise NotImplementedError
|
||||
```
|
||||
|
||||
[scoping]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables
|
||||
|
||||
@@ -398,10 +398,10 @@ if returns_bool():
|
||||
else:
|
||||
class B(Y, X): ...
|
||||
|
||||
# revealed: (<class 'B'>, <class 'X'>, <class 'Y'>, <class 'O'>, <class 'object'>) | (<class 'B'>, <class 'Y'>, <class 'X'>, <class 'O'>, <class 'object'>)
|
||||
# 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'>)
|
||||
reveal_mro(B)
|
||||
|
||||
# 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'>`"
|
||||
# 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'>`"
|
||||
class Z(A, B): ...
|
||||
|
||||
reveal_mro(Z) # revealed: (<class 'Z'>, Unknown, <class 'object'>)
|
||||
|
||||
@@ -348,7 +348,7 @@ def expects_named_tuple(x: typing.NamedTuple):
|
||||
reveal_type(x) # revealed: tuple[object, ...] & NamedTupleLike
|
||||
reveal_type(x._make) # revealed: bound method type[NamedTupleLike]._make(iterable: Iterable[Any]) -> NamedTupleLike
|
||||
reveal_type(x._replace) # revealed: bound method NamedTupleLike._replace(...) -> NamedTupleLike
|
||||
# revealed: Overload[(value: tuple[object, ...], /) -> tuple[object, ...], (value: tuple[_T@__add__, ...], /) -> tuple[object, ...]]
|
||||
# revealed: Overload[(value: tuple[object, ...], /) -> tuple[object, ...], [_T](value: tuple[_T, ...], /) -> tuple[object, ...]]
|
||||
reveal_type(x.__add__)
|
||||
reveal_type(x.__iter__) # revealed: bound method tuple[object, ...].__iter__() -> Iterator[object]
|
||||
|
||||
|
||||
@@ -12,6 +12,30 @@ 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,6 +121,31 @@ 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,6 +16,32 @@ 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:
|
||||
|
||||
@@ -311,7 +311,7 @@ def func[T](x: T) -> T: ...
|
||||
def func[T](x: T | None = None) -> T | None:
|
||||
return x
|
||||
|
||||
reveal_type(func) # revealed: Overload[() -> None, (x: T@func) -> T@func]
|
||||
reveal_type(func) # revealed: Overload[() -> None, [T](x: T) -> T]
|
||||
reveal_type(func()) # revealed: None
|
||||
reveal_type(func(1)) # revealed: Literal[1]
|
||||
reveal_type(func("")) # revealed: Literal[""]
|
||||
|
||||
@@ -453,3 +453,17 @@ def _(y: Y):
|
||||
if isinstance(y, dict):
|
||||
reveal_type(y) # revealed: dict[str, X] | dict[str, Y]
|
||||
```
|
||||
|
||||
### Recursive alias with tuple - stack overflow test (issue 2470)
|
||||
|
||||
This test case used to cause a stack overflow. The returned type `list[int]` is not assignable to
|
||||
`RecursiveT = int | tuple[RecursiveT, ...]`, so we get an error.
|
||||
|
||||
```py
|
||||
type RecursiveT = int | tuple[RecursiveT, ...]
|
||||
|
||||
def foo(a: int, b: int) -> RecursiveT:
|
||||
some_intermediate_var = (a, b)
|
||||
# error: [invalid-return-type] "Return type does not match returned value: expected `RecursiveT`, found `list[int]`"
|
||||
return list(some_intermediate_var)
|
||||
```
|
||||
|
||||
@@ -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 | mdtest_snippet.A @ src/mdtest_snippet.py:13` is not assignable to `mdtest_snippet.A @ src/mdtest_snippet.py:13`"
|
||||
# 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`"
|
||||
x = A()
|
||||
```
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ def _(flag: bool) -> None:
|
||||
abs = 1
|
||||
chr: int = 1
|
||||
|
||||
reveal_type(abs) # revealed: Literal[1] | (def abs[_T](x: SupportsAbs[_T@abs], /) -> _T@abs)
|
||||
reveal_type(abs) # revealed: Literal[1] | (def abs[_T](x: SupportsAbs[_T], /) -> _T)
|
||||
reveal_type(chr) # revealed: Literal[1] | (def chr(i: SupportsIndex, /) -> str)
|
||||
```
|
||||
|
||||
|
||||
@@ -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'> | <class 'mdtest_snippet.<locals of function '_'>.C1 @ src/mdtest_snippet.py:7'>`
|
||||
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'>`
|
||||
--> src/mdtest_snippet.py:11:5
|
||||
|
|
||||
10 | # TODO: The error message here could be improved to explain why the assignment fails.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
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'> | <class 'mdtest_snippet.<locals of function 'f'>.C @ src/mdtest_snippet.py:25'>`
|
||||
| ^ 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'>`
|
||||
|
|
||||
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'> | <class 'mdtest_snippet.<locals of function 'f'>.A @ src/mdtest_snippet.py:8'>`
|
||||
| ^^^^^^^^^^^^^ 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'>`
|
||||
|
|
||||
info: rule `invalid-super-argument` is enabled by default
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
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
|
||||
|
||||
```
|
||||
@@ -200,7 +200,7 @@ info: Type variable defined here
|
||||
| ^^^^^^
|
||||
14 | return 0
|
||||
|
|
||||
info: Union variant `def f4[T](x: T@f4) -> int` is incompatible with this call site
|
||||
info: Union variant `def f4[T](x: T) -> int` is incompatible with this call site
|
||||
info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | ... omitted 5 union elements`
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ use crate::types::generics::{
|
||||
ApplySpecialization, InferableTypeVars, Specialization, SpecializationBuilder, bind_typevar,
|
||||
typing_self, walk_generic_context,
|
||||
};
|
||||
use crate::types::mro::{Mro, MroError, MroIterator};
|
||||
use crate::types::mro::{Mro, MroIterator, StaticMroError};
|
||||
pub(crate) use crate::types::narrow::{NarrowingConstraint, infer_narrowing_constraint};
|
||||
use crate::types::newtype::NewType;
|
||||
pub(crate) use crate::types::signatures::{Parameter, Parameters};
|
||||
@@ -899,6 +899,16 @@ impl<'db> Type<'db> {
|
||||
matches!(self, Type::Never)
|
||||
}
|
||||
|
||||
/// Returns `true` if this type contains a `Self` type variable.
|
||||
pub(crate) fn contains_self(&self, db: &'db dyn Db) -> bool {
|
||||
any_over_type(
|
||||
db,
|
||||
*self,
|
||||
&|ty| matches!(ty, Type::TypeVar(tv) if tv.typevar(db).is_self(db)),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns `true` if `self` is [`Type::Callable`].
|
||||
pub(crate) const fn is_callable_type(&self) -> bool {
|
||||
matches!(self, Type::Callable(..))
|
||||
@@ -2645,8 +2655,15 @@ 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(instance))),
|
||||
Type::Callable(callable.bind_self(db, Some(self_type))),
|
||||
AttributeKind::NormalOrNonDataDescriptor,
|
||||
))
|
||||
};
|
||||
@@ -6099,6 +6116,9 @@ impl<'db> Type<'db> {
|
||||
Type::TypeGuard(type_guard) => type_guard.with_type(db, type_guard.return_type(db).apply_type_mapping(db, type_mapping, tcx)),
|
||||
|
||||
Type::TypeAlias(alias) => {
|
||||
// For EagerExpansion, expand the raw value type. This path relies on Salsa's cycle
|
||||
// detection rather than the visitor's cycle detection, because the visitor tracks
|
||||
// Type values and `RecursiveList` is different from `RecursiveList[T]`.
|
||||
if TypeMapping::EagerExpansion == *type_mapping {
|
||||
return alias.raw_value_type(db).expand_eagerly(db);
|
||||
}
|
||||
@@ -6106,17 +6126,27 @@ impl<'db> Type<'db> {
|
||||
// Do not call `value_type` here. `value_type` does the specialization internally, so `apply_type_mapping` is
|
||||
// performed without `visitor` inheritance. In the case of recursive type aliases, this leads to infinite recursion.
|
||||
// Instead, call `raw_value_type` and perform the specialization after the `visitor` cache has been created.
|
||||
let value_type = visitor.visit(self, || {
|
||||
//
|
||||
// IMPORTANT: All processing must happen inside a single visitor.visit() call so that if we encounter
|
||||
// this same TypeAlias again (e.g., in `type RecursiveT = int | tuple[RecursiveT, ...]`), the visitor
|
||||
// will detect the cycle and return the fallback value.
|
||||
let mapped = visitor.visit(self, || {
|
||||
match type_mapping {
|
||||
// We only want to perform the unique specialization onto the specialization of the type alias below,
|
||||
// not the raw value type.
|
||||
TypeMapping::UniqueSpecialization { .. } => alias.raw_value_type(db),
|
||||
TypeMapping::EagerExpansion => unreachable!("handled above"),
|
||||
|
||||
_ => alias.raw_value_type(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor),
|
||||
// For UniqueSpecialization, get raw value type, apply specialization, then apply mapping.
|
||||
TypeMapping::UniqueSpecialization { .. } => {
|
||||
let value_type = alias.raw_value_type(db);
|
||||
alias.apply_function_specialization(db, value_type).apply_type_mapping_impl(db, type_mapping, tcx, visitor)
|
||||
}
|
||||
|
||||
_ => {
|
||||
let value_type = alias.raw_value_type(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor);
|
||||
alias.apply_function_specialization(db, value_type).apply_type_mapping_impl(db, type_mapping, tcx, visitor)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mapped = alias.apply_function_specialization(db, value_type).apply_type_mapping_impl(db, type_mapping, tcx, visitor);
|
||||
let is_recursive = any_over_type(db, alias.raw_value_type(db).expand_eagerly(db), &|ty| ty.is_divergent(), false);
|
||||
|
||||
// If the type mapping does not result in any change to this (non-recursive) type alias, do not expand it.
|
||||
@@ -6893,8 +6923,26 @@ impl<'db> TypeMapping<'_, 'db> {
|
||||
context: GenericContext<'db>,
|
||||
) -> GenericContext<'db> {
|
||||
match self {
|
||||
TypeMapping::ApplySpecialization(_)
|
||||
| TypeMapping::UniqueSpecialization { .. }
|
||||
TypeMapping::ApplySpecialization(specialization) => {
|
||||
// Filter out type variables that are already specialized
|
||||
// (i.e., mapped to a non-TypeVar type)
|
||||
GenericContext::from_typevar_instances(
|
||||
db,
|
||||
context.variables(db).filter(|bound_typevar| {
|
||||
// Keep the type variable if it's not in the specialization
|
||||
// or if it's mapped to itself (still a TypeVar)
|
||||
match specialization.get(db, *bound_typevar) {
|
||||
None => true,
|
||||
Some(Type::TypeVar(mapped_typevar)) => {
|
||||
// Still a TypeVar, keep it if it's mapping to itself
|
||||
mapped_typevar.identity(db) == bound_typevar.identity(db)
|
||||
}
|
||||
Some(_) => false, // Specialized to a concrete type, filter out
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
TypeMapping::UniqueSpecialization { .. }
|
||||
| TypeMapping::PromoteLiterals(_)
|
||||
| TypeMapping::BindLegacyTypevars(_)
|
||||
| TypeMapping::Materialize(_)
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::sync::{LazyLock, Mutex};
|
||||
|
||||
use super::TypeVarVariance;
|
||||
use super::{
|
||||
BoundTypeVarInstance, MemberLookupPolicy, Mro, MroError, MroIterator, SpecialFormType,
|
||||
BoundTypeVarInstance, MemberLookupPolicy, Mro, MroIterator, SpecialFormType, StaticMroError,
|
||||
SubclassOfType, Truthiness, Type, TypeQualifiers, class_base::ClassBase,
|
||||
function::FunctionType,
|
||||
};
|
||||
@@ -128,8 +128,8 @@ fn try_mro_cycle_initial<'db>(
|
||||
_id: salsa::Id,
|
||||
self_: StaticClassLiteral<'db>,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> Result<Mro<'db>, MroError<'db>> {
|
||||
Err(MroError::cycle(
|
||||
) -> Result<Mro<'db>, StaticMroError<'db>> {
|
||||
Err(StaticMroError::cycle(
|
||||
db,
|
||||
self_.apply_optional_specialization(db, specialization),
|
||||
))
|
||||
@@ -643,13 +643,17 @@ 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.
|
||||
// TODO: A dynamic class could provide ordering methods in the namespace dictionary:
|
||||
// ```python
|
||||
// >>> X = type("X", (), {"__lt__": lambda self, other: True})
|
||||
// ```
|
||||
///
|
||||
/// For dynamic classes, this checks if any ordering methods are provided 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 {
|
||||
self.as_static()
|
||||
.is_some_and(|class| class.has_own_ordering_method(db))
|
||||
match self {
|
||||
Self::Static(class) => class.has_own_ordering_method(db),
|
||||
Self::Dynamic(class) => class.has_own_ordering_method(db),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the static class definition if this is one.
|
||||
@@ -704,20 +708,27 @@ impl<'db> ClassLiteral<'db> {
|
||||
}
|
||||
|
||||
/// Returns whether this class is a disjoint base.
|
||||
// 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
|
||||
// ```
|
||||
///
|
||||
/// 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
|
||||
/// ```
|
||||
pub(super) fn as_disjoint_base(self, db: &'db dyn Db) -> Option<DisjointBase<'db>> {
|
||||
self.as_static()
|
||||
.and_then(|class| class.as_disjoint_base(db))
|
||||
match self {
|
||||
Self::Static(class) => class.as_disjoint_base(db),
|
||||
Self::Dynamic(class) => class.as_disjoint_base(db),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a non-generic instance of this class.
|
||||
@@ -1323,8 +1334,12 @@ impl<'db> ClassType<'db> {
|
||||
Signature::new(parameters, return_annotation)
|
||||
}
|
||||
|
||||
let Some((class_literal, specialization)) = self.static_class_literal(db) else {
|
||||
return Member::unbound();
|
||||
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 fallback_member_lookup = || {
|
||||
@@ -1637,12 +1652,21 @@ 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> {
|
||||
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))
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a callable type (or union of callable types) that represents the callable
|
||||
@@ -2111,16 +2135,27 @@ impl<'db> StaticClassLiteral<'db> {
|
||||
let Some(base_class) = base.into_class() else {
|
||||
continue;
|
||||
};
|
||||
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));
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2376,7 +2411,9 @@ impl<'db> StaticClassLiteral<'db> {
|
||||
{
|
||||
Some(DisjointBase::due_to_decorator(self))
|
||||
} else if SlotsKind::from(db, self) == SlotsKind::NotEmpty {
|
||||
Some(DisjointBase::due_to_dunder_slots(self))
|
||||
Some(DisjointBase::due_to_dunder_slots(ClassLiteral::Static(
|
||||
self,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -2498,7 +2535,7 @@ impl<'db> StaticClassLiteral<'db> {
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> Result<Mro<'db>, MroError<'db>> {
|
||||
) -> Result<Mro<'db>, StaticMroError<'db>> {
|
||||
tracing::trace!("StaticClassLiteral::try_mro: {}", self.name(db));
|
||||
Mro::of_static_class(db, self, specialization)
|
||||
}
|
||||
@@ -2605,7 +2642,7 @@ impl<'db> StaticClassLiteral<'db> {
|
||||
return Ok((SubclassOfType::subclass_of_unknown(), None));
|
||||
}
|
||||
|
||||
if self.try_mro(db, None).is_err_and(MroError::is_cycle) {
|
||||
if self.try_mro(db, None).is_err_and(StaticMroError::is_cycle) {
|
||||
return Ok((SubclassOfType::subclass_of_unknown(), None));
|
||||
}
|
||||
|
||||
@@ -4665,18 +4702,8 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
|
||||
/// - name: "Foo"
|
||||
/// - bases: [Base]
|
||||
///
|
||||
/// # 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.
|
||||
/// This is called "dynamic" because the class is created dynamically at runtime
|
||||
/// via a function call rather than a class statement.
|
||||
///
|
||||
/// # Salsa interning
|
||||
///
|
||||
@@ -4709,6 +4736,16 @@ pub struct DynamicClassLiteral<'db> {
|
||||
/// The definition where this class is created.
|
||||
pub definition: Definition<'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,
|
||||
|
||||
/// Dataclass parameters if this class has been wrapped with `@dataclass` decorator
|
||||
/// or passed to `dataclass()` as a function.
|
||||
pub dataclass_params: Option<DataclassParams<'db>>,
|
||||
@@ -4900,6 +4937,29 @@ 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
|
||||
@@ -4909,6 +4969,50 @@ 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,
|
||||
@@ -4920,6 +5024,8 @@ impl<'db> DynamicClassLiteral<'db> {
|
||||
self.name(db).clone(),
|
||||
self.bases(db),
|
||||
self.definition(db),
|
||||
self.members(db),
|
||||
self.has_dynamic_namespace(db),
|
||||
dataclass_params,
|
||||
)
|
||||
}
|
||||
@@ -5296,7 +5402,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: StaticClassLiteral<'db>,
|
||||
pub(super) class: ClassLiteral<'db>,
|
||||
pub(super) kind: DisjointBaseKind,
|
||||
}
|
||||
|
||||
@@ -5305,14 +5411,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,
|
||||
class: ClassLiteral::Static(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: StaticClassLiteral<'db>) -> Self {
|
||||
fn due_to_dunder_slots(class: ClassLiteral<'db>) -> Self {
|
||||
Self {
|
||||
class,
|
||||
kind: DisjointBaseKind::DefinesSlots,
|
||||
@@ -5324,10 +5430,12 @@ impl<'db> DisjointBase<'db> {
|
||||
self == other
|
||||
|| self
|
||||
.class
|
||||
.is_subclass_of(db, None, other.class.default_specialization(db))
|
||||
.default_specialization(db)
|
||||
.is_subclass_of(db, other.class.default_specialization(db))
|
||||
|| other
|
||||
.class
|
||||
.is_subclass_of(db, None, self.class.default_specialization(db))
|
||||
.default_specialization(db)
|
||||
.is_subclass_of(db, 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, MroError, NormalizedVisitor, SpecialFormType, Type, TypeContext,
|
||||
MaterializationKind, NormalizedVisitor, SpecialFormType, StaticMroError, Type, TypeContext,
|
||||
TypeMapping, todo_type,
|
||||
};
|
||||
|
||||
@@ -381,7 +381,7 @@ impl<'db> ClassBase<'db> {
|
||||
};
|
||||
class_literal
|
||||
.try_mro(db, specialization)
|
||||
.is_err_and(MroError::is_cycle)
|
||||
.is_err_and(StaticMroError::is_cycle)
|
||||
}
|
||||
ClassBase::Dynamic(_)
|
||||
| ClassBase::Generic
|
||||
@@ -408,16 +408,27 @@ 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(self.db).fmt(f),
|
||||
ClassBase::Class(class) => Type::from(class)
|
||||
.display_with(self.db, self.settings.clone())
|
||||
.fmt(f),
|
||||
ClassBase::Protocol => f.write_str("typing.Protocol"),
|
||||
ClassBase::Generic => f.write_str("typing.Generic"),
|
||||
ClassBase::TypedDict => f.write_str("typing.TypedDict"),
|
||||
@@ -425,7 +436,11 @@ impl<'db> ClassBase<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
ClassBaseDisplay { db, base: self }
|
||||
ClassBaseDisplay {
|
||||
db,
|
||||
base: self,
|
||||
settings: display_settings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3009,16 +3009,15 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast:
|
||||
|
||||
pub(crate) fn report_instance_layout_conflict(
|
||||
context: &InferContext,
|
||||
class: StaticClassLiteral,
|
||||
node: &ast::StmtClassDef,
|
||||
header_range: TextRange,
|
||||
base_nodes: Option<&[ast::Expr]>,
|
||||
disjoint_bases: &IncompatibleBases,
|
||||
) {
|
||||
debug_assert!(disjoint_bases.len() > 1);
|
||||
|
||||
let db = context.db();
|
||||
|
||||
let Some(builder) = context.report_lint(&INSTANCE_LAYOUT_CONFLICT, class.header_range(db))
|
||||
else {
|
||||
let Some(builder) = context.report_lint(&INSTANCE_LAYOUT_CONFLICT, header_range) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -3042,9 +3041,14 @@ pub(crate) fn report_instance_layout_conflict(
|
||||
originating_base,
|
||||
} = disjoint_base_info;
|
||||
|
||||
let span = context.span(&node.bases()[*node_index]);
|
||||
// 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 mut annotation = Annotation::secondary(span.clone());
|
||||
if originating_base.as_static() == Some(disjoint_base.class) {
|
||||
if *originating_base == disjoint_base.class {
|
||||
match disjoint_base.kind {
|
||||
DisjointBaseKind::DefinesSlots => {
|
||||
annotation = annotation.message(format_args!(
|
||||
@@ -3168,11 +3172,10 @@ impl<'db> IncompatibleBases<'db> {
|
||||
.keys()
|
||||
.filter(|other_base| other_base != disjoint_base)
|
||||
.all(|other_base| {
|
||||
!disjoint_base.class.is_subclass_of(
|
||||
db,
|
||||
None,
|
||||
other_base.class.default_specialization(db),
|
||||
)
|
||||
!disjoint_base
|
||||
.class
|
||||
.default_specialization(db)
|
||||
.is_subclass_of(db, other_base.class.default_specialization(db))
|
||||
})
|
||||
})
|
||||
.map(|(base, info)| (*base, *info))
|
||||
|
||||
@@ -7,9 +7,10 @@ use std::fmt::{self, Display, Formatter, Write};
|
||||
use std::rc::Rc;
|
||||
|
||||
use ruff_db::files::FilePath;
|
||||
use ruff_db::source::line_index;
|
||||
use ruff_db::source::{line_index, source_text};
|
||||
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};
|
||||
|
||||
@@ -25,10 +26,10 @@ use crate::types::signatures::{
|
||||
use crate::types::tuple::TupleSpec;
|
||||
use crate::types::visitor::TypeVisitor;
|
||||
use crate::types::{
|
||||
BoundTypeVarIdentity, CallableType, CallableTypeKind, IntersectionType, KnownBoundMethodType,
|
||||
KnownClass, KnownInstanceType, MaterializationKind, Protocol, ProtocolInstanceType,
|
||||
SpecialFormType, StringLiteralType, SubclassOfInner, Type, TypeGuardLike, TypedDictType,
|
||||
UnionType, WrapperDescriptorKind, visitor,
|
||||
BindingContext, BoundTypeVarIdentity, CallableType, CallableTypeKind, IntersectionType,
|
||||
KnownBoundMethodType, KnownClass, KnownInstanceType, MaterializationKind, Protocol,
|
||||
ProtocolInstanceType, SpecialFormType, StringLiteralType, SubclassOfInner, Type, TypeGuardLike,
|
||||
TypedDictType, UnionType, WrapperDescriptorKind, visitor,
|
||||
};
|
||||
|
||||
/// Settings for displaying types and signatures
|
||||
@@ -44,6 +45,10 @@ pub struct DisplaySettings<'db> {
|
||||
/// Disallow Signature printing to introduce a name
|
||||
/// (presumably because we rendered one already)
|
||||
pub disallow_signature_name: bool,
|
||||
/// Scopes that are currently active in the display context (e.g. function scopes
|
||||
/// whose type parameters are currently being displayed).
|
||||
/// Used to suppress redundant `@{scope}` suffixes for type variables.
|
||||
pub active_scopes: Rc<FxHashSet<Definition<'db>>>,
|
||||
}
|
||||
|
||||
impl<'db> DisplaySettings<'db> {
|
||||
@@ -87,6 +92,16 @@ impl<'db> DisplaySettings<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_active_scopes(&self, scopes: impl IntoIterator<Item = Definition<'db>>) -> Self {
|
||||
let mut active_scopes = (*self.active_scopes).clone();
|
||||
active_scopes.extend(scopes);
|
||||
Self {
|
||||
active_scopes: Rc::new(active_scopes),
|
||||
..self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_possibly_ambiguous_types<I, T>(db: &'db dyn Db, types: I) -> Self
|
||||
where
|
||||
@@ -581,9 +596,10 @@ impl<'db> FmtDetailed<'db> for ClassDisplay<'db> {
|
||||
FilePath::Vendored(_) | FilePath::SystemVirtual(_) => Cow::Borrowed(path),
|
||||
};
|
||||
let line_index = line_index(self.db, file);
|
||||
let line_number = line_index.line_index(class_offset);
|
||||
let LineColumn { line, column } =
|
||||
line_index.line_column(class_offset, &source_text(self.db, file));
|
||||
f.set_invalid_type_annotation();
|
||||
write!(f, " @ {path}:{line_number}")?;
|
||||
write!(f, " @ {path}:{line}:{column}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -751,7 +767,9 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> {
|
||||
write!(
|
||||
f.with_type(Type::TypeVar(bound_typevar)),
|
||||
"{}",
|
||||
bound_typevar.identity(self.db).display(self.db)
|
||||
bound_typevar
|
||||
.identity(self.db)
|
||||
.display_with(self.db, self.settings.clone())
|
||||
)?;
|
||||
f.write_char(']')
|
||||
}
|
||||
@@ -776,10 +794,14 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> {
|
||||
|
||||
match function.signature(self.db).overloads.as_slice() {
|
||||
[signature] => {
|
||||
let bound_signature = signature.bind_self(self.db, Some(typing_self_ty));
|
||||
let hide_unused_self =
|
||||
bound_signature.should_hide_self_from_display(self.db);
|
||||
let type_parameters = DisplayOptionalGenericContext {
|
||||
generic_context: signature.generic_context.as_ref(),
|
||||
generic_context: bound_signature.generic_context.as_ref(),
|
||||
db: self.db,
|
||||
settings: self.settings.clone(),
|
||||
hide_unused_self,
|
||||
};
|
||||
f.set_invalid_type_annotation();
|
||||
f.write_str("bound method ")?;
|
||||
@@ -789,8 +811,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> {
|
||||
f.write_char('.')?;
|
||||
f.with_type(self.ty).write_str(function.name(self.db))?;
|
||||
type_parameters.fmt_detailed(f)?;
|
||||
signature
|
||||
.bind_self(self.db, Some(typing_self_ty))
|
||||
bound_signature
|
||||
.display_with(self.db, self.settings.disallow_signature_name())
|
||||
.fmt_detailed(f)
|
||||
}
|
||||
@@ -982,7 +1003,13 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> {
|
||||
}
|
||||
Type::TypeVar(bound_typevar) => {
|
||||
f.set_invalid_type_annotation();
|
||||
write!(f, "{}", bound_typevar.identity(self.db).display(self.db))
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
bound_typevar
|
||||
.identity(self.db)
|
||||
.display_with(self.db, self.settings.clone())
|
||||
)
|
||||
}
|
||||
Type::AlwaysTruthy => f.with_type(self.ty).write_str("AlwaysTruthy"),
|
||||
Type::AlwaysFalsy => f.with_type(self.ty).write_str("AlwaysFalsy"),
|
||||
@@ -1047,6 +1074,19 @@ impl<'db> BoundTypeVarIdentity<'db> {
|
||||
DisplayBoundTypeVarIdentity {
|
||||
bound_typevar_identity: self,
|
||||
db,
|
||||
settings: DisplaySettings::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn display_with(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
settings: DisplaySettings<'db>,
|
||||
) -> impl Display {
|
||||
DisplayBoundTypeVarIdentity {
|
||||
bound_typevar_identity: self,
|
||||
db,
|
||||
settings,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1054,13 +1094,18 @@ impl<'db> BoundTypeVarIdentity<'db> {
|
||||
struct DisplayBoundTypeVarIdentity<'db> {
|
||||
bound_typevar_identity: BoundTypeVarIdentity<'db>,
|
||||
db: &'db dyn Db,
|
||||
settings: DisplaySettings<'db>,
|
||||
}
|
||||
|
||||
impl Display for DisplayBoundTypeVarIdentity<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.bound_typevar_identity.identity.name(self.db))?;
|
||||
if let Some(binding_context) = self.bound_typevar_identity.binding_context.name(self.db) {
|
||||
write!(f, "@{binding_context}")?;
|
||||
let binding_context = self.bound_typevar_identity.binding_context;
|
||||
if let Some(binding_context_name) = binding_context.name(self.db)
|
||||
&& let Some(definition) = binding_context.definition()
|
||||
&& !self.settings.active_scopes.contains(&definition)
|
||||
{
|
||||
write!(f, "@{binding_context_name}")?;
|
||||
}
|
||||
if let Some(paramspec_attr) = self.bound_typevar_identity.paramspec_attr {
|
||||
write!(f, ".{paramspec_attr}")?;
|
||||
@@ -1191,10 +1236,12 @@ pub(crate) struct DisplayOverloadLiteral<'db> {
|
||||
impl<'db> FmtDetailed<'db> for DisplayOverloadLiteral<'db> {
|
||||
fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result {
|
||||
let signature = self.literal.signature(self.db);
|
||||
let hide_unused_self = signature.should_hide_self_from_display(self.db);
|
||||
let type_parameters = DisplayOptionalGenericContext {
|
||||
generic_context: signature.generic_context.as_ref(),
|
||||
db: self.db,
|
||||
settings: self.settings.clone(),
|
||||
hide_unused_self,
|
||||
};
|
||||
|
||||
f.set_invalid_type_annotation();
|
||||
@@ -1239,10 +1286,13 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> {
|
||||
|
||||
match signature.overloads.as_slice() {
|
||||
[signature] => {
|
||||
let hide_unused_self = signature.should_hide_self_from_display(self.db);
|
||||
|
||||
let type_parameters = DisplayOptionalGenericContext {
|
||||
generic_context: signature.generic_context.as_ref(),
|
||||
db: self.db,
|
||||
settings: self.settings.clone(),
|
||||
hide_unused_self,
|
||||
};
|
||||
f.set_invalid_type_annotation();
|
||||
f.write_str("def ")?;
|
||||
@@ -1359,6 +1409,7 @@ impl<'db> GenericContext<'db> {
|
||||
db,
|
||||
settings: DisplaySettings::default(),
|
||||
full: true,
|
||||
hide_unused_self: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1372,6 +1423,7 @@ impl<'db> GenericContext<'db> {
|
||||
db,
|
||||
settings,
|
||||
full: false,
|
||||
hide_unused_self: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1380,14 +1432,22 @@ struct DisplayOptionalGenericContext<'a, 'db> {
|
||||
generic_context: Option<&'a GenericContext<'db>>,
|
||||
db: &'db dyn Db,
|
||||
settings: DisplaySettings<'db>,
|
||||
/// If true, hide `Self` type variables from the generic context prefix
|
||||
/// when they are not displayed in the signature body.
|
||||
hide_unused_self: bool,
|
||||
}
|
||||
|
||||
impl<'db> FmtDetailed<'db> for DisplayOptionalGenericContext<'_, 'db> {
|
||||
fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result {
|
||||
if let Some(generic_context) = self.generic_context {
|
||||
generic_context
|
||||
.display_with(self.db, self.settings.clone())
|
||||
.fmt_detailed(f)
|
||||
DisplayGenericContext {
|
||||
generic_context,
|
||||
db: self.db,
|
||||
settings: self.settings.clone(),
|
||||
full: false,
|
||||
hide_unused_self: self.hide_unused_self,
|
||||
}
|
||||
.fmt_detailed(f)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
@@ -1406,22 +1466,27 @@ pub struct DisplayGenericContext<'a, 'db> {
|
||||
#[expect(dead_code)]
|
||||
settings: DisplaySettings<'db>,
|
||||
full: bool,
|
||||
/// If true, hide `Self` type variables from the generic context prefix.
|
||||
hide_unused_self: bool,
|
||||
}
|
||||
|
||||
impl<'db> DisplayGenericContext<'_, 'db> {
|
||||
fn fmt_normal(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result {
|
||||
let variables = self.generic_context.variables(self.db);
|
||||
let mut variables = self
|
||||
.generic_context
|
||||
.variables(self.db)
|
||||
.filter(|bound_typevar| {
|
||||
// If hide_unused_self is true and this is a Self typevar, skip it
|
||||
!self.hide_unused_self || !bound_typevar.typevar(self.db).is_self(self.db)
|
||||
})
|
||||
.peekable();
|
||||
|
||||
let non_implicit_variables: Vec<_> = variables
|
||||
.filter(|bound_typevar| !bound_typevar.typevar(self.db).is_self(self.db))
|
||||
.collect();
|
||||
|
||||
if non_implicit_variables.is_empty() {
|
||||
if variables.peek().is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
f.write_char('[')?;
|
||||
for (idx, bound_typevar) in non_implicit_variables.iter().enumerate() {
|
||||
for (idx, bound_typevar) in variables.enumerate() {
|
||||
if idx > 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
@@ -1431,12 +1496,14 @@ impl<'db> DisplayGenericContext<'_, 'db> {
|
||||
f.write_str("**")?;
|
||||
}
|
||||
write!(
|
||||
f.with_type(Type::TypeVar(*bound_typevar)),
|
||||
f.with_type(Type::TypeVar(bound_typevar)),
|
||||
"{}",
|
||||
typevar.name(self.db)
|
||||
)?;
|
||||
}
|
||||
f.write_char(']')
|
||||
f.write_char(']')?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fmt_full(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result {
|
||||
@@ -1676,6 +1743,7 @@ impl<'db> Signature<'db> {
|
||||
) -> DisplaySignature<'a, 'db> {
|
||||
DisplaySignature {
|
||||
definition: self.definition(),
|
||||
generic_context: self.generic_context.as_ref(),
|
||||
parameters: self.parameters(),
|
||||
return_ty: self.return_ty,
|
||||
db,
|
||||
@@ -1686,13 +1754,14 @@ impl<'db> Signature<'db> {
|
||||
|
||||
pub(crate) struct DisplaySignature<'a, 'db> {
|
||||
definition: Option<Definition<'db>>,
|
||||
generic_context: Option<&'a GenericContext<'db>>,
|
||||
parameters: &'a Parameters<'db>,
|
||||
return_ty: Type<'db>,
|
||||
db: &'db dyn Db,
|
||||
settings: DisplaySettings<'db>,
|
||||
}
|
||||
|
||||
impl DisplaySignature<'_, '_> {
|
||||
impl<'db> DisplaySignature<'_, 'db> {
|
||||
/// Get detailed display information including component ranges
|
||||
pub(crate) fn to_string_parts(&self) -> SignatureDisplayDetails {
|
||||
let mut f = TypeWriter::Details(TypeDetailsWriter::new());
|
||||
@@ -1703,6 +1772,14 @@ impl DisplaySignature<'_, '_> {
|
||||
TypeWriter::Formatter(_) => unreachable!("Expected Details variant"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn should_hide_self_from_display(&self, db: &'db dyn Db) -> bool {
|
||||
!self.return_ty.contains_self(db)
|
||||
&& !self
|
||||
.parameters
|
||||
.iter()
|
||||
.any(|p| p.should_annotation_be_displayed() && p.annotated_type().contains_self(db))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> {
|
||||
@@ -1728,15 +1805,41 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> {
|
||||
f.write_str(&name)?;
|
||||
}
|
||||
|
||||
let settings = if let Some(generic_context) = self.generic_context {
|
||||
self.settings
|
||||
.with_active_scopes(generic_context.variables(self.db).filter_map(|bound| {
|
||||
match bound.binding_context(self.db) {
|
||||
BindingContext::Definition(def) => Some(def),
|
||||
BindingContext::Synthetic => None,
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
self.settings.clone()
|
||||
};
|
||||
|
||||
// Display type parameters if present, but only when the caller hasn't
|
||||
// already displayed them (indicated by disallow_signature_name being false)
|
||||
if !self.settings.disallow_signature_name {
|
||||
let hide_unused_self = self.should_hide_self_from_display(self.db);
|
||||
|
||||
DisplayOptionalGenericContext {
|
||||
generic_context: self.generic_context,
|
||||
db: self.db,
|
||||
settings: settings.clone(),
|
||||
hide_unused_self,
|
||||
}
|
||||
.fmt_detailed(&mut f)?;
|
||||
}
|
||||
|
||||
// Parameters
|
||||
self.parameters
|
||||
.display_with(self.db, self.settings.clone())
|
||||
.display_with(self.db, settings.clone())
|
||||
.fmt_detailed(&mut f)?;
|
||||
|
||||
// Return type
|
||||
f.write_str(" -> ")?;
|
||||
self.return_ty
|
||||
.display_with(self.db, self.settings.singleline())
|
||||
.display_with(self.db, settings.singleline())
|
||||
.fmt_detailed(&mut f)?;
|
||||
|
||||
if self.parameters.is_top() {
|
||||
|
||||
@@ -1818,10 +1818,19 @@ 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(db).to_string());
|
||||
message.push_str(
|
||||
&class.display_with(db, display_settings.clone()).to_string(),
|
||||
);
|
||||
// Omit the comma for the last element (which is always `object`)
|
||||
if class
|
||||
.into_class()
|
||||
|
||||
@@ -53,7 +53,7 @@ use crate::semantic_index::{
|
||||
};
|
||||
use crate::subscript::{PyIndex, PySlice};
|
||||
use crate::types::call::bind::{CallableDescription, MatchingOverloadIndex};
|
||||
use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind};
|
||||
use crate::types::call::{Argument, Binding, Bindings, CallArguments, CallError, CallErrorKind};
|
||||
use crate::types::class::{
|
||||
ClassLiteral, CodeGeneratorKind, DynamicClassLiteral, DynamicMetaclassConflict, FieldKind,
|
||||
MetaclassErrorKind, MethodDecorator,
|
||||
@@ -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, MroErrorKind};
|
||||
use crate::types::mro::{DynamicMroErrorKind, StaticMroErrorKind};
|
||||
use crate::types::newtype::NewType;
|
||||
use crate::types::subclass_of::SubclassOfInner;
|
||||
use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleType};
|
||||
@@ -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() {
|
||||
MroErrorKind::DuplicateBases(duplicates) => {
|
||||
StaticMroErrorKind::DuplicateBases(duplicates) => {
|
||||
let base_nodes = class_node.bases();
|
||||
for duplicate in duplicates {
|
||||
report_duplicate_bases(&self.context, class, duplicate, base_nodes);
|
||||
}
|
||||
}
|
||||
MroErrorKind::InvalidBases(bases) => {
|
||||
StaticMroErrorKind::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> {
|
||||
);
|
||||
}
|
||||
}
|
||||
MroErrorKind::UnresolvableMro { bases_list } => {
|
||||
StaticMroErrorKind::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> {
|
||||
));
|
||||
}
|
||||
}
|
||||
MroErrorKind::Pep695ClassWithGenericInheritance => {
|
||||
StaticMroErrorKind::Pep695ClassWithGenericInheritance => {
|
||||
if let Some(builder) =
|
||||
self.context.report_lint(&INVALID_GENERIC_CLASS, class_node)
|
||||
{
|
||||
@@ -839,7 +839,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
);
|
||||
}
|
||||
}
|
||||
MroErrorKind::InheritanceCycle => {
|
||||
StaticMroErrorKind::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,
|
||||
class_node,
|
||||
class.header_range(self.db()),
|
||||
Some(class_node.bases()),
|
||||
&disjoint_bases,
|
||||
);
|
||||
}
|
||||
@@ -960,7 +960,47 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
// (6) If the class is generic, verify that its generic context does not violate any of
|
||||
// (7) Check that the class arguments matches the arguments of the
|
||||
// base class `__init_subclass__` method.
|
||||
if let Some(args) = class_node.arguments.as_deref() {
|
||||
let call_args: CallArguments = args
|
||||
.keywords
|
||||
.iter()
|
||||
.filter_map(|keyword| match keyword.arg.as_ref() {
|
||||
// We mimic the runtime behaviour and discard the metaclass argument
|
||||
Some(name) if name.id.as_str() == "metaclass" => None,
|
||||
Some(name) => {
|
||||
let ty = self.expression_type(&keyword.value);
|
||||
Some((Argument::Keyword(name.id.as_str()), Some(ty)))
|
||||
}
|
||||
None => {
|
||||
let ty = self.expression_type(&keyword.value);
|
||||
Some((Argument::Keywords, Some(ty)))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let init_subclass_type = class
|
||||
.class_member_from_mro(
|
||||
self.db(),
|
||||
"__init_subclass__",
|
||||
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
|
||||
// skip(1) to skip the current class and only consider base classes.
|
||||
class.iter_mro(self.db(), None).skip(1),
|
||||
)
|
||||
.ignore_possibly_undefined();
|
||||
|
||||
if let Some(init_subclass) = init_subclass_type {
|
||||
let call_args = call_args.with_self(Some(Type::from(class)));
|
||||
if let Err(CallError(CallErrorKind::BindingError, bindings)) =
|
||||
init_subclass.try_call(self.db(), &call_args)
|
||||
{
|
||||
bindings.report_diagnostics(&self.context, class_node.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (8) If the class is generic, verify that its generic context does not violate any of
|
||||
// the typevar scoping rules.
|
||||
if let (Some(legacy), Some(inherited)) = (
|
||||
class.legacy_generic_context(self.db()),
|
||||
@@ -1039,7 +1079,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
// (7) Check that a dataclass does not have more than one `KW_ONLY`.
|
||||
// (9) Check that a dataclass does not have more than one `KW_ONLY`.
|
||||
if let Some(field_policy @ CodeGeneratorKind::DataclassLike(_)) =
|
||||
CodeGeneratorKind::from_class(self.db(), class.into(), None)
|
||||
{
|
||||
@@ -1074,7 +1114,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
// (8) Check for violations of the Liskov Substitution Principle,
|
||||
// (10) Check for violations of the Liskov Substitution Principle,
|
||||
// and for violations of other rules relating to invalid overrides of some sort.
|
||||
overrides::check_class(&self.context, class);
|
||||
|
||||
@@ -1082,7 +1122,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
protocol.validate_members(&self.context);
|
||||
}
|
||||
|
||||
// (9) If it's a `TypedDict` class, check that it doesn't include any invalid
|
||||
// (11) If it's a `TypedDict` class, check that it doesn't include any invalid
|
||||
// statements: https://typing.python.org/en/latest/spec/typeddict.html#class-based-syntax
|
||||
//
|
||||
// The body of the class definition defines the items of the `TypedDict` type. It
|
||||
@@ -6095,15 +6135,59 @@ 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());
|
||||
let namespace_type = self.infer_expression(namespace_arg, TypeContext::default());
|
||||
|
||||
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)
|
||||
// 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)
|
||||
{
|
||||
let mut diagnostic = builder
|
||||
.into_diagnostic("Invalid argument to parameter 3 (`namespace`) of `type()`");
|
||||
@@ -6130,13 +6214,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
ast::name::Name::new_static("<unknown>")
|
||||
};
|
||||
|
||||
let bases = self.extract_dynamic_type_bases(bases_arg, bases_type, &name);
|
||||
let (bases, mut disjoint_bases) =
|
||||
self.extract_dynamic_type_bases(bases_arg, bases_type, &name);
|
||||
|
||||
let dynamic_class = DynamicClassLiteral::new(db, name, bases, definition, None);
|
||||
let dynamic_class = DynamicClassLiteral::new(
|
||||
db,
|
||||
name,
|
||||
bases,
|
||||
definition,
|
||||
members,
|
||||
has_dynamic_namespace,
|
||||
None,
|
||||
);
|
||||
|
||||
// Check for MRO errors.
|
||||
if let Err(error) = dynamic_class.try_mro(db) {
|
||||
match error.reason() {
|
||||
match dynamic_class.try_mro(db) {
|
||||
Err(error) => match error.reason() {
|
||||
DynamicMroErrorKind::DuplicateBases(duplicates) => {
|
||||
if let Some(builder) = self.context.report_lint(&DUPLICATE_BASE, call_expr) {
|
||||
builder.into_diagnostic(format_args!(
|
||||
@@ -6164,6 +6257,18 @@ 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6191,14 +6296,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
|
||||
/// Extract base classes from the second argument of a `type()` call.
|
||||
///
|
||||
/// If any bases were invalid, diagnostics are emitted and the dynamic
|
||||
/// class is inferred as inheriting from `Unknown`.
|
||||
/// 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`.
|
||||
fn extract_dynamic_type_bases(
|
||||
&mut self,
|
||||
bases_node: &ast::Expr,
|
||||
bases_type: Type<'db>,
|
||||
name: &ast::name::Name,
|
||||
) -> Box<[ClassBase<'db>]> {
|
||||
) -> (Box<[ClassBase<'db>]>, IncompatibleBases<'db>) {
|
||||
let db = self.db();
|
||||
|
||||
// Get AST nodes for base expressions (for diagnostics).
|
||||
@@ -6209,7 +6315,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
let placeholder_class: ClassLiteral<'db> =
|
||||
KnownClass::Object.try_to_class_literal(db).unwrap().into();
|
||||
|
||||
bases_type
|
||||
let mut disjoint_bases = IncompatibleBases::default();
|
||||
|
||||
let bases = bases_type
|
||||
.tuple_instance_spec(db)
|
||||
.as_deref()
|
||||
.and_then(|spec| spec.as_fixed_length())
|
||||
@@ -6224,6 +6332,16 @@ 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;
|
||||
}
|
||||
|
||||
@@ -6256,20 +6374,18 @@ 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)
|
||||
} 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
|
||||
));
|
||||
if bases_tuple_elts.is_none() {
|
||||
diagnostic.info(format_args!(
|
||||
"Element {} of the tuple is invalid",
|
||||
idx + 1
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6292,7 +6408,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
));
|
||||
}
|
||||
Box::from([ClassBase::unknown()])
|
||||
})
|
||||
});
|
||||
|
||||
(bases, disjoint_bases)
|
||||
}
|
||||
|
||||
fn infer_annotated_assignment_statement(&mut self, assignment: &ast::StmtAnnAssign) {
|
||||
|
||||
@@ -53,7 +53,7 @@ impl<'db> Mro<'db> {
|
||||
db: &'db dyn Db,
|
||||
class_literal: StaticClassLiteral<'db>,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> Result<Self, MroError<'db>> {
|
||||
) -> Result<Self, StaticMroError<'db>> {
|
||||
/// Possibly add `Generic` to the resolved bases list.
|
||||
///
|
||||
/// This function is called in two cases:
|
||||
@@ -141,10 +141,15 @@ impl<'db> Mro<'db> {
|
||||
{
|
||||
ClassBase::try_from_type(db, *single_base, ClassLiteral::Static(class_literal))
|
||||
.map_or_else(
|
||||
|| Err(MroErrorKind::InvalidBases(Box::from([(0, *single_base)]))),
|
||||
|| {
|
||||
Err(StaticMroErrorKind::InvalidBases(Box::from([(
|
||||
0,
|
||||
*single_base,
|
||||
)])))
|
||||
},
|
||||
|single_base| {
|
||||
if single_base.has_cyclic_mro(db) {
|
||||
Err(MroErrorKind::InheritanceCycle)
|
||||
Err(StaticMroErrorKind::InheritanceCycle)
|
||||
} else {
|
||||
Ok(std::iter::once(ClassBase::Class(class))
|
||||
.chain(single_base.mro(db, specialization))
|
||||
@@ -188,8 +193,10 @@ impl<'db> Mro<'db> {
|
||||
}
|
||||
|
||||
if !invalid_bases.is_empty() {
|
||||
return Err(MroErrorKind::InvalidBases(invalid_bases.into_boxed_slice())
|
||||
.into_mro_error(db, class));
|
||||
return Err(
|
||||
StaticMroErrorKind::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
|
||||
@@ -201,7 +208,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(MroErrorKind::InheritanceCycle.into_mro_error(db, class));
|
||||
return Err(StaticMroErrorKind::InheritanceCycle.into_mro_error(db, class));
|
||||
}
|
||||
seqs.push(base.mro(db, specialization).collect());
|
||||
}
|
||||
@@ -229,9 +236,8 @@ impl<'db> Mro<'db> {
|
||||
)
|
||||
})
|
||||
{
|
||||
return Err(
|
||||
MroErrorKind::Pep695ClassWithGenericInheritance.into_mro_error(db, class)
|
||||
);
|
||||
return Err(StaticMroErrorKind::Pep695ClassWithGenericInheritance
|
||||
.into_mro_error(db, class));
|
||||
}
|
||||
|
||||
let mut duplicate_dynamic_bases = false;
|
||||
@@ -291,14 +297,14 @@ impl<'db> Mro<'db> {
|
||||
if duplicate_dynamic_bases {
|
||||
Ok(Mro::from_error(db, class))
|
||||
} else {
|
||||
Err(MroErrorKind::UnresolvableMro {
|
||||
Err(StaticMroErrorKind::UnresolvableMro {
|
||||
bases_list: original_bases.iter().copied().collect(),
|
||||
}
|
||||
.into_mro_error(db, class))
|
||||
}
|
||||
} else {
|
||||
Err(
|
||||
MroErrorKind::DuplicateBases(duplicate_bases.into_boxed_slice())
|
||||
StaticMroErrorKind::DuplicateBases(duplicate_bases.into_boxed_slice())
|
||||
.into_mro_error(db, class),
|
||||
)
|
||||
}
|
||||
@@ -537,23 +543,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 MroError<'db> {
|
||||
kind: MroErrorKind<'db>,
|
||||
pub(super) struct StaticMroError<'db> {
|
||||
kind: StaticMroErrorKind<'db>,
|
||||
fallback_mro: Mro<'db>,
|
||||
}
|
||||
|
||||
impl<'db> MroError<'db> {
|
||||
impl<'db> StaticMroError<'db> {
|
||||
/// Construct an MRO error of kind `InheritanceCycle`.
|
||||
pub(super) fn cycle(db: &'db dyn Db, class: ClassType<'db>) -> Self {
|
||||
MroErrorKind::InheritanceCycle.into_mro_error(db, class)
|
||||
StaticMroErrorKind::InheritanceCycle.into_mro_error(db, class)
|
||||
}
|
||||
|
||||
pub(super) fn is_cycle(&self) -> bool {
|
||||
matches!(self.kind, MroErrorKind::InheritanceCycle)
|
||||
matches!(self.kind, StaticMroErrorKind::InheritanceCycle)
|
||||
}
|
||||
|
||||
/// Return an [`MroErrorKind`] variant describing why we could not resolve the MRO for this class.
|
||||
pub(super) fn reason(&self) -> &MroErrorKind<'db> {
|
||||
/// Return an [`StaticMroErrorKind`] variant describing why we could not resolve the MRO for this class.
|
||||
pub(super) fn reason(&self) -> &StaticMroErrorKind<'db> {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
@@ -564,9 +570,9 @@ impl<'db> MroError<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible ways in which attempting to resolve the MRO of a class might fail.
|
||||
/// Possible ways in which attempting to resolve the MRO of a statically-defined class might fail.
|
||||
#[derive(Debug, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
pub(super) enum MroErrorKind<'db> {
|
||||
pub(super) enum StaticMroErrorKind<'db> {
|
||||
/// The class inherits from one or more invalid bases.
|
||||
///
|
||||
/// To avoid excessive complexity in our implementation,
|
||||
@@ -596,9 +602,13 @@ pub(super) enum MroErrorKind<'db> {
|
||||
UnresolvableMro { bases_list: Box<[Type<'db>]> },
|
||||
}
|
||||
|
||||
impl<'db> MroErrorKind<'db> {
|
||||
pub(super) fn into_mro_error(self, db: &'db dyn Db, class: ClassType<'db>) -> MroError<'db> {
|
||||
MroError {
|
||||
impl<'db> StaticMroErrorKind<'db> {
|
||||
pub(super) fn into_mro_error(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
class: ClassType<'db>,
|
||||
) -> StaticMroError<'db> {
|
||||
StaticMroError {
|
||||
kind: self,
|
||||
fallback_mro: Mro::from_error(db, class),
|
||||
}
|
||||
@@ -660,7 +670,7 @@ fn c3_merge(mut sequences: Vec<VecDeque<ClassBase>>) -> Option<Mro> {
|
||||
|
||||
/// Error for dynamic class MRO computation with fallback MRO.
|
||||
///
|
||||
/// Separate from [`MroError`] because dynamic classes can only have a subset of MRO errors.
|
||||
/// Separate from [`StaticMroError`] 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,6 +1250,29 @@ 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));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,6 +713,19 @@ impl<'db> Signature<'db> {
|
||||
Self::new(Parameters::bottom(), Type::Never)
|
||||
}
|
||||
|
||||
/// Returns `true` if `Self` should be hidden from the generic context display.
|
||||
///
|
||||
/// `Self` is hidden if it does not appear in:
|
||||
/// 1. The return type
|
||||
/// 2. Any explicitly annotated parameter (not inferred)
|
||||
pub(crate) fn should_hide_self_from_display(&self, db: &'db dyn Db) -> bool {
|
||||
!self.return_ty.contains_self(db)
|
||||
&& !self
|
||||
.parameters()
|
||||
.iter()
|
||||
.any(|p| p.should_annotation_be_displayed() && p.annotated_type().contains_self(db))
|
||||
}
|
||||
|
||||
pub(crate) fn with_inherited_generic_context(
|
||||
mut self,
|
||||
db: &'db dyn Db,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use self::schedule::spawn_main_loop;
|
||||
use crate::PositionEncoding;
|
||||
use crate::capabilities::{ResolvedClientCapabilities, server_capabilities};
|
||||
use crate::session::{InitializationOptions, Session, warn_about_unknown_options};
|
||||
use crate::session::{ClientName, InitializationOptions, Session, warn_about_unknown_options};
|
||||
use anyhow::Context;
|
||||
use lsp_server::Connection;
|
||||
use lsp_types::{ClientCapabilities, InitializeParams, MessageType, Url};
|
||||
@@ -47,6 +47,7 @@ impl Server {
|
||||
initialization_options,
|
||||
capabilities: client_capabilities,
|
||||
workspace_folders,
|
||||
client_info,
|
||||
..
|
||||
} = serde_json::from_value(init_value)
|
||||
.context("Failed to deserialize initialization parameters")?;
|
||||
@@ -65,6 +66,7 @@ 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);
|
||||
@@ -155,6 +157,7 @@ impl Server {
|
||||
workspace_urls,
|
||||
initialization_options,
|
||||
native_system,
|
||||
ClientName::from(client_info),
|
||||
in_test,
|
||||
)?,
|
||||
})
|
||||
|
||||
@@ -119,9 +119,12 @@ 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("ty failed to handle a request from the editor. Check the logs for more details.");
|
||||
client.show_error_message(format!(
|
||||
"ty failed to handle a request from the editor. {}",
|
||||
session.client_name().log_guidance()
|
||||
));
|
||||
}
|
||||
|
||||
respond_silent_error(
|
||||
@@ -175,11 +178,12 @@ 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(
|
||||
"ty failed to handle a notification from the editor. Check the logs for more details."
|
||||
);
|
||||
client.show_error_message(format!(
|
||||
"ty failed to handle a notification from the editor. {}",
|
||||
session.client_name().log_guidance()
|
||||
));
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -193,7 +197,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);
|
||||
respond::<R>(&id, result, client, session.client_name().log_guidance());
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -217,6 +221,7 @@ 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();
|
||||
@@ -238,7 +243,7 @@ where
|
||||
let snapshot = snapshot;
|
||||
R::handle_request(&id, snapshot.0, client, params);
|
||||
}) {
|
||||
panic_response::<R>(&id, client, &error, retry);
|
||||
panic_response::<R>(&id, client, &error, retry, log_guidance);
|
||||
}
|
||||
})
|
||||
}))
|
||||
@@ -284,6 +289,7 @@ 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();
|
||||
@@ -306,7 +312,7 @@ where
|
||||
R::handle_request(&id, &db, document, client, params);
|
||||
});
|
||||
}) {
|
||||
panic_response::<R>(&id, client, &error, retry);
|
||||
panic_response::<R>(&id, client, &error, retry, log_guidance);
|
||||
}
|
||||
})
|
||||
}))
|
||||
@@ -317,6 +323,7 @@ fn panic_response<R>(
|
||||
client: &Client,
|
||||
error: &PanicError,
|
||||
request: Option<lsp_server::Request>,
|
||||
log_guidance: &str,
|
||||
) where
|
||||
R: traits::RetriableRequestHandler,
|
||||
{
|
||||
@@ -346,6 +353,7 @@ fn panic_response<R>(
|
||||
error: anyhow!("request handler {error}"),
|
||||
}),
|
||||
client,
|
||||
log_guidance,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -358,7 +366,10 @@ 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("ty encountered a problem. Check the logs for more details.");
|
||||
client.show_error_message(format!(
|
||||
"ty encountered a problem. {}",
|
||||
session.client_name().log_guidance()
|
||||
));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -390,6 +401,8 @@ 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();
|
||||
|
||||
@@ -399,18 +412,14 @@ where
|
||||
Ok(result) => result,
|
||||
Err(panic) => {
|
||||
tracing::error!("An error occurred while running {id}: {panic}");
|
||||
client.show_error_message(
|
||||
"ty encountered a panic. Check the logs for more details.",
|
||||
);
|
||||
client.show_error_message(format!("ty encountered a panic. {log_guidance}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = result {
|
||||
tracing::error!("An error occurred while running {id}: {err}");
|
||||
client.show_error_message(
|
||||
"ty encountered a problem. Check the logs for more details.",
|
||||
);
|
||||
client.show_error_message(format!("ty encountered a problem. {log_guidance}"));
|
||||
}
|
||||
})
|
||||
}))
|
||||
@@ -449,12 +458,13 @@ 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("ty encountered a problem. Check the logs for more details.");
|
||||
client.show_error_message(format!("ty encountered a problem. {log_guidance}"));
|
||||
}
|
||||
client.respond(id, result);
|
||||
}
|
||||
|
||||
@@ -116,7 +116,10 @@ pub(super) trait BackgroundDocumentRequestHandler: RetriableRequestHandler {
|
||||
|
||||
if let Err(err) = &result {
|
||||
tracing::error!("An error occurred with request ID {id}: {err}");
|
||||
client.show_error_message("ty encountered a problem. Check the logs for more details.");
|
||||
client.show_error_message(format!(
|
||||
"ty encountered a problem. {}",
|
||||
snapshot.client_name().log_guidance()
|
||||
));
|
||||
}
|
||||
|
||||
client.respond(id, result);
|
||||
@@ -153,7 +156,10 @@ pub(super) trait BackgroundRequestHandler: RetriableRequestHandler {
|
||||
|
||||
if let Err(err) = &result {
|
||||
tracing::error!("An error occurred with request ID {id}: {err}");
|
||||
client.show_error_message("ty encountered a problem. Check the logs for more details.");
|
||||
client.show_error_message(format!(
|
||||
"ty encountered a problem. {}",
|
||||
snapshot.client_name().log_guidance()
|
||||
));
|
||||
}
|
||||
|
||||
client.respond(id, result);
|
||||
|
||||
@@ -13,7 +13,7 @@ use lsp_types::request::{
|
||||
WorkspaceDiagnosticRequest,
|
||||
};
|
||||
use lsp_types::{
|
||||
DiagnosticRegistrationOptions, DiagnosticServerCapabilities,
|
||||
ClientInfo, DiagnosticRegistrationOptions, DiagnosticServerCapabilities,
|
||||
DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, Registration, RegistrationParams,
|
||||
TextDocumentContentChangeEvent, Unregistration, UnregistrationParams, Url,
|
||||
};
|
||||
@@ -106,6 +106,9 @@ 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
|
||||
@@ -141,6 +144,7 @@ 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());
|
||||
@@ -168,6 +172,7 @@ impl Session {
|
||||
suspended_workspace_diagnostics_request: None,
|
||||
revision: 0,
|
||||
registrations: HashSet::new(),
|
||||
client_name,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -532,8 +537,8 @@ impl Session {
|
||||
);
|
||||
|
||||
client.show_error_message(format!(
|
||||
"Failed to load project for workspace {url}. \
|
||||
Please refer to the logs for more details.",
|
||||
"Failed to load project for workspace {url}. {}",
|
||||
self.client_name.log_guidance(),
|
||||
));
|
||||
|
||||
let db_with_default_settings = ProjectMetadata::from_options(
|
||||
@@ -819,6 +824,7 @@ impl Session {
|
||||
.unwrap_or_else(|| Arc::new(WorkspaceSettings::default())),
|
||||
position_encoding: self.position_encoding,
|
||||
document: document_handle,
|
||||
client_name: self.client_name,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -837,6 +843,7 @@ impl Session {
|
||||
in_test: self.in_test,
|
||||
resolved_client_capabilities: self.resolved_client_capabilities,
|
||||
revision: self.revision,
|
||||
client_name: self.client_name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -976,6 +983,10 @@ 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.
|
||||
@@ -1025,6 +1036,7 @@ pub(crate) struct DocumentSnapshot {
|
||||
workspace_settings: Arc<WorkspaceSettings>,
|
||||
position_encoding: PositionEncoding,
|
||||
document: DocumentHandle,
|
||||
client_name: ClientName,
|
||||
}
|
||||
|
||||
impl DocumentSnapshot {
|
||||
@@ -1071,6 +1083,10 @@ 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`].
|
||||
@@ -1081,6 +1097,7 @@ 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`
|
||||
@@ -1122,6 +1139,42 @@ 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` on Unix,
|
||||
/// but `foo.bar.A @ src\foo\bar.py:10` on Windows).
|
||||
/// 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).
|
||||
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,6 +360,8 @@ impl Matcher {
|
||||
return false;
|
||||
};
|
||||
|
||||
let primary_annotation = normalize_paths(primary_annotation);
|
||||
|
||||
// reveal_type, reveal_protocol_interface
|
||||
if matches!(
|
||||
primary_message,
|
||||
|
||||
106
issue_2438_root_cause_analysis.md
Normal file
106
issue_2438_root_cause_analysis.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Root Cause Analysis: Issue #2438
|
||||
|
||||
## Summary
|
||||
|
||||
**Issue**: A `try-except` block before an import causes incorrect type inference in function scopes. With `from .demo import demo` where `demo.py` contains `demo = 42`, the function scope incorrectly shows `<module 'repro.demo'> | Literal[42]` instead of just `Literal[42]`.
|
||||
|
||||
## Root Cause
|
||||
|
||||
The bug is caused by an interaction between how submodule import bindings are created and how declaration reachability affects the type resolution path in `place_by_id`.
|
||||
|
||||
### Key Finding
|
||||
|
||||
When processing `from .demo import demo` in an `__init__.py`:
|
||||
|
||||
1. **Two bindings are created for symbol `demo`**:
|
||||
- `ImportFromSubmoduleDefinitionNodeRef` - creates only a **binding** (type: module)
|
||||
- `ImportFromDefinitionNodeRef` - creates **both** a declaration and a binding (type: Literal[42])
|
||||
|
||||
2. **Reachability affects which path is taken**:
|
||||
- **Without try-except**: All declarations have `AlwaysTrue` reachability
|
||||
- **With try-except before import**: All declarations have `Ambiguous` reachability
|
||||
|
||||
3. **Declaration definedness calculation** (`place.rs:1520-1530`):
|
||||
```rust
|
||||
let boundness = match boundness_analysis {
|
||||
BoundnessAnalysis::AssumeBound => {
|
||||
if all_declarations_definitely_reachable {
|
||||
Definedness::AlwaysDefined // When reachability is AlwaysTrue
|
||||
} else {
|
||||
Definedness::PossiblyUndefined // When reachability is Ambiguous
|
||||
}
|
||||
}
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
Where `all_declarations_definitely_reachable` is `true` only when ALL declarations have `AlwaysTrue` reachability (via `is_always_true()`).
|
||||
|
||||
4. **Dispatch in `place_by_id`** (`place.rs:949-956`):
|
||||
```rust
|
||||
// Place is declared, trust the declared type
|
||||
place_and_quals @ PlaceAndQualifiers {
|
||||
place: Place::Defined(DefinedPlace {
|
||||
definedness: Definedness::AlwaysDefined, // Case 1 matches here
|
||||
..
|
||||
}),
|
||||
qualifiers: _,
|
||||
} => place_and_quals, // Returns declaration type directly, ignoring bindings
|
||||
```
|
||||
|
||||
### Result
|
||||
|
||||
- **Without try-except** (AlwaysTrue): Declaration is `AlwaysDefined`, so declared type (`Literal[42]`) is returned directly. The submodule binding (module type) is never consulted.
|
||||
|
||||
- **With try-except** (Ambiguous): Declaration is `PossiblyUndefined`, so code falls through to union declarations with bindings. Both the module type and `Literal[42]` are included in the result.
|
||||
|
||||
## Why This Is a Bug
|
||||
|
||||
The intent of submodule import tracking is to ensure that `demo` can be accessed as both:
|
||||
- The actual imported value (`Literal[42]`)
|
||||
- The submodule itself (for cases like `package.demo`)
|
||||
|
||||
However, when declaration reachability is `AlwaysTrue`, the code shortcuts directly to the declared type without considering bindings. This means the submodule binding is silently ignored, which works correctly for module-level access but breaks when accessed from function scope (which uses `AllReachable` bindings).
|
||||
|
||||
## Affected Code Paths
|
||||
|
||||
- `crates/ty_python_semantic/src/place.rs`:
|
||||
- `place_by_id` (lines 949-956): Early return for `AlwaysDefined` declarations
|
||||
- `place_from_declarations_impl` (lines 1520-1530): Boundness calculation based on reachability
|
||||
|
||||
- `crates/ty_python_semantic/src/semantic_index/builder.rs`:
|
||||
- Lines 1531-1546: `ImportFromSubmoduleDefinitionNodeRef` creation (binding only)
|
||||
- Lines 1677-1686: `ImportFromDefinitionNodeRef` creation (declaration + binding)
|
||||
|
||||
## Potential Fixes
|
||||
|
||||
1. **Make ImportFromSubmodule also create a declaration**: This would ensure the submodule type is included in the declared type computation.
|
||||
|
||||
2. **Special-case submodule imports**: When resolving a symbol that has both a submodule binding and another binding, always consider both.
|
||||
|
||||
3. **Reconsider the `AlwaysDefined` shortcut**: Perhaps the early return for `AlwaysDefined` declarations should still consider certain bindings (like submodule imports).
|
||||
|
||||
## Test Case
|
||||
|
||||
A failing test that reproduces this issue:
|
||||
|
||||
```python
|
||||
# package/__init__.py - Without try-except (BUG: returns only Literal[42])
|
||||
from .demo import demo
|
||||
|
||||
def foo():
|
||||
reveal_type(demo) # Should be Literal[42], NOT module | Literal[42]
|
||||
|
||||
# package2/__init__.py - With try-except (Incorrectly returns union)
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
from .demo import demo
|
||||
|
||||
def foo():
|
||||
reveal_type(demo) # Shows: <module 'package2.demo'> | Literal[42]
|
||||
```
|
||||
|
||||
Both should produce the same result (`Literal[42]`), but currently Case 1 works correctly while Case 2 incorrectly includes the module type.
|
||||
Reference in New Issue
Block a user