Compare commits

...

11 Commits

Author SHA1 Message Date
Claude
47f29b9250 Add root cause analysis for issue #2438
Document the root cause of incorrect type inference when a try-except
block precedes a relative import. The bug is in how declaration
reachability affects the code path in place_by_id - when reachability
is AlwaysTrue, the code shortcuts directly to declared type without
consulting bindings, causing submodule bindings to be silently ignored.
2026-01-14 00:31:58 +00:00
Carl Meyer
a697050a83 [ty] Fix stack overflow with recursive type aliases containing tuple … (#22543)
This fixes issue #2470 where recursive type aliases like `type
RecursiveT = int | tuple[RecursiveT, ...]` caused a stack overflow when
used in return type checking with constructors like `list()`.

The fix moves all type mapping processing for `UniqueSpecialization`
(and other non-EagerExpansion mappings) inside the `visitor.visit()`
closure. This ensures that if we encounter the same TypeAlias
recursively during type mapping, the cycle detector will properly detect
it and return the fallback value instead of recursing infinitely.

The key insight is that the previous code called
`apply_function_specialization` followed by another
`apply_type_mapping_impl` AFTER the visitor closure returned. At that
point, the TypeAlias was no longer in the visitor's `seen` set, so
recursive references would not be detected as cycles.
2026-01-13 11:25:01 -08:00
Dex Devlon
2f64ef9c72 [ty] Include type parameters in generic callable display (#22435) 2026-01-13 17:29:08 +00:00
RasmusNygren
fde7d72fbb [ty] Add diagnostics for __init_subclass__ argument mismatch (#22185) 2026-01-13 16:15:51 +00:00
drbh
d13b5db066 [ty] narrow the right-hand side of ==, !=, is and is not conditions when the left-hand side is not narrowable (#22511)
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2026-01-13 16:01:54 +00:00
Alex Waygood
c7b41060f4 [ty] Improve disambiguation of types (#22547) 2026-01-13 14:56:56 +00:00
Charlie Marsh
3878701265 [ty] Support own instance members for type(...) classes (#22480)
## Summary

Addresses
https://github.com/astral-sh/ruff/pull/22291#discussion_r2674467950.
2026-01-13 09:36:03 -05:00
Enric Calabuig
6e89e0abff [ty] Fix classmethod + contextmanager + Self (#22407)
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
  requests.)
- Does this pull request include references to any relevant issues?
-->

## Summary

The test I've added illustrates the fix. Copying it here too:

```python
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): ...

with Base.create() as base:
    reveal_type(base)  # revealed: Base (after the fix, None before)

with Child.create() as child:
    reveal_type(child)  # revealed: Child (after the fix, None before)
```

Full disclosure: I've used LLMs for this PR, but the result is
thoroughly reviewed by me before submitting. I'm excited about my first
Rust contribution to Astral tools and will address feedback quickly.

Related to https://github.com/astral-sh/ty/issues/2030, I am working on
a fix for the TypeVar case also reported in that issue (by me)

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

<!-- How was it tested? -->

Updated mdtests

---------

Co-authored-by: Douglas Creager <dcreager@dcreager.net>
2026-01-13 09:29:58 -05:00
Manuel Jacob
cb31883c5f Correct comment about public functions starting with an underscore. (#22550) 2026-01-13 14:04:19 +00:00
Charlie Marsh
6d8f2864c3 [ty] Rename MRO structs to match static nomenclature (#22549)
## Summary

I didn't want to make the "dynamic" `type(...)` PR any larger, but it
probably makes sense to rename these now that we have `Dynamic`
variants.
2026-01-13 08:53:49 -05:00
Dhruv Manilawala
990d0a8999 [ty] Improve log guidance message for Zed (#22530)
## Summary

closes: https://github.com/astral-sh/ty/issues/1717

## Test Plan


https://github.com/user-attachments/assets/5d8d6d03-3451-4403-a6cd-2deba9e796fc
2026-01-13 08:13:11 +00:00
43 changed files with 1558 additions and 264 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -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:

View File

@@ -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))
```

View File

@@ -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())
```

View File

@@ -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)

View File

@@ -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))
```

View File

@@ -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()
```

View File

@@ -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)
```

View File

@@ -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)
```

View File

@@ -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)))

View File

@@ -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

View File

@@ -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'>)

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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[""]

View File

@@ -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)
```

View File

@@ -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()
```

View File

@@ -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)
```

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
```

View File

@@ -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

View File

@@ -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(_)

View File

@@ -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))
}
}

View File

@@ -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,
}
}
}

View File

@@ -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))

View File

@@ -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() {

View File

@@ -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()

View File

@@ -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) {

View File

@@ -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>,

View File

@@ -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));
}
}
_ => {}
}
}

View File

@@ -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,

View File

@@ -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,
)?,
})

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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)]

View File

@@ -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: &regex::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,

View 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.