mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-26 11:59:10 +00:00
[red-knot] Use Unknown | T_inferred
for undeclared public symbols (#15674)
## Summary Use `Unknown | T_inferred` as the type for *undeclared* public symbols. ## Test Plan - Updated existing tests - New test for external `__slots__` modifications. - New tests for external modifications of public symbols.
This commit is contained in:
parent
7778d1d646
commit
1feb3cf41a
23 changed files with 212 additions and 91 deletions
|
@ -36,7 +36,7 @@ def f():
|
|||
reveal_type(a7) # revealed: None
|
||||
reveal_type(a8) # revealed: Literal[1]
|
||||
# TODO: This should be Color.RED
|
||||
reveal_type(b1) # revealed: Literal[0]
|
||||
reveal_type(b1) # revealed: Unknown | Literal[0]
|
||||
|
||||
# error: [invalid-type-form]
|
||||
invalid1: Literal[3 + 4]
|
||||
|
|
|
@ -175,7 +175,7 @@ class C:
|
|||
|
||||
reveal_type(C.pure_class_variable1) # revealed: str
|
||||
|
||||
# TODO: this should be `Literal[1]`, or `Unknown | Literal[1]`.
|
||||
# TODO: Should be `Unknown | Literal[1]`.
|
||||
reveal_type(C.pure_class_variable2) # revealed: Unknown
|
||||
|
||||
c_instance = C()
|
||||
|
@ -252,8 +252,7 @@ class C:
|
|||
|
||||
reveal_type(C.variable_with_class_default1) # revealed: str
|
||||
|
||||
# TODO: this should be `Unknown | Literal[1]`.
|
||||
reveal_type(C.variable_with_class_default2) # revealed: Literal[1]
|
||||
reveal_type(C.variable_with_class_default2) # revealed: Unknown | Literal[1]
|
||||
|
||||
c_instance = C()
|
||||
|
||||
|
@ -296,8 +295,8 @@ def _(flag: bool):
|
|||
else:
|
||||
x = 4
|
||||
|
||||
reveal_type(C1.x) # revealed: Literal[1, 2]
|
||||
reveal_type(C2.x) # revealed: Literal[3, 4]
|
||||
reveal_type(C1.x) # revealed: Unknown | Literal[1, 2]
|
||||
reveal_type(C2.x) # revealed: Unknown | Literal[3, 4]
|
||||
```
|
||||
|
||||
## Inherited class attributes
|
||||
|
@ -311,7 +310,7 @@ class A:
|
|||
class B(A): ...
|
||||
class C(B): ...
|
||||
|
||||
reveal_type(C.X) # revealed: Literal["foo"]
|
||||
reveal_type(C.X) # revealed: Unknown | Literal["foo"]
|
||||
```
|
||||
|
||||
### Multiple inheritance
|
||||
|
@ -334,7 +333,7 @@ class A(B, C): ...
|
|||
reveal_type(A.__mro__)
|
||||
|
||||
# `E` is earlier in the MRO than `F`, so we should use the type of `E.X`
|
||||
reveal_type(A.X) # revealed: Literal[42]
|
||||
reveal_type(A.X) # revealed: Unknown | Literal[42]
|
||||
```
|
||||
|
||||
## Unions with possibly unbound paths
|
||||
|
@ -356,7 +355,7 @@ def _(flag1: bool, flag2: bool):
|
|||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 3]
|
||||
reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
|
||||
```
|
||||
|
||||
### Possibly-unbound within a class
|
||||
|
@ -379,7 +378,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
|||
C = C1 if flag1 else C2 if flag2 else C3
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C1, C2, C3]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[1, 2, 3]
|
||||
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
### Unions with all paths unbound
|
||||
|
|
|
@ -262,7 +262,8 @@ class A:
|
|||
class B:
|
||||
__add__ = A()
|
||||
|
||||
reveal_type(B() + B()) # revealed: int
|
||||
# TODO: this could be `int` if we declare `B.__add__` using a `Callable` type
|
||||
reveal_type(B() + B()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Integration test: numbers from typeshed
|
||||
|
|
|
@ -5,6 +5,11 @@ that is, a use of a symbol from another scope. If a symbol has a declared type i
|
|||
(e.g. `int`), we use that as the symbol's "public type" (the type of the symbol from the perspective
|
||||
of other scopes) even if there is a more precise local inferred type for the symbol (`Literal[1]`).
|
||||
|
||||
If a symbol has no declared type, we use the union of `Unknown` with the inferred type as the public
|
||||
type. If there is no declaration, then the symbol can be reassigned to any type from another scope;
|
||||
the union with `Unknown` reflects that its type must at least be as large as the type of the
|
||||
assigned value, but could be arbitrarily larger.
|
||||
|
||||
We test the whole matrix of possible boundness and declaredness states. The current behavior is
|
||||
summarized in the following table, while the tests below demonstrate each case. Note that some of
|
||||
this behavior is questionable and might change in the future. See the TODOs in `symbol_by_id`
|
||||
|
@ -12,11 +17,11 @@ this behavior is questionable and might change in the future. See the TODOs in `
|
|||
In particular, we should raise errors in the "possibly-undeclared-and-unbound" as well as the
|
||||
"undeclared-and-possibly-unbound" cases (marked with a "?").
|
||||
|
||||
| **Public type** | declared | possibly-undeclared | undeclared |
|
||||
| ---------------- | ------------ | -------------------------- | ------------ |
|
||||
| bound | `T_declared` | `T_declared \| T_inferred` | `T_inferred` |
|
||||
| possibly-unbound | `T_declared` | `T_declared \| T_inferred` | `T_inferred` |
|
||||
| unbound | `T_declared` | `T_declared` | `Unknown` |
|
||||
| **Public type** | declared | possibly-undeclared | undeclared |
|
||||
| ---------------- | ------------ | -------------------------- | ----------------------- |
|
||||
| bound | `T_declared` | `T_declared \| T_inferred` | `Unknown \| T_inferred` |
|
||||
| possibly-unbound | `T_declared` | `T_declared \| T_inferred` | `Unknown \| T_inferred` |
|
||||
| unbound | `T_declared` | `T_declared` | `Unknown` |
|
||||
|
||||
| **Diagnostic** | declared | possibly-undeclared | undeclared |
|
||||
| ---------------- | -------- | ------------------------- | ------------------- |
|
||||
|
@ -97,17 +102,24 @@ def flag() -> bool: ...
|
|||
|
||||
x = 1
|
||||
y = 2
|
||||
z = 3
|
||||
if flag():
|
||||
x: Any
|
||||
x: int
|
||||
y: Any
|
||||
# error: [invalid-declaration]
|
||||
y: str
|
||||
z: str
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import x, y
|
||||
from mod import x, y, z
|
||||
|
||||
reveal_type(x) # revealed: Literal[1] | Any
|
||||
reveal_type(y) # revealed: Literal[2] | Unknown
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(y) # revealed: Literal[2] | Any
|
||||
reveal_type(z) # revealed: Literal[3] | Unknown
|
||||
|
||||
# External modifications of `x` that violate the declared type are not allowed:
|
||||
# error: [invalid-assignment]
|
||||
x = None
|
||||
```
|
||||
|
||||
### Possibly undeclared and possibly unbound
|
||||
|
@ -134,6 +146,10 @@ from mod import x, y
|
|||
|
||||
reveal_type(x) # revealed: Literal[1] | Any
|
||||
reveal_type(y) # revealed: Literal[2] | str
|
||||
|
||||
# External modifications of `y` that violate the declared type are not allowed:
|
||||
# error: [invalid-assignment]
|
||||
y = None
|
||||
```
|
||||
|
||||
### Possibly undeclared and unbound
|
||||
|
@ -154,14 +170,16 @@ if flag():
|
|||
from mod import x
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
# External modifications to `x` that violate the declared type are not allowed:
|
||||
# error: [invalid-assignment]
|
||||
x = None
|
||||
```
|
||||
|
||||
## Undeclared
|
||||
|
||||
### Undeclared but bound
|
||||
|
||||
We use the inferred type as the public type, if a symbol has no declared type.
|
||||
|
||||
```py path=mod.py
|
||||
x = 1
|
||||
```
|
||||
|
@ -169,7 +187,10 @@ x = 1
|
|||
```py
|
||||
from mod import x
|
||||
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
|
||||
# All external modifications of `x` are allowed:
|
||||
x = None
|
||||
```
|
||||
|
||||
### Undeclared and possibly unbound
|
||||
|
@ -189,7 +210,10 @@ if flag:
|
|||
# on top of this document.
|
||||
from mod import x
|
||||
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
|
||||
# All external modifications of `x` are allowed:
|
||||
x = None
|
||||
```
|
||||
|
||||
### Undeclared and unbound
|
||||
|
@ -206,4 +230,7 @@ if False:
|
|||
from mod import x
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
# Modifications allowed in this case:
|
||||
x = None
|
||||
```
|
||||
|
|
|
@ -52,7 +52,7 @@ class NonCallable:
|
|||
__call__ = 1
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `NonCallable` is not callable"
|
||||
# error: "Object of type `Unknown | Literal[1]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
|
|
@ -43,7 +43,8 @@ class IntIterable:
|
|||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
# revealed: tuple[int, int]
|
||||
# TODO: This could be a `tuple[int, int]` if we model that `y` can not be modified in the outer comprehension scope
|
||||
# revealed: tuple[int, Unknown | int]
|
||||
[[reveal_type((x, y)) for x in IntIterable()] for y in IntIterable()]
|
||||
```
|
||||
|
||||
|
@ -66,7 +67,8 @@ class IterableOfIterables:
|
|||
def __iter__(self) -> IteratorOfIterables:
|
||||
return IteratorOfIterables()
|
||||
|
||||
# revealed: tuple[int, IntIterable]
|
||||
# TODO: This could be a `tuple[int, int]` (see above)
|
||||
# revealed: tuple[int, Unknown | IntIterable]
|
||||
[[reveal_type((x, y)) for x in y] for y in IterableOfIterables()]
|
||||
```
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
```py
|
||||
def _(flag: bool):
|
||||
class A:
|
||||
always_bound = 1
|
||||
always_bound: int = 1
|
||||
|
||||
if flag:
|
||||
union = 1
|
||||
|
@ -13,14 +13,21 @@ def _(flag: bool):
|
|||
union = "abc"
|
||||
|
||||
if flag:
|
||||
possibly_unbound = "abc"
|
||||
union_declared: int = 1
|
||||
else:
|
||||
union_declared: str = "abc"
|
||||
|
||||
reveal_type(A.always_bound) # revealed: Literal[1]
|
||||
if flag:
|
||||
possibly_unbound: str = "abc"
|
||||
|
||||
reveal_type(A.union) # revealed: Literal[1, "abc"]
|
||||
reveal_type(A.always_bound) # revealed: int
|
||||
|
||||
reveal_type(A.union) # revealed: Unknown | Literal[1, "abc"]
|
||||
|
||||
reveal_type(A.union_declared) # revealed: int | str
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
|
||||
reveal_type(A.possibly_unbound) # revealed: Literal["abc"]
|
||||
reveal_type(A.possibly_unbound) # revealed: str
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
|
||||
reveal_type(A.non_existent) # revealed: Unknown
|
||||
|
|
|
@ -55,7 +55,7 @@ reveal_type("x" or "y" and "") # revealed: Literal["x"]
|
|||
## Evaluates to builtin
|
||||
|
||||
```py path=a.py
|
||||
redefined_builtin_bool = bool
|
||||
redefined_builtin_bool: type[bool] = bool
|
||||
|
||||
def my_bool(x) -> bool:
|
||||
return True
|
||||
|
|
|
@ -172,10 +172,10 @@ class IntUnion:
|
|||
def __len__(self) -> Literal[SomeEnum.INT, SomeEnum.INT_2]: ...
|
||||
|
||||
reveal_type(len(Auto())) # revealed: int
|
||||
reveal_type(len(Int())) # revealed: Literal[2]
|
||||
reveal_type(len(Int())) # revealed: int
|
||||
reveal_type(len(Str())) # revealed: int
|
||||
reveal_type(len(Tuple())) # revealed: int
|
||||
reveal_type(len(IntUnion())) # revealed: Literal[2, 32]
|
||||
reveal_type(len(IntUnion())) # revealed: int
|
||||
```
|
||||
|
||||
### Negative integers
|
||||
|
|
|
@ -20,7 +20,7 @@ wrong_innards: MyBox[int] = MyBox("five")
|
|||
# TODO reveal int, do not leak the typevar
|
||||
reveal_type(box.data) # revealed: T
|
||||
|
||||
reveal_type(MyBox.box_model_number) # revealed: Literal[695]
|
||||
reveal_type(MyBox.box_model_number) # revealed: Unknown | Literal[695]
|
||||
```
|
||||
|
||||
## Subclassing
|
||||
|
|
|
@ -23,8 +23,8 @@ reveal_type(y)
|
|||
# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound` is possibly unbound"
|
||||
from maybe_unbound import x, y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
reveal_type(y) # revealed: Literal[3]
|
||||
reveal_type(x) # revealed: Unknown | Literal[3]
|
||||
reveal_type(y) # revealed: Unknown | Literal[3]
|
||||
```
|
||||
|
||||
## Maybe unbound annotated
|
||||
|
@ -52,7 +52,7 @@ Importing an annotated name prefers the declared type over the inferred type:
|
|||
# error: [possibly-unbound-import] "Member `y` of module `maybe_unbound_annotated` is possibly unbound"
|
||||
from maybe_unbound_annotated import x, y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
reveal_type(x) # revealed: Unknown | Literal[3]
|
||||
reveal_type(y) # revealed: int
|
||||
```
|
||||
|
||||
|
|
|
@ -109,9 +109,9 @@ reveal_type(x)
|
|||
def _(flag: bool):
|
||||
class NotIterable:
|
||||
if flag:
|
||||
__iter__ = 1
|
||||
__iter__: int = 1
|
||||
else:
|
||||
__iter__ = None
|
||||
__iter__: None = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
|
@ -135,7 +135,7 @@ for x in nonsense: # error: "Object of type `Literal[123]` is not iterable"
|
|||
class NotIterable:
|
||||
def __getitem__(self, key: int) -> int:
|
||||
return 42
|
||||
__iter__ = None
|
||||
__iter__: None = None
|
||||
|
||||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
|
|
|
@ -99,9 +99,9 @@ def _(x: str | int):
|
|||
class A: ...
|
||||
class B: ...
|
||||
|
||||
alias_for_type = type
|
||||
|
||||
def _(x: A | B):
|
||||
alias_for_type = type
|
||||
|
||||
if alias_for_type(x) is A:
|
||||
reveal_type(x) # revealed: A
|
||||
```
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
def f():
|
||||
x = 1
|
||||
def g():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
## Two levels up
|
||||
|
@ -16,7 +16,7 @@ def f():
|
|||
x = 1
|
||||
def g():
|
||||
def h():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
## Skips class scope
|
||||
|
@ -28,7 +28,7 @@ def f():
|
|||
class C:
|
||||
x = 2
|
||||
def g():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
## Skips annotation-only assignment
|
||||
|
@ -41,7 +41,7 @@ def f():
|
|||
# name is otherwise not defined; maybe should be an error?
|
||||
x: int
|
||||
def h():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
## Implicit global in function
|
||||
|
@ -52,5 +52,5 @@ A name reference to a never-defined symbol in a function is implicitly a global
|
|||
x = 1
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
|
|
@ -17,8 +17,8 @@ class C:
|
|||
x = 2
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `x` on type `Literal[C]` is possibly unbound"
|
||||
reveal_type(C.x) # revealed: Literal[2]
|
||||
reveal_type(C.y) # revealed: Literal[1]
|
||||
reveal_type(C.x) # revealed: Unknown | Literal[2]
|
||||
reveal_type(C.y) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
## Possibly unbound in class and global scope
|
||||
|
@ -37,7 +37,7 @@ class C:
|
|||
# error: [possibly-unresolved-reference]
|
||||
y = x
|
||||
|
||||
reveal_type(C.y) # revealed: Literal[1, "abc"]
|
||||
reveal_type(C.y) # revealed: Unknown | Literal[1, "abc"]
|
||||
```
|
||||
|
||||
## Unbound function local
|
||||
|
|
|
@ -182,3 +182,34 @@ class C(A, B): ...
|
|||
# False negative: [incompatible-slots]
|
||||
class A(int, str): ...
|
||||
```
|
||||
|
||||
### Diagnostic if `__slots__` is externally modified
|
||||
|
||||
We special-case type inference for `__slots__` and return the pure inferred type, even if the symbol
|
||||
is not declared — a case in which we union with `Unknown` for other public symbols. The reason for
|
||||
this is that `__slots__` has a special handling in the runtime. Modifying it externally is actually
|
||||
allowed, but those changes do not take effect. If you have a class `C` with `__slots__ = ("foo",)`
|
||||
and externally set `C.__slots__ = ("bar",)`, you still can't access `C.bar`. And you can still
|
||||
access `C.foo`. We therefore issue a diagnostic for such assignments:
|
||||
|
||||
```py
|
||||
class A:
|
||||
__slots__ = ("a",)
|
||||
|
||||
# Modifying `__slots__` from within the class body is fine:
|
||||
__slots__ = ("a", "b")
|
||||
|
||||
# No `Unknown` here:
|
||||
reveal_type(A.__slots__) # revealed: tuple[Literal["a"], Literal["b"]]
|
||||
|
||||
# But modifying it externally is not:
|
||||
|
||||
# error: [invalid-assignment]
|
||||
A.__slots__ = ("a",)
|
||||
|
||||
# error: [invalid-assignment]
|
||||
A.__slots__ = ("a", "b_new")
|
||||
|
||||
# error: [invalid-assignment]
|
||||
A.__slots__ = ("a", "b", "c")
|
||||
```
|
||||
|
|
|
@ -14,7 +14,8 @@ a = NotSubscriptable()[0] # error: "Cannot subscript object of type `NotSubscri
|
|||
class NotSubscriptable:
|
||||
__getitem__ = None
|
||||
|
||||
a = NotSubscriptable()[0] # error: "Method `__getitem__` of type `None` is not callable on object of type `NotSubscriptable`"
|
||||
# error: "Method `__getitem__` of type `Unknown | None` is not callable on object of type `NotSubscriptable`"
|
||||
a = NotSubscriptable()[0]
|
||||
```
|
||||
|
||||
## Valid getitem
|
||||
|
|
|
@ -139,7 +139,9 @@ reveal_type(not AlwaysFalse())
|
|||
|
||||
# We don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin:
|
||||
class BoolIsBool:
|
||||
__bool__ = bool
|
||||
# TODO: The `type[bool]` declaration here is a workaround to avoid running into
|
||||
# https://github.com/astral-sh/ruff/issues/15672
|
||||
__bool__: type[bool] = bool
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not BoolIsBool())
|
||||
|
|
|
@ -76,11 +76,11 @@ with Manager():
|
|||
|
||||
```py
|
||||
class Manager:
|
||||
__enter__ = 42
|
||||
__enter__: int = 42
|
||||
|
||||
def __exit__(self, exc_tpe, exc_value, traceback): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` of type `Literal[42]` is not callable"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` of type `int` is not callable"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
@ -91,9 +91,9 @@ with Manager():
|
|||
class Manager:
|
||||
def __enter__(self) -> Self: ...
|
||||
|
||||
__exit__ = 32
|
||||
__exit__: int = 32
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__exit__` of type `Literal[32]` is not callable"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__exit__` of type `int` is not callable"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
|
|
@ -85,6 +85,14 @@ impl<'db> Symbol<'db> {
|
|||
Symbol::Unbound => self,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn map_type(self, f: impl FnOnce(Type<'db>) -> Type<'db>) -> Symbol<'db> {
|
||||
match self {
|
||||
Symbol::Type(ty, boundness) => Symbol::Type(f(ty), boundness),
|
||||
Symbol::Unbound => Symbol::Unbound,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -80,29 +80,54 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
|
|||
diagnostics
|
||||
}
|
||||
|
||||
/// Computes a possibly-widened type `Unknown | T_inferred` from the inferred type `T_inferred`
|
||||
/// of a symbol, unless the type is a known-instance type (e.g. `typing.Any`) or the symbol is
|
||||
/// considered non-modifiable (e.g. when the symbol is `@Final`). We need this for public uses
|
||||
/// of symbols that have no declared type.
|
||||
fn widen_type_for_undeclared_public_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
inferred: Symbol<'db>,
|
||||
is_considered_non_modifiable: bool,
|
||||
) -> Symbol<'db> {
|
||||
// We special-case known-instance types here since symbols like `typing.Any` are typically
|
||||
// not declared in the stubs (e.g. `Any = object()`), but we still want to treat them as
|
||||
// such.
|
||||
let is_known_instance = inferred
|
||||
.ignore_possibly_unbound()
|
||||
.is_some_and(|ty| matches!(ty, Type::KnownInstance(_)));
|
||||
|
||||
if is_considered_non_modifiable || is_known_instance {
|
||||
inferred
|
||||
} else {
|
||||
inferred.map_type(|ty| UnionType::from_elements(db, [Type::unknown(), ty]))
|
||||
}
|
||||
}
|
||||
|
||||
/// Infer the public type of a symbol (its type as seen from outside its scope).
|
||||
fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> {
|
||||
#[salsa::tracked]
|
||||
fn symbol_by_id<'db>(
|
||||
db: &'db dyn Db,
|
||||
scope: ScopeId<'db>,
|
||||
symbol: ScopedSymbolId,
|
||||
is_dunder_slots: bool,
|
||||
symbol_id: ScopedSymbolId,
|
||||
) -> Symbol<'db> {
|
||||
let use_def = use_def_map(db, scope);
|
||||
|
||||
// If the symbol is declared, the public type is based on declarations; otherwise, it's based
|
||||
// on inference from bindings.
|
||||
|
||||
let declarations = use_def.public_declarations(symbol);
|
||||
let declared =
|
||||
symbol_from_declarations(db, declarations).map(|SymbolAndQualifiers(ty, _)| ty);
|
||||
let declarations = use_def.public_declarations(symbol_id);
|
||||
let declared = symbol_from_declarations(db, declarations);
|
||||
let is_final = declared.as_ref().is_ok_and(SymbolAndQualifiers::is_final);
|
||||
let declared = declared.map(|SymbolAndQualifiers(symbol, _)| symbol);
|
||||
|
||||
match declared {
|
||||
// Symbol is declared, trust the declared type
|
||||
Ok(symbol @ Symbol::Type(_, Boundness::Bound)) => symbol,
|
||||
// Symbol is possibly declared
|
||||
Ok(Symbol::Type(declared_ty, Boundness::PossiblyUnbound)) => {
|
||||
let bindings = use_def.public_bindings(symbol);
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
|
||||
match inferred {
|
||||
|
@ -120,12 +145,14 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
|||
),
|
||||
}
|
||||
}
|
||||
// Symbol is undeclared, return the inferred type
|
||||
// Symbol is undeclared, return the union of `Unknown` with the inferred type
|
||||
Ok(Symbol::Unbound) => {
|
||||
let bindings = use_def.public_bindings(symbol);
|
||||
symbol_from_bindings(db, bindings)
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
|
||||
widen_type_for_undeclared_public_symbol(db, inferred, is_dunder_slots || is_final)
|
||||
}
|
||||
// Symbol is possibly undeclared
|
||||
// Symbol has conflicting declared types
|
||||
Err((declared_ty, _)) => {
|
||||
// Intentionally ignore conflicting declared types; that's not our problem,
|
||||
// it's the problem of the module we are importing from.
|
||||
|
@ -177,9 +204,15 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
|||
}
|
||||
|
||||
let table = symbol_table(db, scope);
|
||||
// `__slots__` is a symbol with special behavior in Python's runtime. It can be
|
||||
// modified externally, but those changes do not take effect. We therefore issue
|
||||
// a diagnostic if we see it being modified externally. In type inference, we
|
||||
// can assign a "narrow" type to it even if it is not *declared*. This means, we
|
||||
// do not have to call [`widen_type_for_undeclared_public_symbol`].
|
||||
let is_dunder_slots = name == "__slots__";
|
||||
table
|
||||
.symbol_id_by_name(name)
|
||||
.map(|symbol| symbol_by_id(db, scope, symbol))
|
||||
.map(|symbol| symbol_by_id(db, scope, is_dunder_slots, symbol))
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
}
|
||||
|
||||
|
@ -378,6 +411,10 @@ impl SymbolAndQualifiers<'_> {
|
|||
fn is_class_var(&self) -> bool {
|
||||
self.1.contains(TypeQualifiers::CLASS_VAR)
|
||||
}
|
||||
|
||||
fn is_final(&self) -> bool {
|
||||
self.1.contains(TypeQualifiers::FINAL)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Symbol<'db>> for SymbolAndQualifiers<'db> {
|
||||
|
@ -4076,7 +4113,7 @@ impl<'db> Class<'db> {
|
|||
/// this class, not on its superclasses.
|
||||
fn own_instance_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
|
||||
// TODO: There are many things that are not yet implemented here:
|
||||
// - `typing.ClassVar` and `typing.Final`
|
||||
// - `typing.Final`
|
||||
// - Proper diagnostics
|
||||
// - Handling of possibly-undeclared/possibly-unbound attributes
|
||||
// - The descriptor protocol
|
||||
|
@ -4084,10 +4121,10 @@ impl<'db> Class<'db> {
|
|||
let body_scope = self.body_scope(db);
|
||||
let table = symbol_table(db, body_scope);
|
||||
|
||||
if let Some(symbol) = table.symbol_id_by_name(name) {
|
||||
if let Some(symbol_id) = table.symbol_id_by_name(name) {
|
||||
let use_def = use_def_map(db, body_scope);
|
||||
|
||||
let declarations = use_def.public_declarations(symbol);
|
||||
let declarations = use_def.public_declarations(symbol_id);
|
||||
|
||||
match symbol_from_declarations(db, declarations) {
|
||||
Ok(SymbolAndQualifiers(Symbol::Type(declared_ty, _), qualifiers)) => {
|
||||
|
@ -4104,20 +4141,14 @@ impl<'db> Class<'db> {
|
|||
SymbolAndQualifiers(Symbol::Type(declared_ty, Boundness::Bound), qualifiers)
|
||||
}
|
||||
}
|
||||
Ok(SymbolAndQualifiers(Symbol::Unbound, qualifiers)) => {
|
||||
let bindings = use_def.public_bindings(symbol);
|
||||
Ok(symbol @ SymbolAndQualifiers(Symbol::Unbound, qualifiers)) => {
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
|
||||
match inferred {
|
||||
Symbol::Type(ty, _) => SymbolAndQualifiers(
|
||||
Symbol::Type(
|
||||
UnionType::from_elements(db, [Type::unknown(), ty]),
|
||||
Boundness::Bound,
|
||||
),
|
||||
qualifiers,
|
||||
),
|
||||
Symbol::Unbound => SymbolAndQualifiers(Symbol::Unbound, qualifiers),
|
||||
}
|
||||
SymbolAndQualifiers(
|
||||
widen_type_for_undeclared_public_symbol(db, inferred, symbol.is_final()),
|
||||
qualifiers,
|
||||
)
|
||||
}
|
||||
Err((declared_ty, _conflicting_declarations)) => {
|
||||
// Ignore conflicting declarations
|
||||
|
@ -4694,7 +4725,10 @@ pub(crate) mod tests {
|
|||
let bar = system_path_to_file(&db, "src/bar.py")?;
|
||||
let a = global_symbol(&db, bar, "a");
|
||||
|
||||
assert_eq!(a.expect_type(), KnownClass::Int.to_instance(&db));
|
||||
assert_eq!(
|
||||
a.expect_type(),
|
||||
UnionType::from_elements(&db, [Type::unknown(), KnownClass::Int.to_instance(&db)])
|
||||
);
|
||||
|
||||
// Add a docstring to foo to trigger a re-run.
|
||||
// The bar-call site of foo should not be re-run because of that
|
||||
|
@ -4710,7 +4744,10 @@ pub(crate) mod tests {
|
|||
|
||||
let a = global_symbol(&db, bar, "a");
|
||||
|
||||
assert_eq!(a.expect_type(), KnownClass::Int.to_instance(&db));
|
||||
assert_eq!(
|
||||
a.expect_type(),
|
||||
UnionType::from_elements(&db, [Type::unknown(), KnownClass::Int.to_instance(&db)])
|
||||
);
|
||||
let events = db.take_salsa_events();
|
||||
|
||||
let call = &*parsed_module(&db, bar).syntax().body[1]
|
||||
|
|
|
@ -543,7 +543,10 @@ mod tests {
|
|||
assert_eq!(a_name, "a");
|
||||
assert_eq!(b_name, "b");
|
||||
// TODO resolution should not be deferred; we should see A not B
|
||||
assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "B");
|
||||
assert_eq!(
|
||||
a_annotated_ty.unwrap().display(&db).to_string(),
|
||||
"Unknown | B"
|
||||
);
|
||||
assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T");
|
||||
}
|
||||
|
||||
|
@ -583,7 +586,10 @@ mod tests {
|
|||
assert_eq!(a_name, "a");
|
||||
assert_eq!(b_name, "b");
|
||||
// Parameter resolution deferred; we should see B
|
||||
assert_eq!(a_annotated_ty.unwrap().display(&db).to_string(), "B");
|
||||
assert_eq!(
|
||||
a_annotated_ty.unwrap().display(&db).to_string(),
|
||||
"Unknown | B"
|
||||
);
|
||||
assert_eq!(b_annotated_ty.unwrap().display(&db).to_string(), "T");
|
||||
}
|
||||
|
||||
|
|
|
@ -43,10 +43,10 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
|
|||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined",
|
||||
// We don't handle intersections in `is_assignable_to` yet
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:626:46 Object of type `@Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_datetime`; expected type `Match`",
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:626:46 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_datetime`; expected type `Match`",
|
||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined",
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:632:58 Object of type `@Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_localtime`; expected type `Match`",
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:639:52 Object of type `@Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_number`; expected type `Match`",
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:632:58 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_localtime`; expected type `Match`",
|
||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:639:52 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_number`; expected type `Match`",
|
||||
"warning[lint:unused-ignore-comment] /src/tomllib/_parser.py:682:31 Unused blanket `type: ignore` directive",
|
||||
];
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue