mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 14:21:24 +00:00
Refactor CallOutcome
to Result
(#16161)
This commit is contained in:
parent
5cd0de3e4c
commit
4ed5db0d42
19 changed files with 717 additions and 737 deletions
|
@ -40,7 +40,7 @@ class C:
|
|||
return 42
|
||||
|
||||
x = C()
|
||||
# error: [invalid-argument-type]
|
||||
# error: [unsupported-operator] "Operator `-=` is unsupported between objects of type `C` and `Literal[1]`"
|
||||
x -= 1
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
|
|
|
@ -244,10 +244,7 @@ class B:
|
|||
def __rsub__(self, other: A) -> B:
|
||||
return B()
|
||||
|
||||
# TODO: this should be `B` (the return annotation of `B.__rsub__`),
|
||||
# because `A.__sub__` is annotated as only accepting `A`,
|
||||
# but `B.__rsub__` will accept `A`.
|
||||
reveal_type(A() - B()) # revealed: A
|
||||
reveal_type(A() - B()) # revealed: B
|
||||
```
|
||||
|
||||
## Callable instances as dunders
|
||||
|
@ -263,7 +260,10 @@ class B:
|
|||
__add__ = A()
|
||||
|
||||
# TODO: this could be `int` if we declare `B.__add__` using a `Callable` type
|
||||
reveal_type(B() + B()) # revealed: Unknown | int
|
||||
# TODO: Should not be an error: `A` instance is not a method descriptor, don't prepend `self` arg.
|
||||
# Revealed type should be `Unknown | int`.
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `B` and `B`"
|
||||
reveal_type(B() + B()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Integration test: numbers from typeshed
|
||||
|
@ -277,22 +277,14 @@ return annotations from the widening, and preserve a bit more precision here?
|
|||
reveal_type(3j + 3.14) # revealed: int | float | complex
|
||||
reveal_type(4.2 + 42) # revealed: int | float
|
||||
reveal_type(3j + 3) # revealed: int | float | complex
|
||||
|
||||
# TODO should be int | float | complex, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(3.14 + 3j) # revealed: int | float
|
||||
|
||||
# TODO should be int | float, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(42 + 4.2) # revealed: int
|
||||
|
||||
# TODO should be int | float | complex, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(3 + 3j) # revealed: int
|
||||
reveal_type(3.14 + 3j) # revealed: int | float | complex
|
||||
reveal_type(42 + 4.2) # revealed: int | float
|
||||
reveal_type(3 + 3j) # revealed: int | float | complex
|
||||
|
||||
def _(x: bool, y: int):
|
||||
reveal_type(x + y) # revealed: int
|
||||
reveal_type(4.2 + x) # revealed: int | float
|
||||
|
||||
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
|
||||
reveal_type(y + 4.12) # revealed: int
|
||||
reveal_type(y + 4.12) # revealed: int | float
|
||||
```
|
||||
|
||||
## With literal types
|
||||
|
@ -309,8 +301,7 @@ class A:
|
|||
return self
|
||||
|
||||
reveal_type(A() + 1) # revealed: A
|
||||
# TODO should be `A` since `int.__add__` doesn't support `A` instances
|
||||
reveal_type(1 + A()) # revealed: int
|
||||
reveal_type(1 + A()) # revealed: A
|
||||
|
||||
reveal_type(A() + "foo") # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
|
|
|
@ -10,9 +10,10 @@ reveal_type(-3 // 3) # revealed: Literal[-1]
|
|||
reveal_type(-3 / 3) # revealed: float
|
||||
reveal_type(5 % 3) # revealed: Literal[2]
|
||||
|
||||
# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared
|
||||
# formal parameter type.
|
||||
reveal_type(2 + "f") # revealed: int
|
||||
# TODO: This should emit an unsupported-operator error but we don't currently
|
||||
# verify that the actual parameter to `int.__add__` matches the declared
|
||||
# formal parameter type.
|
||||
reveal_type(2 + "f") # revealed: Unknown
|
||||
|
||||
def lhs(x: int):
|
||||
reveal_type(x + 1) # revealed: int
|
||||
|
|
|
@ -52,7 +52,7 @@ class NonCallable:
|
|||
__call__ = 1
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `Unknown | Literal[1]` is not callable (due to union element `Literal[1]`)"
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
reveal_type(a()) # revealed: Unknown
|
||||
```
|
||||
|
||||
|
@ -67,8 +67,8 @@ def _(flag: bool):
|
|||
def __call__(self) -> int: ...
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: Unknown | int
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
reveal_type(a()) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
## Call binding errors
|
||||
|
@ -99,3 +99,26 @@ c = C()
|
|||
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of function `__call__`; expected type `int`"
|
||||
reveal_type(c()) # revealed: int
|
||||
```
|
||||
|
||||
## Union over callables
|
||||
|
||||
### Possibly unbound `__call__`
|
||||
|
||||
```py
|
||||
def outer(cond1: bool):
|
||||
class Test:
|
||||
if cond1:
|
||||
def __call__(self): ...
|
||||
|
||||
class Other:
|
||||
def __call__(self): ...
|
||||
|
||||
def inner(cond2: bool):
|
||||
if cond2:
|
||||
a = Test()
|
||||
else:
|
||||
a = Other()
|
||||
|
||||
# error: [call-non-callable] "Object of type `Test` is not callable (possibly unbound `__call__` method)"
|
||||
a()
|
||||
```
|
||||
|
|
|
@ -278,10 +278,10 @@ proper diagnostics in case of missing or superfluous arguments.
|
|||
from typing_extensions import reveal_type
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`"
|
||||
reveal_type() # revealed: Unknown
|
||||
reveal_type()
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2"
|
||||
reveal_type(1, 2) # revealed: Literal[1]
|
||||
reveal_type(1, 2)
|
||||
```
|
||||
|
||||
### `static_assert`
|
||||
|
@ -290,7 +290,6 @@ reveal_type(1, 2) # revealed: Literal[1]
|
|||
from knot_extensions import static_assert
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`"
|
||||
# error: [static-assert-error]
|
||||
static_assert()
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3"
|
||||
|
|
|
@ -39,8 +39,8 @@ def _(flag: bool):
|
|||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(x) # revealed: Unknown | int
|
||||
x = f() # error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
reveal_type(x) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
## Multiple non-callable elements in a union
|
||||
|
@ -56,8 +56,8 @@ def _(flag: bool, flag2: bool):
|
|||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
# error: "Object of type `Literal[1, "foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
|
||||
# revealed: Unknown | int
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
# revealed: int | Unknown
|
||||
reveal_type(f())
|
||||
```
|
||||
|
||||
|
@ -72,6 +72,39 @@ def _(flag: bool):
|
|||
else:
|
||||
f = "foo"
|
||||
|
||||
x = f() # error: "Object of type `Literal[1, "foo"]` is not callable"
|
||||
x = f() # error: [call-non-callable] "Object of type `Literal[1, "foo"]` is not callable"
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Mismatching signatures
|
||||
|
||||
Calling a union where the arguments don't match the signature of all variants.
|
||||
|
||||
```py
|
||||
def f1(a: int) -> int: ...
|
||||
def f2(a: str) -> str: ...
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = f1
|
||||
else:
|
||||
f = f2
|
||||
|
||||
# error: [invalid-argument-type] "Object of type `Literal[3]` cannot be assigned to parameter 1 (`a`) of function `f2`; expected type `str`"
|
||||
x = f(3)
|
||||
reveal_type(x) # revealed: int | str
|
||||
```
|
||||
|
||||
## Any non-callable variant
|
||||
|
||||
```py
|
||||
def f1(a: int): ...
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = f1
|
||||
else:
|
||||
f = "This is a string literal"
|
||||
|
||||
# error: [call-non-callable] "Object of type `Literal["This is a string literal"]` is not callable"
|
||||
x = f(3)
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
|
|
@ -21,8 +21,9 @@ class A:
|
|||
|
||||
reveal_type("hello" in A()) # revealed: bool
|
||||
reveal_type("hello" not in A()) # revealed: bool
|
||||
# TODO: should emit diagnostic, need to check arg type, will fail
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
|
||||
reveal_type(42 in A()) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `not in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
|
||||
reveal_type(42 not in A()) # revealed: bool
|
||||
```
|
||||
|
||||
|
@ -126,9 +127,9 @@ class A:
|
|||
|
||||
reveal_type(CheckContains() in A()) # revealed: bool
|
||||
|
||||
# TODO: should emit diagnostic, need to check arg type,
|
||||
# should not fall back to __iter__ or __getitem__
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `CheckIter` and `A`"
|
||||
reveal_type(CheckIter() in A()) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `CheckGetItem` and `A`"
|
||||
reveal_type(CheckGetItem() in A()) # revealed: bool
|
||||
|
||||
class B:
|
||||
|
@ -154,7 +155,8 @@ class A:
|
|||
def __getitem__(self, key: str) -> str:
|
||||
return "foo"
|
||||
|
||||
# TODO should emit a diagnostic
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
|
||||
reveal_type(42 in A()) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `str` and `A`, in comparing `Literal["hello"]` with `A`"
|
||||
reveal_type("hello" in A()) # revealed: bool
|
||||
```
|
||||
|
|
|
@ -117,14 +117,11 @@ class B:
|
|||
def __ne__(self, other: str) -> B:
|
||||
return B()
|
||||
|
||||
# TODO: should be `int` and `bytearray`.
|
||||
# Need to check arg type and fall back to `rhs.__eq__` and `rhs.__ne__`.
|
||||
#
|
||||
# Because `object.__eq__` and `object.__ne__` accept `object` in typeshed,
|
||||
# this can only happen with an invalid override of these methods,
|
||||
# but we still support it.
|
||||
reveal_type(B() == A()) # revealed: B
|
||||
reveal_type(B() != A()) # revealed: B
|
||||
reveal_type(B() == A()) # revealed: int
|
||||
reveal_type(B() != A()) # revealed: bytearray
|
||||
|
||||
reveal_type(B() < A()) # revealed: list
|
||||
reveal_type(B() <= A()) # revealed: set
|
||||
|
@ -222,9 +219,8 @@ class B(A):
|
|||
def __gt__(self, other: int) -> B:
|
||||
return B()
|
||||
|
||||
# TODO: should be `A`, need to check argument type and fall back to LHS method
|
||||
reveal_type(A() < B()) # revealed: B
|
||||
reveal_type(A() > B()) # revealed: B
|
||||
reveal_type(A() < B()) # revealed: A
|
||||
reveal_type(A() > B()) # revealed: A
|
||||
```
|
||||
|
||||
## Operations involving instances of classes inheriting from `Any`
|
||||
|
@ -272,9 +268,8 @@ class A:
|
|||
def __ne__(self, other: int) -> A:
|
||||
return A()
|
||||
|
||||
# TODO: it should be `bool`, need to check arg type and fall back to `is` and `is not`
|
||||
reveal_type(A() == A()) # revealed: A
|
||||
reveal_type(A() != A()) # revealed: A
|
||||
reveal_type(A() == A()) # revealed: bool
|
||||
reveal_type(A() != A()) # revealed: bool
|
||||
```
|
||||
|
||||
## Object Comparisons with Typeshed
|
||||
|
@ -305,12 +300,14 @@ reveal_type(1 >= 1.0) # revealed: bool
|
|||
reveal_type(1 == 2j) # revealed: bool
|
||||
reveal_type(1 != 2j) # revealed: bool
|
||||
|
||||
# TODO: should be Unknown and emit diagnostic,
|
||||
# need to check arg type and should be failed
|
||||
reveal_type(1 < 2j) # revealed: bool
|
||||
reveal_type(1 <= 2j) # revealed: bool
|
||||
reveal_type(1 > 2j) # revealed: bool
|
||||
reveal_type(1 >= 2j) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
|
||||
reveal_type(1 < 2j) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
|
||||
reveal_type(1 <= 2j) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
|
||||
reveal_type(1 > 2j) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
|
||||
reveal_type(1 >= 2j) # revealed: Unknown
|
||||
|
||||
def f(x: bool, y: int):
|
||||
reveal_type(x < y) # revealed: bool
|
||||
|
|
|
@ -12,8 +12,8 @@ reveal_type(1 is 1) # revealed: bool
|
|||
reveal_type(1 is not 1) # revealed: bool
|
||||
reveal_type(1 is 2) # revealed: Literal[False]
|
||||
reveal_type(1 is not 7) # revealed: Literal[True]
|
||||
# TODO: should be Unknown, and emit diagnostic, once we check call argument types
|
||||
reveal_type(1 <= "" and 0 < 1) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`"
|
||||
reveal_type(1 <= "" and 0 < 1) # revealed: Unknown & ~AlwaysTruthy | Literal[True]
|
||||
```
|
||||
|
||||
## Integer instance
|
||||
|
|
|
@ -8,7 +8,9 @@ types, we can infer that the result for the intersection type is also true/false
|
|||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Base: ...
|
||||
class Base:
|
||||
def __gt__(self, other) -> bool:
|
||||
return False
|
||||
|
||||
class Child1(Base):
|
||||
def __eq__(self, other) -> Literal[True]:
|
||||
|
|
|
@ -23,6 +23,7 @@ from __future__ import annotations
|
|||
|
||||
class A:
|
||||
def __lt__(self, other) -> A: ...
|
||||
def __gt__(self, other) -> bool: ...
|
||||
|
||||
class B:
|
||||
def __lt__(self, other) -> B: ...
|
||||
|
|
|
@ -92,11 +92,14 @@ reveal_type(a == b) # revealed: bool
|
|||
# TODO: should be Literal[True], once we implement (in)equality for mismatched literals
|
||||
reveal_type(a != b) # revealed: bool
|
||||
|
||||
# TODO: should be Unknown and add more informative diagnostics
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
|
||||
reveal_type(a < b) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
|
||||
reveal_type(a <= b) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
|
||||
reveal_type(a > b) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
|
||||
reveal_type(a >= b) # revealed: Unknown
|
||||
```
|
||||
|
||||
However, if the lexicographic comparison completes without reaching a point where str and int are
|
||||
|
|
|
@ -9,28 +9,22 @@ def _(flag: bool, flag1: bool, flag2: bool):
|
|||
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
|
||||
reveal_type(b) # revealed: bool
|
||||
|
||||
# TODO: should error, once operand type check is implemented
|
||||
# ("Operator `<` is not supported for types `object` and `int`")
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `object` and `int`, in comparing `object` with `Literal[5]`"
|
||||
c = object() < 5
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(c) # revealed: bool
|
||||
reveal_type(c) # revealed: Unknown
|
||||
|
||||
# TODO: should error, once operand type check is implemented
|
||||
# ("Operator `<` is not supported for types `int` and `object`")
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `object`, in comparing `Literal[5]` with `object`"
|
||||
d = 5 < object()
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(d) # revealed: bool
|
||||
reveal_type(d) # revealed: Unknown
|
||||
|
||||
int_literal_or_str_literal = 1 if flag else "foo"
|
||||
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1, "foo"]`"
|
||||
e = 42 in int_literal_or_str_literal
|
||||
reveal_type(e) # revealed: bool
|
||||
|
||||
# TODO: should error, need to check if __lt__ signature is valid for right operand
|
||||
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
|
||||
f = (1, 2) < (1, "hello")
|
||||
# TODO: should be Unknown, once operand type check is implemented
|
||||
reveal_type(f) # revealed: bool
|
||||
reveal_type(f) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
|
||||
g = (flag1, A()) < (flag2, A())
|
||||
|
|
|
@ -245,9 +245,10 @@ class Test2:
|
|||
return 42
|
||||
|
||||
def _(flag: bool):
|
||||
# TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
|
||||
# error: "Object of type `Test | Test2` is not iterable"
|
||||
for x in Test() if flag else Test2():
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Union type as iterator where one union element has no `__next__` method
|
||||
|
@ -263,5 +264,5 @@ class Test:
|
|||
|
||||
# error: [not-iterable] "Object of type `Test` is not iterable"
|
||||
for x in Test():
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
|
|
@ -80,7 +80,7 @@ class Manager:
|
|||
|
||||
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 `int` is not callable"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
@ -95,7 +95,7 @@ class Manager:
|
|||
|
||||
__exit__: int = 32
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__exit__` of type `int` is not callable"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__exit__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
@ -134,3 +134,19 @@ def _(flag: bool):
|
|||
with Manager() as f:
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
## Invalid `__enter__` signature
|
||||
|
||||
```py
|
||||
class Manager:
|
||||
def __enter__() -> str:
|
||||
return "foo"
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback): ...
|
||||
|
||||
context_expr = Manager()
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`"
|
||||
with context_expr as f:
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use std::hash::Hash;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use call::{CallDunderError, CallError};
|
||||
use context::InferContext;
|
||||
use diagnostic::{report_not_iterable, report_not_iterable_possibly_unbound};
|
||||
use indexmap::IndexSet;
|
||||
use itertools::Itertools;
|
||||
use ruff_db::diagnostic::Severity;
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::python_version::PythonVersion;
|
||||
|
@ -36,9 +36,7 @@ use crate::symbol::{
|
|||
global_symbol, imported_symbol, known_module_symbol, symbol, symbol_from_bindings,
|
||||
symbol_from_declarations, Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers,
|
||||
};
|
||||
use crate::types::call::{
|
||||
bind_call, CallArguments, CallBinding, CallDunderResult, CallOutcome, StaticAssertionErrorKind,
|
||||
};
|
||||
use crate::types::call::{bind_call, CallArguments, CallBinding, CallOutcome};
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::diagnostic::INVALID_TYPE_FORM;
|
||||
use crate::types::infer::infer_unpack_types;
|
||||
|
@ -1469,9 +1467,9 @@ impl<'db> Type<'db> {
|
|||
return Truthiness::Ambiguous;
|
||||
};
|
||||
|
||||
if let Some(Type::BooleanLiteral(bool_val)) = bool_method
|
||||
if let Ok(Type::BooleanLiteral(bool_val)) = bool_method
|
||||
.call_bound(db, instance_ty, &CallArguments::positional([]))
|
||||
.return_type(db)
|
||||
.map(|outcome| outcome.return_type(db))
|
||||
{
|
||||
bool_val.into()
|
||||
} else {
|
||||
|
@ -1544,72 +1542,39 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
|
||||
let return_ty = match self.call_dunder(db, "__len__", &CallArguments::positional([*self])) {
|
||||
// TODO: emit a diagnostic
|
||||
CallDunderResult::MethodNotAvailable => return None,
|
||||
Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => outcome.return_type(db),
|
||||
|
||||
CallDunderResult::CallOutcome(outcome) | CallDunderResult::PossiblyUnbound(outcome) => {
|
||||
outcome.return_type(db)?
|
||||
}
|
||||
// TODO: emit a diagnostic
|
||||
Err(err) => err.return_type(db)?,
|
||||
};
|
||||
|
||||
non_negative_int_literal(db, return_ty)
|
||||
}
|
||||
|
||||
/// Return the outcome of calling an object of this type.
|
||||
#[must_use]
|
||||
fn call(self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) -> CallOutcome<'db> {
|
||||
/// Calls `self`
|
||||
///
|
||||
/// Returns `Ok` if the call with the given arguments is successful and `Err` otherwise.
|
||||
fn call(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
) -> Result<CallOutcome<'db>, CallError<'db>> {
|
||||
match self {
|
||||
Type::FunctionLiteral(function_type) => {
|
||||
let mut binding = bind_call(db, arguments, function_type.signature(db), self);
|
||||
match function_type.known(db) {
|
||||
Some(KnownFunction::RevealType) => {
|
||||
let revealed_ty = binding.one_parameter_type().unwrap_or(Type::unknown());
|
||||
CallOutcome::revealed(binding, revealed_ty)
|
||||
}
|
||||
Some(KnownFunction::StaticAssert) => {
|
||||
if let Some((parameter_ty, message)) = binding.two_parameter_types() {
|
||||
let truthiness = parameter_ty.bool(db);
|
||||
|
||||
if truthiness.is_always_true() {
|
||||
CallOutcome::callable(binding)
|
||||
} else {
|
||||
let error_kind = if let Some(message) =
|
||||
message.into_string_literal().map(|s| &**s.value(db))
|
||||
{
|
||||
StaticAssertionErrorKind::CustomError(message)
|
||||
} else if parameter_ty == Type::BooleanLiteral(false) {
|
||||
StaticAssertionErrorKind::ArgumentIsFalse
|
||||
} else if truthiness.is_always_false() {
|
||||
StaticAssertionErrorKind::ArgumentIsFalsy(parameter_ty)
|
||||
} else {
|
||||
StaticAssertionErrorKind::ArgumentTruthinessIsAmbiguous(
|
||||
parameter_ty,
|
||||
)
|
||||
};
|
||||
|
||||
CallOutcome::StaticAssertionError {
|
||||
binding,
|
||||
error_kind,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
}
|
||||
Some(KnownFunction::IsEquivalentTo) => {
|
||||
let (ty_a, ty_b) = binding
|
||||
.two_parameter_types()
|
||||
.unwrap_or((Type::unknown(), Type::unknown()));
|
||||
binding
|
||||
.set_return_type(Type::BooleanLiteral(ty_a.is_equivalent_to(db, ty_b)));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
Some(KnownFunction::IsSubtypeOf) => {
|
||||
let (ty_a, ty_b) = binding
|
||||
.two_parameter_types()
|
||||
.unwrap_or((Type::unknown(), Type::unknown()));
|
||||
binding.set_return_type(Type::BooleanLiteral(ty_a.is_subtype_of(db, ty_b)));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
Some(KnownFunction::IsAssignableTo) => {
|
||||
let (ty_a, ty_b) = binding
|
||||
|
@ -1617,7 +1582,6 @@ impl<'db> Type<'db> {
|
|||
.unwrap_or((Type::unknown(), Type::unknown()));
|
||||
binding
|
||||
.set_return_type(Type::BooleanLiteral(ty_a.is_assignable_to(db, ty_b)));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
Some(KnownFunction::IsDisjointFrom) => {
|
||||
let (ty_a, ty_b) = binding
|
||||
|
@ -1625,7 +1589,6 @@ impl<'db> Type<'db> {
|
|||
.unwrap_or((Type::unknown(), Type::unknown()));
|
||||
binding
|
||||
.set_return_type(Type::BooleanLiteral(ty_a.is_disjoint_from(db, ty_b)));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
Some(KnownFunction::IsGradualEquivalentTo) => {
|
||||
let (ty_a, ty_b) = binding
|
||||
|
@ -1634,22 +1597,18 @@ impl<'db> Type<'db> {
|
|||
binding.set_return_type(Type::BooleanLiteral(
|
||||
ty_a.is_gradual_equivalent_to(db, ty_b),
|
||||
));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
Some(KnownFunction::IsFullyStatic) => {
|
||||
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
|
||||
binding.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db)));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
Some(KnownFunction::IsSingleton) => {
|
||||
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
|
||||
binding.set_return_type(Type::BooleanLiteral(ty.is_singleton(db)));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
Some(KnownFunction::IsSingleValued) => {
|
||||
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
|
||||
binding.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db)));
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
|
||||
Some(KnownFunction::Len) => {
|
||||
|
@ -1658,108 +1617,111 @@ impl<'db> Type<'db> {
|
|||
binding.set_return_type(len_ty);
|
||||
}
|
||||
};
|
||||
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
|
||||
Some(KnownFunction::Repr) => {
|
||||
if let Some(first_arg) = binding.one_parameter_type() {
|
||||
binding.set_return_type(first_arg.repr(db));
|
||||
};
|
||||
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
|
||||
Some(KnownFunction::AssertType) => {
|
||||
let Some((_, asserted_ty)) = binding.two_parameter_types() else {
|
||||
return CallOutcome::callable(binding);
|
||||
};
|
||||
|
||||
CallOutcome::asserted(binding, asserted_ty)
|
||||
}
|
||||
|
||||
Some(KnownFunction::Cast) => {
|
||||
// TODO: Use `.two_parameter_tys()` exclusively
|
||||
// when overloads are supported.
|
||||
if binding.two_parameter_types().is_none() {
|
||||
return CallOutcome::callable(binding);
|
||||
};
|
||||
|
||||
if let Some(casted_ty) = arguments.first_argument() {
|
||||
binding.set_return_type(casted_ty);
|
||||
if binding.two_parameter_types().is_some() {
|
||||
binding.set_return_type(casted_ty);
|
||||
}
|
||||
};
|
||||
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
|
||||
_ => CallOutcome::callable(binding),
|
||||
_ => {}
|
||||
};
|
||||
|
||||
if binding.has_binding_errors() {
|
||||
Err(CallError::BindingError { binding })
|
||||
} else {
|
||||
Ok(CallOutcome::Single(binding))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO annotated return type on `__new__` or metaclass `__call__`
|
||||
// TODO check call vs signatures of `__new__` and/or `__init__`
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => {
|
||||
CallOutcome::callable(CallBinding::from_return_type(match class.known(db) {
|
||||
// If the class is the builtin-bool class (for example `bool(1)`), we try to
|
||||
// return the specific truthiness value of the input arg, `Literal[True]` for
|
||||
// the example above.
|
||||
Some(KnownClass::Bool) => arguments
|
||||
.first_argument()
|
||||
.map(|arg| arg.bool(db).into_type(db))
|
||||
.unwrap_or(Type::BooleanLiteral(false)),
|
||||
Ok(CallOutcome::Single(CallBinding::from_return_type(
|
||||
match class.known(db) {
|
||||
// If the class is the builtin-bool class (for example `bool(1)`), we try to
|
||||
// return the specific truthiness value of the input arg, `Literal[True]` for
|
||||
// the example above.
|
||||
Some(KnownClass::Bool) => arguments
|
||||
.first_argument()
|
||||
.map(|arg| arg.bool(db).into_type(db))
|
||||
.unwrap_or(Type::BooleanLiteral(false)),
|
||||
|
||||
Some(KnownClass::Str) => arguments
|
||||
.first_argument()
|
||||
.map(|arg| arg.str(db))
|
||||
.unwrap_or(Type::string_literal(db, "")),
|
||||
// TODO: Don't ignore the second and third arguments to `str`
|
||||
// https://github.com/astral-sh/ruff/pull/16161#discussion_r1958425568
|
||||
Some(KnownClass::Str) => arguments
|
||||
.first_argument()
|
||||
.map(|arg| arg.str(db))
|
||||
.unwrap_or(Type::string_literal(db, "")),
|
||||
|
||||
_ => Type::Instance(InstanceType { class }),
|
||||
}))
|
||||
_ => Type::Instance(InstanceType { class }),
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
instance_ty @ Type::Instance(_) => {
|
||||
match instance_ty.call_dunder(db, "__call__", &arguments.with_self(instance_ty)) {
|
||||
CallDunderResult::CallOutcome(CallOutcome::NotCallable { .. }) => {
|
||||
// Turn "`<type of illegal '__call__'>` not callable" into
|
||||
// "`X` not callable"
|
||||
CallOutcome::NotCallable {
|
||||
not_callable_ty: self,
|
||||
instance_ty
|
||||
.call_dunder(db, "__call__", &arguments.with_self(instance_ty))
|
||||
.map_err(|err| match err {
|
||||
CallDunderError::Call(CallError::NotCallable { .. }) => {
|
||||
// Turn "`<type of illegal '__call__'>` not callable" into
|
||||
// "`X` not callable"
|
||||
CallError::NotCallable {
|
||||
not_callable_ty: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
CallDunderResult::CallOutcome(outcome) => outcome,
|
||||
CallDunderResult::PossiblyUnbound(call_outcome) => {
|
||||
CallDunderError::Call(CallError::Union {
|
||||
called_ty: _,
|
||||
bindings,
|
||||
errors,
|
||||
}) => CallError::Union {
|
||||
called_ty: self,
|
||||
bindings,
|
||||
errors,
|
||||
},
|
||||
CallDunderError::Call(error) => error,
|
||||
// Turn "possibly unbound object of type `Literal['__call__']`"
|
||||
// into "`X` not callable (possibly unbound `__call__` method)"
|
||||
CallOutcome::PossiblyUnboundDunderCall {
|
||||
called_ty: self,
|
||||
call_outcome: Box::new(call_outcome),
|
||||
CallDunderError::PossiblyUnbound(outcome) => {
|
||||
CallError::PossiblyUnboundDunderCall {
|
||||
called_type: self,
|
||||
outcome: Box::new(outcome),
|
||||
}
|
||||
}
|
||||
}
|
||||
CallDunderResult::MethodNotAvailable => {
|
||||
// Turn "`X.__call__` unbound" into "`X` not callable"
|
||||
CallOutcome::NotCallable {
|
||||
not_callable_ty: self,
|
||||
CallDunderError::MethodNotAvailable => {
|
||||
// Turn "`X.__call__` unbound" into "`X` not callable"
|
||||
CallError::NotCallable {
|
||||
not_callable_ty: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Dynamic types are callable, and the return type is the same dynamic type
|
||||
Type::Dynamic(_) => CallOutcome::callable(CallBinding::from_return_type(self)),
|
||||
Type::Dynamic(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(self))),
|
||||
|
||||
Type::Union(union) => CallOutcome::union(
|
||||
self,
|
||||
union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|elem| elem.call(db, arguments)),
|
||||
),
|
||||
Type::Union(union) => {
|
||||
CallOutcome::try_call_union(db, union, |element| element.call(db, arguments))
|
||||
}
|
||||
|
||||
Type::Intersection(_) => CallOutcome::callable(CallBinding::from_return_type(
|
||||
Type::Intersection(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(
|
||||
todo_type!("Type::Intersection.call()"),
|
||||
)),
|
||||
))),
|
||||
|
||||
_ => CallOutcome::not_callable(self),
|
||||
_ => Err(CallError::NotCallable {
|
||||
not_callable_ty: self,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1769,13 +1731,12 @@ impl<'db> Type<'db> {
|
|||
/// `receiver_ty` must be `Type::Instance(_)` or `Type::ClassLiteral`.
|
||||
///
|
||||
/// TODO: handle `super()` objects properly
|
||||
#[must_use]
|
||||
fn call_bound(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
receiver_ty: &Type<'db>,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
) -> CallOutcome<'db> {
|
||||
) -> Result<CallOutcome<'db>, CallError<'db>> {
|
||||
debug_assert!(receiver_ty.is_instance() || receiver_ty.is_class_literal());
|
||||
|
||||
match self {
|
||||
|
@ -1790,22 +1751,20 @@ impl<'db> Type<'db> {
|
|||
self.call(db, arguments)
|
||||
}
|
||||
|
||||
Type::Union(union) => CallOutcome::union(
|
||||
self,
|
||||
union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|elem| elem.call_bound(db, receiver_ty, arguments)),
|
||||
),
|
||||
Type::Union(union) => CallOutcome::try_call_union(db, union, |element| {
|
||||
element.call_bound(db, receiver_ty, arguments)
|
||||
}),
|
||||
|
||||
Type::Intersection(_) => CallOutcome::callable(CallBinding::from_return_type(
|
||||
Type::Intersection(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(
|
||||
todo_type!("Type::Intersection.call_bound()"),
|
||||
)),
|
||||
))),
|
||||
|
||||
// Cases that duplicate, and thus must be kept in sync with, `Type::call()`
|
||||
Type::Dynamic(_) => CallOutcome::callable(CallBinding::from_return_type(self)),
|
||||
Type::Dynamic(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(self))),
|
||||
|
||||
_ => CallOutcome::not_callable(self),
|
||||
_ => Err(CallError::NotCallable {
|
||||
not_callable_ty: self,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1815,15 +1774,14 @@ impl<'db> Type<'db> {
|
|||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
) -> CallDunderResult<'db> {
|
||||
) -> Result<CallOutcome<'db>, CallDunderError<'db>> {
|
||||
match self.to_meta_type(db).member(db, name) {
|
||||
Symbol::Type(callable_ty, Boundness::Bound) => {
|
||||
CallDunderResult::CallOutcome(callable_ty.call(db, arguments))
|
||||
}
|
||||
Symbol::Type(callable_ty, Boundness::Bound) => Ok(callable_ty.call(db, arguments)?),
|
||||
Symbol::Type(callable_ty, Boundness::PossiblyUnbound) => {
|
||||
CallDunderResult::PossiblyUnbound(callable_ty.call(db, arguments))
|
||||
let call = callable_ty.call(db, arguments)?;
|
||||
Err(CallDunderError::PossiblyUnbound(call))
|
||||
}
|
||||
Symbol::Unbound => CallDunderResult::MethodNotAvailable,
|
||||
Symbol::Unbound => Err(CallDunderError::MethodNotAvailable),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1844,34 +1802,51 @@ impl<'db> Type<'db> {
|
|||
|
||||
let dunder_iter_result =
|
||||
self.call_dunder(db, "__iter__", &CallArguments::positional([self]));
|
||||
match dunder_iter_result {
|
||||
CallDunderResult::CallOutcome(ref call_outcome)
|
||||
| CallDunderResult::PossiblyUnbound(ref call_outcome) => {
|
||||
let Some(iterator_ty) = call_outcome.return_type(db) else {
|
||||
return IterationOutcome::NotIterable {
|
||||
not_iterable_ty: self,
|
||||
};
|
||||
};
|
||||
match &dunder_iter_result {
|
||||
Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => {
|
||||
let iterator_ty = outcome.return_type(db);
|
||||
|
||||
return if let Some(element_ty) = iterator_ty
|
||||
.call_dunder(db, "__next__", &CallArguments::positional([iterator_ty]))
|
||||
.return_type(db)
|
||||
{
|
||||
if matches!(dunder_iter_result, CallDunderResult::PossiblyUnbound(..)) {
|
||||
return match iterator_ty.call_dunder(
|
||||
db,
|
||||
"__next__",
|
||||
&CallArguments::positional([iterator_ty]),
|
||||
) {
|
||||
Ok(outcome) => {
|
||||
if matches!(
|
||||
dunder_iter_result,
|
||||
Err(CallDunderError::PossiblyUnbound { .. })
|
||||
) {
|
||||
IterationOutcome::PossiblyUnboundDunderIter {
|
||||
iterable_ty: self,
|
||||
element_ty: outcome.return_type(db),
|
||||
}
|
||||
} else {
|
||||
IterationOutcome::Iterable {
|
||||
element_ty: outcome.return_type(db),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(CallDunderError::PossiblyUnbound(outcome)) => {
|
||||
IterationOutcome::PossiblyUnboundDunderIter {
|
||||
iterable_ty: self,
|
||||
element_ty,
|
||||
element_ty: outcome.return_type(db),
|
||||
}
|
||||
} else {
|
||||
IterationOutcome::Iterable { element_ty }
|
||||
}
|
||||
} else {
|
||||
IterationOutcome::NotIterable {
|
||||
Err(_) => IterationOutcome::NotIterable {
|
||||
not_iterable_ty: self,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
CallDunderResult::MethodNotAvailable => {}
|
||||
// If `__iter__` exists but can't be called or doesn't have the expected signature,
|
||||
// return not iterable over falling back to `__getitem__`.
|
||||
Err(CallDunderError::Call(_)) => {
|
||||
return IterationOutcome::NotIterable {
|
||||
not_iterable_ty: self,
|
||||
}
|
||||
}
|
||||
Err(CallDunderError::MethodNotAvailable) => {
|
||||
// No `__iter__` attribute, try `__getitem__` next.
|
||||
}
|
||||
}
|
||||
|
||||
// Although it's not considered great practice,
|
||||
|
@ -1880,19 +1855,23 @@ impl<'db> Type<'db> {
|
|||
//
|
||||
// TODO(Alex) this is only valid if the `__getitem__` method is annotated as
|
||||
// accepting `int` or `SupportsIndex`
|
||||
if let Some(element_ty) = self
|
||||
.call_dunder(
|
||||
db,
|
||||
"__getitem__",
|
||||
&CallArguments::positional([self, KnownClass::Int.to_instance(db)]),
|
||||
)
|
||||
.return_type(db)
|
||||
{
|
||||
IterationOutcome::Iterable { element_ty }
|
||||
} else {
|
||||
IterationOutcome::NotIterable {
|
||||
not_iterable_ty: self,
|
||||
match self.call_dunder(
|
||||
db,
|
||||
"__getitem__",
|
||||
&CallArguments::positional([self, KnownClass::Int.to_instance(db)]),
|
||||
) {
|
||||
Ok(outcome) => IterationOutcome::Iterable {
|
||||
element_ty: outcome.return_type(db),
|
||||
},
|
||||
Err(CallDunderError::PossiblyUnbound(outcome)) => {
|
||||
IterationOutcome::PossiblyUnboundDunderIter {
|
||||
iterable_ty: self,
|
||||
element_ty: outcome.return_type(db),
|
||||
}
|
||||
}
|
||||
Err(_) => IterationOutcome::NotIterable {
|
||||
not_iterable_ty: self,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3694,20 +3673,23 @@ impl<'db> Class<'db> {
|
|||
let arguments = CallArguments::positional([name, bases, namespace]);
|
||||
|
||||
let return_ty_result = match metaclass.call(db, &arguments) {
|
||||
CallOutcome::NotCallable { not_callable_ty } => Err(MetaclassError {
|
||||
Ok(outcome) => Ok(outcome.return_type(db)),
|
||||
|
||||
Err(CallError::NotCallable { not_callable_ty }) => Err(MetaclassError {
|
||||
kind: MetaclassErrorKind::NotCallable(not_callable_ty),
|
||||
}),
|
||||
|
||||
CallOutcome::Union {
|
||||
outcomes,
|
||||
Err(CallError::Union {
|
||||
called_ty,
|
||||
} => {
|
||||
errors,
|
||||
bindings,
|
||||
}) => {
|
||||
let mut partly_not_callable = false;
|
||||
|
||||
let return_ty = outcomes
|
||||
let return_ty = errors
|
||||
.iter()
|
||||
.fold(None, |acc, outcome| {
|
||||
let ty = outcome.return_type(db);
|
||||
.fold(None, |acc, error| {
|
||||
let ty = error.return_type(db);
|
||||
|
||||
match (acc, ty) {
|
||||
(acc, None) => {
|
||||
|
@ -3718,7 +3700,13 @@ impl<'db> Class<'db> {
|
|||
(Some(builder), Some(ty)) => Some(builder.add(ty)),
|
||||
}
|
||||
})
|
||||
.map(UnionBuilder::build);
|
||||
.map(|mut builder| {
|
||||
for binding in bindings {
|
||||
builder = builder.add(binding.return_type());
|
||||
}
|
||||
|
||||
builder.build()
|
||||
});
|
||||
|
||||
if partly_not_callable {
|
||||
Err(MetaclassError {
|
||||
|
@ -3729,16 +3717,13 @@ impl<'db> Class<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
CallOutcome::PossiblyUnboundDunderCall { called_ty, .. } => Err(MetaclassError {
|
||||
kind: MetaclassErrorKind::PartlyNotCallable(called_ty),
|
||||
Err(CallError::PossiblyUnboundDunderCall { .. }) => Err(MetaclassError {
|
||||
kind: MetaclassErrorKind::PartlyNotCallable(metaclass),
|
||||
}),
|
||||
|
||||
// TODO we should also check for binding errors that would indicate the metaclass
|
||||
// does not accept the right arguments
|
||||
CallOutcome::Callable { binding }
|
||||
| CallOutcome::RevealType { binding, .. }
|
||||
| CallOutcome::StaticAssertionError { binding, .. }
|
||||
| CallOutcome::AssertType { binding, .. } => Ok(binding.return_type()),
|
||||
Err(CallError::BindingError { binding }) => Ok(binding.return_type()),
|
||||
};
|
||||
|
||||
return return_ty_result.map(|ty| ty.to_meta_type(db));
|
||||
|
|
|
@ -1,423 +1,206 @@
|
|||
use super::context::InferContext;
|
||||
use super::diagnostic::{CALL_NON_CALLABLE, TYPE_ASSERTION_FAILURE};
|
||||
use super::{Severity, Signature, Type, TypeArrayDisplay, UnionBuilder};
|
||||
use crate::types::diagnostic::STATIC_ASSERT_ERROR;
|
||||
use super::{Signature, Type};
|
||||
use crate::types::UnionType;
|
||||
use crate::Db;
|
||||
use ruff_db::diagnostic::DiagnosticId;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
mod arguments;
|
||||
mod bind;
|
||||
|
||||
pub(super) use arguments::{Argument, CallArguments};
|
||||
pub(super) use bind::{bind_call, CallBinding};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum StaticAssertionErrorKind<'db> {
|
||||
ArgumentIsFalse,
|
||||
ArgumentIsFalsy(Type<'db>),
|
||||
ArgumentTruthinessIsAmbiguous(Type<'db>),
|
||||
CustomError(&'db str),
|
||||
}
|
||||
|
||||
/// A successfully bound call where all arguments are valid.
|
||||
///
|
||||
/// It's guaranteed that the wrapped bindings have no errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum CallOutcome<'db> {
|
||||
Callable {
|
||||
binding: CallBinding<'db>,
|
||||
},
|
||||
RevealType {
|
||||
binding: CallBinding<'db>,
|
||||
revealed_ty: Type<'db>,
|
||||
},
|
||||
NotCallable {
|
||||
not_callable_ty: Type<'db>,
|
||||
},
|
||||
Union {
|
||||
called_ty: Type<'db>,
|
||||
outcomes: Box<[CallOutcome<'db>]>,
|
||||
},
|
||||
PossiblyUnboundDunderCall {
|
||||
called_ty: Type<'db>,
|
||||
call_outcome: Box<CallOutcome<'db>>,
|
||||
},
|
||||
StaticAssertionError {
|
||||
binding: CallBinding<'db>,
|
||||
error_kind: StaticAssertionErrorKind<'db>,
|
||||
},
|
||||
AssertType {
|
||||
binding: CallBinding<'db>,
|
||||
asserted_ty: Type<'db>,
|
||||
},
|
||||
/// The call resolves to exactly one binding.
|
||||
Single(CallBinding<'db>),
|
||||
|
||||
/// The call resolves to multiple bindings.
|
||||
Union(Box<[CallBinding<'db>]>),
|
||||
}
|
||||
|
||||
impl<'db> CallOutcome<'db> {
|
||||
/// Create a new `CallOutcome::Callable` with given binding.
|
||||
pub(super) fn callable(binding: CallBinding<'db>) -> CallOutcome<'db> {
|
||||
CallOutcome::Callable { binding }
|
||||
}
|
||||
/// Calls each union element using the provided `call` function.
|
||||
///
|
||||
/// Returns `Ok` if all variants can be called without error according to the callback and `Err` otherwise.
|
||||
pub(super) fn try_call_union<F>(
|
||||
db: &'db dyn Db,
|
||||
union: UnionType<'db>,
|
||||
call: F,
|
||||
) -> Result<Self, CallError<'db>>
|
||||
where
|
||||
F: Fn(Type<'db>) -> Result<Self, CallError<'db>>,
|
||||
{
|
||||
let elements = union.elements(db);
|
||||
let mut bindings = Vec::with_capacity(elements.len());
|
||||
let mut errors = Vec::new();
|
||||
let mut not_callable = true;
|
||||
|
||||
/// Create a new `CallOutcome::NotCallable` with given not-callable type.
|
||||
pub(super) fn not_callable(not_callable_ty: Type<'db>) -> CallOutcome<'db> {
|
||||
CallOutcome::NotCallable { not_callable_ty }
|
||||
}
|
||||
for element in elements {
|
||||
match call(*element) {
|
||||
Ok(CallOutcome::Single(binding)) => bindings.push(binding),
|
||||
Ok(CallOutcome::Union(inner_bindings)) => {
|
||||
bindings.extend(inner_bindings);
|
||||
}
|
||||
Err(error) => {
|
||||
not_callable |= error.is_not_callable();
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `CallOutcome::RevealType` with given revealed and return types.
|
||||
pub(super) fn revealed(binding: CallBinding<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> {
|
||||
CallOutcome::RevealType {
|
||||
binding,
|
||||
revealed_ty,
|
||||
if errors.is_empty() {
|
||||
Ok(CallOutcome::Union(bindings.into()))
|
||||
} else if bindings.is_empty() && not_callable {
|
||||
Err(CallError::NotCallable {
|
||||
not_callable_ty: Type::Union(union),
|
||||
})
|
||||
} else {
|
||||
Err(CallError::Union {
|
||||
errors: errors.into(),
|
||||
bindings: bindings.into(),
|
||||
called_ty: Type::Union(union),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `CallOutcome::Union` with given wrapped outcomes.
|
||||
pub(super) fn union(
|
||||
called_ty: Type<'db>,
|
||||
outcomes: impl IntoIterator<Item = CallOutcome<'db>>,
|
||||
) -> CallOutcome<'db> {
|
||||
CallOutcome::Union {
|
||||
called_ty,
|
||||
outcomes: outcomes.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `CallOutcome::AssertType` with given asserted and return types.
|
||||
pub(super) fn asserted(binding: CallBinding<'db>, asserted_ty: Type<'db>) -> CallOutcome<'db> {
|
||||
CallOutcome::AssertType {
|
||||
binding,
|
||||
asserted_ty,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the return type of the call, or `None` if not callable.
|
||||
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
/// The type returned by this call.
|
||||
pub(super) fn return_type(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
match self {
|
||||
Self::Callable { binding } => Some(binding.return_type()),
|
||||
Self::RevealType {
|
||||
binding,
|
||||
revealed_ty: _,
|
||||
} => Some(binding.return_type()),
|
||||
Self::NotCallable { not_callable_ty: _ } => None,
|
||||
Self::Union {
|
||||
outcomes,
|
||||
called_ty: _,
|
||||
} => outcomes
|
||||
.iter()
|
||||
// If all outcomes are NotCallable, we return None; if some outcomes are callable
|
||||
// and some are not, we return a union including Unknown.
|
||||
.fold(None, |acc, outcome| {
|
||||
let ty = outcome.return_type(db);
|
||||
match (acc, ty) {
|
||||
(None, None) => None,
|
||||
(None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)),
|
||||
(Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::unknown()))),
|
||||
}
|
||||
})
|
||||
.map(UnionBuilder::build),
|
||||
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_type(db),
|
||||
Self::StaticAssertionError { .. } => Some(Type::none(db)),
|
||||
Self::AssertType {
|
||||
binding,
|
||||
asserted_ty: _,
|
||||
} => Some(binding.return_type()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the return type of the call, emitting default diagnostics if needed.
|
||||
pub(super) fn unwrap_with_diagnostic(
|
||||
&self,
|
||||
context: &InferContext<'db>,
|
||||
node: ast::AnyNodeRef,
|
||||
) -> Type<'db> {
|
||||
match self.return_type_result(context, node) {
|
||||
Ok(return_ty) => return_ty,
|
||||
Err(NotCallableError::Type {
|
||||
not_callable_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable",
|
||||
not_callable_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
return_ty
|
||||
}
|
||||
Err(NotCallableError::UnionElement {
|
||||
not_callable_ty,
|
||||
called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (due to union element `{}`)",
|
||||
called_ty.display(context.db()),
|
||||
not_callable_ty.display(context.db()),
|
||||
),
|
||||
);
|
||||
return_ty
|
||||
}
|
||||
Err(NotCallableError::UnionElements {
|
||||
not_callable_tys,
|
||||
called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (due to union elements {})",
|
||||
called_ty.display(context.db()),
|
||||
not_callable_tys.display(context.db()),
|
||||
),
|
||||
);
|
||||
return_ty
|
||||
}
|
||||
Err(NotCallableError::PossiblyUnboundDunderCall {
|
||||
callable_ty: called_ty,
|
||||
return_ty,
|
||||
}) => {
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
|
||||
called_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
return_ty
|
||||
Self::Single(binding) => binding.return_type(),
|
||||
Self::Union(bindings) => {
|
||||
UnionType::from_elements(db, bindings.iter().map(bind::CallBinding::return_type))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the return type of the call as a result.
|
||||
pub(super) fn return_type_result(
|
||||
&self,
|
||||
context: &InferContext<'db>,
|
||||
node: ast::AnyNodeRef,
|
||||
) -> Result<Type<'db>, NotCallableError<'db>> {
|
||||
// TODO should this method emit diagnostics directly, or just return results that allow the
|
||||
// caller to decide about emitting diagnostics? Currently it emits binding diagnostics, but
|
||||
// only non-callable diagnostics in the union case, which is inconsistent.
|
||||
pub(super) fn bindings(&self) -> &[CallBinding<'db>] {
|
||||
match self {
|
||||
Self::Callable { binding } => {
|
||||
binding.report_diagnostics(context, node);
|
||||
Ok(binding.return_type())
|
||||
}
|
||||
Self::RevealType {
|
||||
binding,
|
||||
revealed_ty,
|
||||
} => {
|
||||
binding.report_diagnostics(context, node);
|
||||
context.report_diagnostic(
|
||||
node,
|
||||
DiagnosticId::RevealedType,
|
||||
Severity::Info,
|
||||
format_args!("Revealed type is `{}`", revealed_ty.display(context.db())),
|
||||
);
|
||||
Ok(binding.return_type())
|
||||
}
|
||||
Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type {
|
||||
not_callable_ty: *not_callable_ty,
|
||||
return_ty: Type::unknown(),
|
||||
}),
|
||||
Self::PossiblyUnboundDunderCall {
|
||||
called_ty,
|
||||
call_outcome,
|
||||
} => Err(NotCallableError::PossiblyUnboundDunderCall {
|
||||
callable_ty: *called_ty,
|
||||
return_ty: call_outcome
|
||||
.return_type(context.db())
|
||||
.unwrap_or(Type::unknown()),
|
||||
}),
|
||||
Self::Union {
|
||||
outcomes,
|
||||
called_ty,
|
||||
} => {
|
||||
let mut not_callable = vec![];
|
||||
let mut union_builder = UnionBuilder::new(context.db());
|
||||
let mut revealed = false;
|
||||
for outcome in outcomes {
|
||||
let return_ty = match outcome {
|
||||
Self::NotCallable { not_callable_ty } => {
|
||||
not_callable.push(*not_callable_ty);
|
||||
Type::unknown()
|
||||
}
|
||||
Self::RevealType {
|
||||
binding,
|
||||
revealed_ty: _,
|
||||
} => {
|
||||
if revealed {
|
||||
binding.return_type()
|
||||
} else {
|
||||
revealed = true;
|
||||
outcome.unwrap_with_diagnostic(context, node)
|
||||
}
|
||||
}
|
||||
_ => outcome.unwrap_with_diagnostic(context, node),
|
||||
};
|
||||
union_builder = union_builder.add(return_ty);
|
||||
}
|
||||
let return_ty = union_builder.build();
|
||||
match not_callable[..] {
|
||||
[] => Ok(return_ty),
|
||||
[elem] => Err(NotCallableError::UnionElement {
|
||||
not_callable_ty: elem,
|
||||
called_ty: *called_ty,
|
||||
return_ty,
|
||||
}),
|
||||
_ if not_callable.len() == outcomes.len() => Err(NotCallableError::Type {
|
||||
not_callable_ty: *called_ty,
|
||||
return_ty,
|
||||
}),
|
||||
_ => Err(NotCallableError::UnionElements {
|
||||
not_callable_tys: not_callable.into_boxed_slice(),
|
||||
called_ty: *called_ty,
|
||||
return_ty,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Self::StaticAssertionError {
|
||||
binding,
|
||||
error_kind,
|
||||
} => {
|
||||
binding.report_diagnostics(context, node);
|
||||
|
||||
match error_kind {
|
||||
StaticAssertionErrorKind::ArgumentIsFalse => {
|
||||
context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
node,
|
||||
format_args!("Static assertion error: argument evaluates to `False`"),
|
||||
);
|
||||
}
|
||||
StaticAssertionErrorKind::ArgumentIsFalsy(parameter_ty) => {
|
||||
context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
node,
|
||||
format_args!(
|
||||
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy",
|
||||
parameter_ty=parameter_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
StaticAssertionErrorKind::ArgumentTruthinessIsAmbiguous(parameter_ty) => {
|
||||
context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
node,
|
||||
format_args!(
|
||||
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness",
|
||||
parameter_ty=parameter_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
StaticAssertionErrorKind::CustomError(message) => {
|
||||
context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
node,
|
||||
format_args!("Static assertion error: {message}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Type::unknown())
|
||||
}
|
||||
Self::AssertType {
|
||||
binding,
|
||||
asserted_ty,
|
||||
} => {
|
||||
let [actual_ty, _asserted] = binding.parameter_types() else {
|
||||
return Ok(binding.return_type());
|
||||
};
|
||||
|
||||
if !actual_ty.is_gradual_equivalent_to(context.db(), *asserted_ty) {
|
||||
context.report_lint(
|
||||
&TYPE_ASSERTION_FAILURE,
|
||||
node,
|
||||
format_args!(
|
||||
"Actual type `{}` is not the same as asserted type `{}`",
|
||||
actual_ty.display(context.db()),
|
||||
asserted_ty.display(context.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(binding.return_type())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) enum CallDunderResult<'db> {
|
||||
CallOutcome(CallOutcome<'db>),
|
||||
PossiblyUnbound(CallOutcome<'db>),
|
||||
MethodNotAvailable,
|
||||
}
|
||||
|
||||
impl<'db> CallDunderResult<'db> {
|
||||
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
match self {
|
||||
Self::CallOutcome(outcome) => outcome.return_type(db),
|
||||
Self::PossiblyUnbound { .. } => None,
|
||||
Self::MethodNotAvailable => None,
|
||||
Self::Single(binding) => std::slice::from_ref(binding),
|
||||
Self::Union(bindings) => bindings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The reason why calling a type failed.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum NotCallableError<'db> {
|
||||
pub(super) enum CallError<'db> {
|
||||
/// The type is not callable.
|
||||
Type {
|
||||
NotCallable {
|
||||
/// The type that can't be called.
|
||||
not_callable_ty: Type<'db>,
|
||||
return_ty: Type<'db>,
|
||||
},
|
||||
/// A single union element is not callable.
|
||||
UnionElement {
|
||||
not_callable_ty: Type<'db>,
|
||||
|
||||
/// A call to a union failed because at least one variant
|
||||
/// can't be called with the given arguments.
|
||||
///
|
||||
/// A union where all variants are not callable is represented as a `NotCallable` error.
|
||||
Union {
|
||||
/// The variants that can't be called with the given arguments.
|
||||
errors: Box<[CallError<'db>]>,
|
||||
|
||||
/// The bindings for the callable variants (that have no binding errors).
|
||||
bindings: Box<[CallBinding<'db>]>,
|
||||
|
||||
/// The union type that we tried calling.
|
||||
called_ty: Type<'db>,
|
||||
return_ty: Type<'db>,
|
||||
},
|
||||
/// Multiple (but not all) union elements are not callable.
|
||||
UnionElements {
|
||||
not_callable_tys: Box<[Type<'db>]>,
|
||||
called_ty: Type<'db>,
|
||||
return_ty: Type<'db>,
|
||||
},
|
||||
|
||||
/// The type has a `__call__` method but it isn't always bound.
|
||||
PossiblyUnboundDunderCall {
|
||||
callable_ty: Type<'db>,
|
||||
return_ty: Type<'db>,
|
||||
called_type: Type<'db>,
|
||||
outcome: Box<CallOutcome<'db>>,
|
||||
},
|
||||
|
||||
/// The type is callable but not with the given arguments.
|
||||
BindingError { binding: CallBinding<'db> },
|
||||
}
|
||||
|
||||
impl<'db> NotCallableError<'db> {
|
||||
/// The return type that should be used when a call is not callable.
|
||||
pub(super) fn return_type(&self) -> Type<'db> {
|
||||
impl<'db> CallError<'db> {
|
||||
/// Returns a fallback return type to use that best approximates the return type of the call.
|
||||
///
|
||||
/// Returns `None` if the type isn't callable.
|
||||
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
match self {
|
||||
Self::Type { return_ty, .. } => *return_ty,
|
||||
Self::UnionElement { return_ty, .. } => *return_ty,
|
||||
Self::UnionElements { return_ty, .. } => *return_ty,
|
||||
Self::PossiblyUnboundDunderCall { return_ty, .. } => *return_ty,
|
||||
CallError::NotCallable { .. } => None,
|
||||
// If some variants are callable, and some are not, return the union of the return types of the callable variants
|
||||
// combined with `Type::Unknown`
|
||||
CallError::Union {
|
||||
errors, bindings, ..
|
||||
} => Some(UnionType::from_elements(
|
||||
db,
|
||||
bindings
|
||||
.iter()
|
||||
.map(CallBinding::return_type)
|
||||
.chain(errors.iter().map(|err| err.fallback_return_type(db))),
|
||||
)),
|
||||
Self::PossiblyUnboundDunderCall { outcome, .. } => Some(outcome.return_type(db)),
|
||||
Self::BindingError { binding } => Some(binding.return_type()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the return type of the call or a fallback that
|
||||
/// represents the best guess of the return type (e.g. the actual return type even if the
|
||||
/// dunder is possibly unbound).
|
||||
///
|
||||
/// If the type is not callable, returns `Type::Unknown`.
|
||||
pub(super) fn fallback_return_type(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
self.return_type(db).unwrap_or(Type::unknown())
|
||||
}
|
||||
|
||||
/// The resolved type that was not callable.
|
||||
///
|
||||
/// For unions, returns the union type itself, which may contain a mix of callable and
|
||||
/// non-callable types.
|
||||
pub(super) fn called_type(&self) -> Type<'db> {
|
||||
match self {
|
||||
Self::Type {
|
||||
Self::NotCallable {
|
||||
not_callable_ty, ..
|
||||
} => *not_callable_ty,
|
||||
Self::UnionElement { called_ty, .. } => *called_ty,
|
||||
Self::UnionElements { called_ty, .. } => *called_ty,
|
||||
Self::PossiblyUnboundDunderCall {
|
||||
callable_ty: called_ty,
|
||||
..
|
||||
} => *called_ty,
|
||||
Self::Union { called_ty, .. } => *called_ty,
|
||||
Self::PossiblyUnboundDunderCall { called_type, .. } => *called_type,
|
||||
Self::BindingError { binding } => binding.callable_type(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) const fn is_not_callable(&self) -> bool {
|
||||
matches!(self, Self::NotCallable { .. })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum CallDunderError<'db> {
|
||||
/// The dunder attribute exists but it can't be called with the given arguments.
|
||||
///
|
||||
/// This includes non-callable dunder attributes that are possibly unbound.
|
||||
Call(CallError<'db>),
|
||||
|
||||
/// The type has the specified dunder method and it is callable
|
||||
/// with the specified arguments without any binding errors
|
||||
/// but it is possibly unbound.
|
||||
PossiblyUnbound(CallOutcome<'db>),
|
||||
|
||||
/// The dunder method with the specified name is missing.
|
||||
MethodNotAvailable,
|
||||
}
|
||||
|
||||
impl<'db> CallDunderError<'db> {
|
||||
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
match self {
|
||||
Self::Call(error) => error.return_type(db),
|
||||
Self::PossiblyUnbound(_) => None,
|
||||
Self::MethodNotAvailable => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn fallback_return_type(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
self.return_type(db).unwrap_or(Type::unknown())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<CallError<'db>> for CallDunderError<'db> {
|
||||
fn from(error: CallError<'db>) -> Self {
|
||||
Self::Call(error)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -161,6 +161,10 @@ impl<'db> CallBinding<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn callable_type(&self) -> Type<'db> {
|
||||
self.callable_ty
|
||||
}
|
||||
|
||||
pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) {
|
||||
self.return_ty = return_ty;
|
||||
}
|
||||
|
@ -195,12 +199,16 @@ impl<'db> CallBinding<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) {
|
||||
pub(crate) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) {
|
||||
let callable_name = self.callable_name(context.db());
|
||||
for error in &self.errors {
|
||||
error.report_diagnostic(context, node, callable_name);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_binding_errors(&self) -> bool {
|
||||
!self.errors.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Information needed to emit a diagnostic regarding a parameter.
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
use std::num::NonZeroU32;
|
||||
|
||||
use itertools::{Either, Itertools};
|
||||
use ruff_db::diagnostic::{DiagnosticId, Severity};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext};
|
||||
|
@ -66,29 +67,30 @@ use crate::types::diagnostic::{
|
|||
use crate::types::mro::MroErrorKind;
|
||||
use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{
|
||||
todo_type, Boundness, CallDunderResult, Class, ClassLiteralType, DynamicType, FunctionType,
|
||||
InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass,
|
||||
KnownFunction, KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType,
|
||||
SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType,
|
||||
TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints,
|
||||
TypeVarInstance, UnionBuilder, UnionType,
|
||||
todo_type, Boundness, Class, ClassLiteralType, DynamicType, FunctionType, InstanceType,
|
||||
IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction,
|
||||
KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, SubclassOfType,
|
||||
Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
|
||||
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder,
|
||||
UnionType,
|
||||
};
|
||||
use crate::unpack::Unpack;
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
use crate::Db;
|
||||
|
||||
use super::call::CallError;
|
||||
use super::context::{InNoTypeCheck, InferContext, WithDiagnostics};
|
||||
use super::diagnostic::{
|
||||
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
|
||||
report_invalid_exception_raised, report_non_subscriptable,
|
||||
report_possibly_unresolved_reference, report_slice_step_size_zero, report_unresolved_reference,
|
||||
INVALID_METACLASS, SUBCLASS_OF_FINAL_CLASS,
|
||||
INVALID_METACLASS, STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE,
|
||||
};
|
||||
use super::slots::check_class_slots;
|
||||
use super::string_annotation::{
|
||||
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
||||
};
|
||||
use super::{global_symbol, ParameterExpectation, ParameterExpectations};
|
||||
use super::{global_symbol, CallDunderError, ParameterExpectation, ParameterExpectations};
|
||||
|
||||
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
|
||||
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
|
||||
|
@ -1616,16 +1618,20 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
|
||||
let target_ty = enter_ty
|
||||
.call(self.db(), &CallArguments::positional([context_expression_ty]))
|
||||
.return_type_result(&self.context, context_expression.into())
|
||||
.unwrap_or_else(|err| {
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.unwrap_or_else(|err| {
|
||||
// TODO: Use more specific error messages for the different error cases.
|
||||
// E.g. hint toward the union variant that doesn't correctly implement enter,
|
||||
// distinguish between a not callable `__enter__` attribute and a wrong signature.
|
||||
self.context.report_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
format_args!("
|
||||
Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` of type `{enter_ty}` is not callable", context_expression = context_expression_ty.display(self.db()), enter_ty = enter_ty.display(self.db())
|
||||
Object of type `{context_expression}` cannot be used with `with` because it does not correctly implement `__enter__`",
|
||||
context_expression = context_expression_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
err.return_type()
|
||||
err.fallback_return_type(self.db())
|
||||
});
|
||||
|
||||
match exit {
|
||||
|
@ -1663,16 +1669,17 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
Type::none(self.db()),
|
||||
]),
|
||||
)
|
||||
.return_type_result(&self.context, context_expression.into())
|
||||
.is_err()
|
||||
{
|
||||
// TODO: Use more specific error messages for the different error cases.
|
||||
// E.g. hint toward the union variant that doesn't correctly implement enter,
|
||||
// distinguish between a not callable `__exit__` attribute and a wrong signature.
|
||||
self.context.report_lint(
|
||||
&INVALID_CONTEXT_MANAGER,
|
||||
context_expression.into(),
|
||||
format_args!(
|
||||
"Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` of type `{exit_ty}` is not callable",
|
||||
"Object of type `{context_expression}` cannot be used with `with` because it does not correctly implement `__exit__`",
|
||||
context_expression = context_expression_ty.display(self.db()),
|
||||
exit_ty = exit_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -2207,10 +2214,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
self.db(),
|
||||
&CallArguments::positional([target_type, value_type]),
|
||||
);
|
||||
let augmented_return_ty = match call
|
||||
.return_type_result(&self.context, AnyNodeRef::StmtAugAssign(assignment))
|
||||
{
|
||||
Ok(t) => t,
|
||||
let augmented_return_ty = match call {
|
||||
Ok(t) => t.return_type(self.db()),
|
||||
Err(e) => {
|
||||
self.context.report_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
|
@ -2221,7 +2226,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
value_type.display(self.db())
|
||||
),
|
||||
);
|
||||
e.return_type()
|
||||
e.fallback_return_type(self.db())
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -3243,9 +3248,155 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
.unwrap_or_default();
|
||||
|
||||
let call_arguments = self.infer_arguments(arguments, parameter_expectations);
|
||||
function_type
|
||||
.call(self.db(), &call_arguments)
|
||||
.unwrap_with_diagnostic(&self.context, call_expression.into())
|
||||
let call = function_type.call(self.db(), &call_arguments);
|
||||
|
||||
match call {
|
||||
Ok(outcome) => {
|
||||
for binding in outcome.bindings() {
|
||||
let Some(known_function) = binding
|
||||
.callable_type()
|
||||
.into_function_literal()
|
||||
.and_then(|function_type| function_type.known(self.db()))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match known_function {
|
||||
KnownFunction::RevealType => {
|
||||
if let Some(revealed_type) = binding.one_parameter_type() {
|
||||
self.context.report_diagnostic(
|
||||
call_expression.into(),
|
||||
DiagnosticId::RevealedType,
|
||||
Severity::Info,
|
||||
format_args!(
|
||||
"Revealed type is `{}`",
|
||||
revealed_type.display(self.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
KnownFunction::AssertType => {
|
||||
if let [actual_ty, asserted_ty] = binding.parameter_types() {
|
||||
if !actual_ty.is_gradual_equivalent_to(self.db(), *asserted_ty) {
|
||||
self.context.report_lint(
|
||||
&TYPE_ASSERTION_FAILURE,
|
||||
call_expression.into(),
|
||||
format_args!(
|
||||
"Actual type `{}` is not the same as asserted type `{}`",
|
||||
actual_ty.display(self.db()),
|
||||
asserted_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
KnownFunction::StaticAssert => {
|
||||
if let Some((parameter_ty, message)) = binding.two_parameter_types() {
|
||||
let truthiness = parameter_ty.bool(self.db());
|
||||
|
||||
if !truthiness.is_always_true() {
|
||||
if let Some(message) =
|
||||
message.into_string_literal().map(|s| &**s.value(self.db()))
|
||||
{
|
||||
self.context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
call_expression.into(),
|
||||
format_args!("Static assertion error: {message}"),
|
||||
);
|
||||
} else if parameter_ty == Type::BooleanLiteral(false) {
|
||||
self.context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
call_expression.into(),
|
||||
format_args!("Static assertion error: argument evaluates to `False`"),
|
||||
);
|
||||
} else if truthiness.is_always_false() {
|
||||
self.context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
call_expression.into(),
|
||||
format_args!(
|
||||
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy",
|
||||
parameter_ty=parameter_ty.display(self.db())
|
||||
),
|
||||
);
|
||||
} else {
|
||||
self.context.report_lint(
|
||||
&STATIC_ASSERT_ERROR,
|
||||
call_expression.into(),
|
||||
format_args!(
|
||||
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness",
|
||||
parameter_ty=parameter_ty.display(self.db())
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
outcome.return_type(self.db())
|
||||
}
|
||||
Err(err) => {
|
||||
// TODO: We currently only report the first error. Ideally, we'd report
|
||||
// an error saying that the union type can't be called, followed by a sub
|
||||
// diagnostic explaining why.
|
||||
fn report_call_error(
|
||||
context: &InferContext,
|
||||
err: CallError,
|
||||
call_expression: &ast::ExprCall,
|
||||
) {
|
||||
match err {
|
||||
CallError::NotCallable { not_callable_ty } => {
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
call_expression.into(),
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable",
|
||||
not_callable_ty.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
CallError::Union {
|
||||
called_ty: _,
|
||||
bindings: _,
|
||||
errors,
|
||||
} => {
|
||||
// TODO: Remove the `Vec::from` call once we use the Rust 2024 edition
|
||||
// which adds `Box<[T]>::into_iter`
|
||||
if let Some(first) = Vec::from(errors).into_iter().next() {
|
||||
report_call_error(context, first, call_expression);
|
||||
} else {
|
||||
debug_assert!(
|
||||
false,
|
||||
"Expected `CalLError::Union` to at least have one error"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CallError::PossiblyUnboundDunderCall { called_type, .. } => {
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
call_expression.into(),
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
|
||||
called_type.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
CallError::BindingError { binding, .. } => {
|
||||
binding.report_diagnostics(context, call_expression.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let return_type = err.fallback_return_type(self.db());
|
||||
report_call_error(&self.context, err, call_expression);
|
||||
|
||||
return_type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_starred_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> {
|
||||
|
@ -3567,37 +3718,23 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
}
|
||||
};
|
||||
|
||||
if let CallDunderResult::CallOutcome(call)
|
||||
| CallDunderResult::PossiblyUnbound(call) = operand_type.call_dunder(
|
||||
match operand_type.call_dunder(
|
||||
self.db(),
|
||||
unary_dunder_method,
|
||||
&CallArguments::positional([operand_type]),
|
||||
) {
|
||||
match call.return_type_result(&self.context, AnyNodeRef::ExprUnaryOp(unary)) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
self.context.report_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
unary.into(),
|
||||
format_args!(
|
||||
"Unary operator `{op}` is unsupported for type `{}`",
|
||||
operand_type.display(self.db()),
|
||||
),
|
||||
);
|
||||
e.return_type()
|
||||
}
|
||||
Ok(outcome) => outcome.return_type(self.db()),
|
||||
Err(e) => {
|
||||
self.context.report_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
unary.into(),
|
||||
format_args!(
|
||||
"Unary operator `{op}` is unsupported for type `{}`",
|
||||
operand_type.display(self.db()),
|
||||
),
|
||||
);
|
||||
e.fallback_return_type(self.db())
|
||||
}
|
||||
} else {
|
||||
self.context.report_lint(
|
||||
&UNSUPPORTED_OPERATOR,
|
||||
unary.into(),
|
||||
format_args!(
|
||||
"Unary operator `{op}` is unsupported for type `{}`",
|
||||
operand_type.display(self.db()),
|
||||
),
|
||||
);
|
||||
|
||||
Type::unknown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3835,25 +3972,28 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
reflected_dunder,
|
||||
&CallArguments::positional([right_ty, left_ty]),
|
||||
)
|
||||
.return_type(self.db())
|
||||
.or_else(|| {
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.or_else(|_| {
|
||||
left_ty
|
||||
.call_dunder(
|
||||
self.db(),
|
||||
op.dunder(),
|
||||
&CallArguments::positional([left_ty, right_ty]),
|
||||
)
|
||||
.return_type(self.db())
|
||||
});
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use `call_dunder`?
|
||||
let call_on_left_instance = if let Symbol::Type(class_member, _) =
|
||||
left_class.member(self.db(), op.dunder())
|
||||
{
|
||||
class_member
|
||||
.call(self.db(), &CallArguments::positional([left_ty, right_ty]))
|
||||
.return_type(self.db())
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -3865,9 +4005,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
if let Symbol::Type(class_member, _) =
|
||||
right_class.member(self.db(), op.reflected_dunder())
|
||||
{
|
||||
// TODO: Use `call_dunder`
|
||||
class_member
|
||||
.call(self.db(), &CallArguments::positional([right_ty, left_ty]))
|
||||
.return_type(self.db())
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -4626,43 +4768,44 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
Type::IntLiteral(i64::from(bool)),
|
||||
),
|
||||
(value_ty, slice_ty) => {
|
||||
// Resolve the value to its class.
|
||||
let value_meta_ty = value_ty.to_meta_type(self.db());
|
||||
|
||||
// If the class defines `__getitem__`, return its return type.
|
||||
//
|
||||
// See: https://docs.python.org/3/reference/datamodel.html#class-getitem-versus-getitem
|
||||
match value_meta_ty.member(self.db(), "__getitem__") {
|
||||
Symbol::Unbound => {}
|
||||
Symbol::Type(dunder_getitem_method, boundness) => {
|
||||
if boundness == Boundness::PossiblyUnbound {
|
||||
self.context.report_lint(
|
||||
&CALL_POSSIBLY_UNBOUND_METHOD,
|
||||
match value_ty.call_dunder(
|
||||
self.db(),
|
||||
"__getitem__",
|
||||
&CallArguments::positional([value_ty, slice_ty]),
|
||||
) {
|
||||
Ok(outcome) => return outcome.return_type(self.db()),
|
||||
Err(err @ CallDunderError::PossiblyUnbound { .. }) => {
|
||||
self.context.report_lint(
|
||||
&CALL_POSSIBLY_UNBOUND_METHOD,
|
||||
value_node.into(),
|
||||
format_args!(
|
||||
"Method `__getitem__` of type `{}` is possibly unbound",
|
||||
value_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
|
||||
return err.fallback_return_type(self.db());
|
||||
}
|
||||
Err(CallDunderError::Call(err)) => {
|
||||
self.context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
value_node.into(),
|
||||
format_args!(
|
||||
"Method `__getitem__` of type `{}` is possibly unbound",
|
||||
"Method `__getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
err.called_type().display(self.db()),
|
||||
value_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return dunder_getitem_method
|
||||
.call(self.db(), &CallArguments::positional([value_ty, slice_ty]))
|
||||
.return_type_result(&self.context, value_node.into())
|
||||
.unwrap_or_else(|err| {
|
||||
self.context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
value_node.into(),
|
||||
format_args!(
|
||||
"Method `__getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
err.called_type().display(self.db()),
|
||||
value_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
err.return_type()
|
||||
});
|
||||
return err.fallback_return_type(self.db());
|
||||
}
|
||||
}
|
||||
Err(CallDunderError::MethodNotAvailable) => {
|
||||
// try `__class_getitem__`
|
||||
}
|
||||
};
|
||||
|
||||
// Otherwise, if the value is itself a class and defines `__class_getitem__`,
|
||||
// return its return type.
|
||||
|
@ -4693,7 +4836,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
|
||||
return ty
|
||||
.call(self.db(), &CallArguments::positional([value_ty, slice_ty]))
|
||||
.return_type_result(&self.context, value_node.into())
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.unwrap_or_else(|err| {
|
||||
self.context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
|
@ -4704,7 +4847,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
value_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
err.return_type()
|
||||
err.fallback_return_type(self.db())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -5929,23 +6072,20 @@ fn perform_rich_comparison<'db>(
|
|||
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
||||
// The following resource has details about the rich comparison algorithm:
|
||||
// https://snarky.ca/unravelling-rich-comparison-operators/
|
||||
//
|
||||
// TODO: this currently gives the return type even if the arg types are invalid
|
||||
// (e.g. int.__lt__ with string instance should be errored, currently bool)
|
||||
|
||||
let call_dunder = |op: RichCompareOperator,
|
||||
left: InstanceType<'db>,
|
||||
right: InstanceType<'db>| {
|
||||
match left.class.class_member(db, op.dunder()) {
|
||||
Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder
|
||||
.call(
|
||||
db,
|
||||
&CallArguments::positional([Type::Instance(left), Type::Instance(right)]),
|
||||
)
|
||||
.return_type(db),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let call_dunder =
|
||||
|op: RichCompareOperator, left: InstanceType<'db>, right: InstanceType<'db>| {
|
||||
// TODO: How do we want to handle possibly unbound dunder methods?
|
||||
match left.class.class_member(db, op.dunder()) {
|
||||
Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder
|
||||
.call(
|
||||
db,
|
||||
&CallArguments::positional([Type::Instance(left), Type::Instance(right)]),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(db))
|
||||
.ok(),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
// The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side.
|
||||
if left != right && right.is_subtype_of(db, left) {
|
||||
|
@ -5989,7 +6129,8 @@ fn perform_membership_test_comparison<'db>(
|
|||
db,
|
||||
&CallArguments::positional([Type::Instance(right), Type::Instance(left)]),
|
||||
)
|
||||
.return_type(db)
|
||||
.map(|outcome| outcome.return_type(db))
|
||||
.ok()
|
||||
}
|
||||
_ => {
|
||||
// iteration-based membership test
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue