mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 04:45:01 +00:00
[red-knot] Diagnostics for incorrect bool
usages (#16238)
This commit is contained in:
parent
3aa7ba31b1
commit
5fab97f1ef
28 changed files with 1267 additions and 260 deletions
|
@ -351,6 +351,20 @@ class Y(Foo): ...
|
||||||
reveal_type(X() + Y()) # revealed: int
|
reveal_type(X() + Y()) # revealed: int
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Operations involving types with invalid `__bool__` methods
|
||||||
|
|
||||||
|
<!-- snapshot-diagnostics -->
|
||||||
|
|
||||||
|
```py
|
||||||
|
class NotBoolable:
|
||||||
|
__bool__ = 3
|
||||||
|
|
||||||
|
a = NotBoolable()
|
||||||
|
|
||||||
|
# error: [unsupported-bool-conversion]
|
||||||
|
10 and a and True
|
||||||
|
```
|
||||||
|
|
||||||
## Unsupported
|
## Unsupported
|
||||||
|
|
||||||
### Dunder as instance attribute
|
### Dunder as instance attribute
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Calling builtins
|
||||||
|
|
||||||
|
## `bool` with incorrect arguments
|
||||||
|
|
||||||
|
```py
|
||||||
|
class NotBool:
|
||||||
|
__bool__ = None
|
||||||
|
|
||||||
|
# TODO: We should emit an `invalid-argument` error here for `2` because `bool` only takes one argument.
|
||||||
|
bool(1, 2)
|
||||||
|
|
||||||
|
# TODO: We should emit an `unsupported-bool-conversion` error here because the argument doesn't implement `__bool__` correctly.
|
||||||
|
bool(NotBool())
|
||||||
|
```
|
|
@ -160,3 +160,45 @@ 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`"
|
# 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
|
reveal_type("hello" in A()) # revealed: bool
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Return type that doesn't implement `__bool__` correctly
|
||||||
|
|
||||||
|
`in` and `not in` operations will fail at runtime if the object on the right-hand side of the
|
||||||
|
operation has a `__contains__` method that returns a type which is not convertible to `bool`. This
|
||||||
|
is because of the way these operations are handled by the Python interpreter at runtime. If we
|
||||||
|
assume that `y` is an object that has a `__contains__` method, the Python expression `x in y`
|
||||||
|
desugars to a `contains(y, x)` call, where `contains` looks something like this:
|
||||||
|
|
||||||
|
```ignore
|
||||||
|
def contains(y, x):
|
||||||
|
return bool(type(y).__contains__(y, x))
|
||||||
|
```
|
||||||
|
|
||||||
|
where the `bool()` conversion itself implicitly calls `__bool__` under the hood.
|
||||||
|
|
||||||
|
TODO: Ideally the message would explain to the user what's wrong. E.g,
|
||||||
|
|
||||||
|
```ignore
|
||||||
|
error: [operator] cannot use `in` operator on object of type `WithContains`
|
||||||
|
note: This is because the `in` operator implicitly calls `WithContains.__contains__`, but `WithContains.__contains__` is invalidly defined
|
||||||
|
note: `WithContains.__contains__` is invalidly defined because it returns an instance of `NotBoolable`, which cannot be evaluated in a boolean context
|
||||||
|
note: `NotBoolable` cannot be evaluated in a boolean context because its `__bool__` attribute is not callable
|
||||||
|
```
|
||||||
|
|
||||||
|
It may also be more appropriate to use `unsupported-operator` as the error code.
|
||||||
|
|
||||||
|
<!-- snapshot-diagnostics -->
|
||||||
|
|
||||||
|
```py
|
||||||
|
class NotBoolable:
|
||||||
|
__bool__ = 3
|
||||||
|
|
||||||
|
class WithContains:
|
||||||
|
def __contains__(self, item) -> NotBoolable:
|
||||||
|
return NotBoolable()
|
||||||
|
|
||||||
|
# error: [unsupported-bool-conversion]
|
||||||
|
10 in WithContains()
|
||||||
|
# error: [unsupported-bool-conversion]
|
||||||
|
10 not in WithContains()
|
||||||
|
```
|
||||||
|
|
|
@ -345,3 +345,29 @@ def f(x: bool, y: int):
|
||||||
reveal_type(4.2 < x) # revealed: bool
|
reveal_type(4.2 < x) # revealed: bool
|
||||||
reveal_type(x < 4.2) # revealed: bool
|
reveal_type(x < 4.2) # revealed: bool
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Chained comparisons with objects that don't implement `__bool__` correctly
|
||||||
|
|
||||||
|
<!-- snapshot-diagnostics -->
|
||||||
|
|
||||||
|
Python implicitly calls `bool` on the comparison result of preceding elements (but not for the last
|
||||||
|
element) of a chained comparison.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class NotBoolable:
|
||||||
|
__bool__ = 3
|
||||||
|
|
||||||
|
class Comparable:
|
||||||
|
def __lt__(self, item) -> NotBoolable:
|
||||||
|
return NotBoolable()
|
||||||
|
|
||||||
|
def __gt__(self, item) -> NotBoolable:
|
||||||
|
return NotBoolable()
|
||||||
|
|
||||||
|
# error: [unsupported-bool-conversion]
|
||||||
|
10 < Comparable() < 20
|
||||||
|
# error: [unsupported-bool-conversion]
|
||||||
|
10 < Comparable() < Comparable()
|
||||||
|
|
||||||
|
Comparable() < Comparable() # fine
|
||||||
|
```
|
||||||
|
|
|
@ -334,3 +334,61 @@ reveal_type(a is not c) # revealed: Literal[True]
|
||||||
For tuples like `tuple[int, ...]`, `tuple[Any, ...]`
|
For tuples like `tuple[int, ...]`, `tuple[Any, ...]`
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
|
|
||||||
|
## Chained comparisons with elements that incorrectly implement `__bool__`
|
||||||
|
|
||||||
|
<!-- snapshot-diagnostics -->
|
||||||
|
|
||||||
|
For an operation `A() < A()` to succeed at runtime, the `A.__lt__` method does not necessarily need
|
||||||
|
to return an object that is convertible to a `bool`. However, the return type _does_ need to be
|
||||||
|
convertible to a `bool` for the operation `A() < A() < A()` (a _chained_ comparison) to succeed.
|
||||||
|
This is because `A() < A() < A()` desugars to something like this, which involves several implicit
|
||||||
|
conversions to `bool`:
|
||||||
|
|
||||||
|
```ignore
|
||||||
|
def compute_chained_comparison():
|
||||||
|
a1 = A()
|
||||||
|
a2 = A()
|
||||||
|
first_comparison = a1 < a2
|
||||||
|
return first_comparison and (a2 < A())
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
class NotBoolable:
|
||||||
|
__bool__ = 5
|
||||||
|
|
||||||
|
class Comparable:
|
||||||
|
def __lt__(self, other) -> NotBoolable:
|
||||||
|
return NotBoolable()
|
||||||
|
|
||||||
|
def __gt__(self, other) -> NotBoolable:
|
||||||
|
return NotBoolable()
|
||||||
|
|
||||||
|
a = (1, Comparable())
|
||||||
|
b = (1, Comparable())
|
||||||
|
|
||||||
|
# error: [unsupported-bool-conversion]
|
||||||
|
a < b < b
|
||||||
|
|
||||||
|
a < b # fine
|
||||||
|
```
|
||||||
|
|
||||||
|
## Equality with elements that incorrectly implement `__bool__`
|
||||||
|
|
||||||
|
<!-- snapshot-diagnostics -->
|
||||||
|
|
||||||
|
Python does not generally attempt to coerce the result of `==` and `!=` operations between two
|
||||||
|
arbitrary objects to a `bool`, but a comparison of tuples will fail if the result of comparing any
|
||||||
|
pair of elements at equivalent positions cannot be converted to a `bool`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
def __eq__(self, other) -> NotBoolable:
|
||||||
|
return NotBoolable()
|
||||||
|
|
||||||
|
class NotBoolable:
|
||||||
|
__bool__ = None
|
||||||
|
|
||||||
|
# error: [unsupported-bool-conversion]
|
||||||
|
(A(),) == (A(),)
|
||||||
|
```
|
||||||
|
|
|
@ -35,3 +35,13 @@ def _(flag: bool):
|
||||||
x = 1 if flag else None
|
x = 1 if flag else None
|
||||||
reveal_type(x) # revealed: Literal[1] | None
|
reveal_type(x) # revealed: Literal[1] | None
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Condition with object that implements `__bool__` incorrectly
|
||||||
|
|
||||||
|
```py
|
||||||
|
class NotBoolable:
|
||||||
|
__bool__ = 3
|
||||||
|
|
||||||
|
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||||
|
3 if NotBoolable() else 4
|
||||||
|
```
|
||||||
|
|
|
@ -147,3 +147,17 @@ def _(flag: bool):
|
||||||
|
|
||||||
reveal_type(y) # revealed: Literal[0, 1]
|
reveal_type(y) # revealed: Literal[0, 1]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Condition with object that implements `__bool__` incorrectly
|
||||||
|
|
||||||
|
```py
|
||||||
|
class NotBoolable:
|
||||||
|
__bool__ = 3
|
||||||
|
|
||||||
|
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||||
|
if NotBoolable():
|
||||||
|
...
|
||||||
|
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||||
|
elif NotBoolable():
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
|
@ -43,3 +43,21 @@ def _(target: int):
|
||||||
|
|
||||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Guard with object that implements `__bool__` incorrectly
|
||||||
|
|
||||||
|
```py
|
||||||
|
class NotBoolable:
|
||||||
|
__bool__ = 3
|
||||||
|
|
||||||
|
def _(target: int, flag: NotBoolable):
|
||||||
|
y = 1
|
||||||
|
match target:
|
||||||
|
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||||
|
case 1 if flag:
|
||||||
|
y = 2
|
||||||
|
case 2:
|
||||||
|
y = 3
|
||||||
|
|
||||||
|
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
## Condition with object that implements `__bool__` incorrectly
|
||||||
|
|
||||||
|
```py
|
||||||
|
class NotBoolable:
|
||||||
|
__bool__ = 3
|
||||||
|
|
||||||
|
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||||
|
assert NotBoolable()
|
||||||
|
```
|
|
@ -101,3 +101,55 @@ reveal_type(bool([])) # revealed: bool
|
||||||
reveal_type(bool({})) # revealed: bool
|
reveal_type(bool({})) # revealed: bool
|
||||||
reveal_type(bool(set())) # revealed: bool
|
reveal_type(bool(set())) # revealed: bool
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `__bool__` returning `NoReturn`
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import NoReturn
|
||||||
|
|
||||||
|
class NotBoolable:
|
||||||
|
def __bool__(self) -> NoReturn:
|
||||||
|
raise NotImplementedError("This object can't be converted to a boolean")
|
||||||
|
|
||||||
|
# TODO: This should emit an error that `NotBoolable` can't be converted to a bool but it currently doesn't
|
||||||
|
# because `Never` is assignable to `bool`. This probably requires dead code analysis to fix.
|
||||||
|
if NotBoolable():
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Not callable `__bool__`
|
||||||
|
|
||||||
|
```py
|
||||||
|
class NotBoolable:
|
||||||
|
__bool__ = None
|
||||||
|
|
||||||
|
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||||
|
if NotBoolable():
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Not-boolable union
|
||||||
|
|
||||||
|
```py
|
||||||
|
def test(cond: bool):
|
||||||
|
class NotBoolable:
|
||||||
|
__bool__ = None if cond else 3
|
||||||
|
|
||||||
|
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; it incorrectly implements `__bool__`"
|
||||||
|
if NotBoolable():
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Union with some variants implementing `__bool__` incorrectly
|
||||||
|
|
||||||
|
```py
|
||||||
|
def test(cond: bool):
|
||||||
|
class NotBoolable:
|
||||||
|
__bool__: int
|
||||||
|
|
||||||
|
a = 10 if cond else NotBoolable()
|
||||||
|
|
||||||
|
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`; its `__bool__` method isn't callable"
|
||||||
|
if a:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
|
@ -116,3 +116,14 @@ def _(flag: bool, flag2: bool):
|
||||||
# error: [possibly-unresolved-reference]
|
# error: [possibly-unresolved-reference]
|
||||||
y
|
y
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Condition with object that implements `__bool__` incorrectly
|
||||||
|
|
||||||
|
```py
|
||||||
|
class NotBoolable:
|
||||||
|
__bool__ = 3
|
||||||
|
|
||||||
|
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||||
|
while NotBoolable():
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
|
@ -266,7 +266,7 @@ def _(
|
||||||
if af:
|
if af:
|
||||||
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy
|
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy
|
||||||
|
|
||||||
# TODO: Emit a diagnostic (`d` is not valid in boolean context)
|
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`; the return type of its bool method (`MetaAmbiguous`) isn't assignable to `bool"
|
||||||
if d:
|
if d:
|
||||||
# TODO: Should be `Unknown`
|
# TODO: Should be `Unknown`
|
||||||
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy
|
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: instances.md - Binary operations on instances - Operations involving types with invalid `__bool__` methods
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/binary/instances.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | class NotBoolable:
|
||||||
|
2 | __bool__ = 3
|
||||||
|
3 |
|
||||||
|
4 | a = NotBoolable()
|
||||||
|
5 |
|
||||||
|
6 | # error: [unsupported-bool-conversion]
|
||||||
|
7 | 10 and a and True
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:unsupported-bool-conversion
|
||||||
|
--> /src/mdtest_snippet.py:7:8
|
||||||
|
|
|
||||||
|
6 | # error: [unsupported-bool-conversion]
|
||||||
|
7 | 10 and a and True
|
||||||
|
| ^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,53 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: membership_test.md - Comparison: Membership Test - Return type that doesn't implement `__bool__` correctly
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | class NotBoolable:
|
||||||
|
2 | __bool__ = 3
|
||||||
|
3 |
|
||||||
|
4 | class WithContains:
|
||||||
|
5 | def __contains__(self, item) -> NotBoolable:
|
||||||
|
6 | return NotBoolable()
|
||||||
|
7 |
|
||||||
|
8 | # error: [unsupported-bool-conversion]
|
||||||
|
9 | 10 in WithContains()
|
||||||
|
10 | # error: [unsupported-bool-conversion]
|
||||||
|
11 | 10 not in WithContains()
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:unsupported-bool-conversion
|
||||||
|
--> /src/mdtest_snippet.py:9:1
|
||||||
|
|
|
||||||
|
8 | # error: [unsupported-bool-conversion]
|
||||||
|
9 | 10 in WithContains()
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||||
|
10 | # error: [unsupported-bool-conversion]
|
||||||
|
11 | 10 not in WithContains()
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:unsupported-bool-conversion
|
||||||
|
--> /src/mdtest_snippet.py:11:1
|
||||||
|
|
|
||||||
|
9 | 10 in WithContains()
|
||||||
|
10 | # error: [unsupported-bool-conversion]
|
||||||
|
11 | 10 not in WithContains()
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: not.md - Unary not - Object that implements `__bool__` incorrectly
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/unary/not.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | class NotBoolable:
|
||||||
|
2 | __bool__ = 3
|
||||||
|
3 |
|
||||||
|
4 | # error: [unsupported-bool-conversion]
|
||||||
|
5 | not NotBoolable()
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:unsupported-bool-conversion
|
||||||
|
--> /src/mdtest_snippet.py:5:1
|
||||||
|
|
|
||||||
|
4 | # error: [unsupported-bool-conversion]
|
||||||
|
5 | not NotBoolable()
|
||||||
|
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: rich_comparison.md - Comparison: Rich Comparison - Chained comparisons with objects that don't implement `__bool__` correctly
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | class NotBoolable:
|
||||||
|
2 | __bool__ = 3
|
||||||
|
3 |
|
||||||
|
4 | class Comparable:
|
||||||
|
5 | def __lt__(self, item) -> NotBoolable:
|
||||||
|
6 | return NotBoolable()
|
||||||
|
7 |
|
||||||
|
8 | def __gt__(self, item) -> NotBoolable:
|
||||||
|
9 | return NotBoolable()
|
||||||
|
10 |
|
||||||
|
11 | # error: [unsupported-bool-conversion]
|
||||||
|
12 | 10 < Comparable() < 20
|
||||||
|
13 | # error: [unsupported-bool-conversion]
|
||||||
|
14 | 10 < Comparable() < Comparable()
|
||||||
|
15 |
|
||||||
|
16 | Comparable() < Comparable() # fine
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:unsupported-bool-conversion
|
||||||
|
--> /src/mdtest_snippet.py:12:1
|
||||||
|
|
|
||||||
|
11 | # error: [unsupported-bool-conversion]
|
||||||
|
12 | 10 < Comparable() < 20
|
||||||
|
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||||
|
13 | # error: [unsupported-bool-conversion]
|
||||||
|
14 | 10 < Comparable() < Comparable()
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:unsupported-bool-conversion
|
||||||
|
--> /src/mdtest_snippet.py:14:1
|
||||||
|
|
|
||||||
|
12 | 10 < Comparable() < 20
|
||||||
|
13 | # error: [unsupported-bool-conversion]
|
||||||
|
14 | 10 < Comparable() < Comparable()
|
||||||
|
| ^^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||||
|
15 |
|
||||||
|
16 | Comparable() < Comparable() # fine
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,47 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: tuples.md - Comparison: Tuples - Chained comparisons with elements that incorrectly implement `__bool__`
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | class NotBoolable:
|
||||||
|
2 | __bool__ = 5
|
||||||
|
3 |
|
||||||
|
4 | class Comparable:
|
||||||
|
5 | def __lt__(self, other) -> NotBoolable:
|
||||||
|
6 | return NotBoolable()
|
||||||
|
7 |
|
||||||
|
8 | def __gt__(self, other) -> NotBoolable:
|
||||||
|
9 | return NotBoolable()
|
||||||
|
10 |
|
||||||
|
11 | a = (1, Comparable())
|
||||||
|
12 | b = (1, Comparable())
|
||||||
|
13 |
|
||||||
|
14 | # error: [unsupported-bool-conversion]
|
||||||
|
15 | a < b < b
|
||||||
|
16 |
|
||||||
|
17 | a < b # fine
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:unsupported-bool-conversion
|
||||||
|
--> /src/mdtest_snippet.py:15:1
|
||||||
|
|
|
||||||
|
14 | # error: [unsupported-bool-conversion]
|
||||||
|
15 | a < b < b
|
||||||
|
| ^^^^^ Boolean conversion is unsupported for type `NotBoolable | Literal[False]`; its `__bool__` method isn't callable
|
||||||
|
16 |
|
||||||
|
17 | a < b # fine
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
|
@ -0,0 +1,37 @@
|
||||||
|
---
|
||||||
|
source: crates/red_knot_test/src/lib.rs
|
||||||
|
expression: snapshot
|
||||||
|
---
|
||||||
|
---
|
||||||
|
mdtest name: tuples.md - Comparison: Tuples - Equality with elements that incorrectly implement `__bool__`
|
||||||
|
mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python source files
|
||||||
|
|
||||||
|
## mdtest_snippet.py
|
||||||
|
|
||||||
|
```
|
||||||
|
1 | class A:
|
||||||
|
2 | def __eq__(self, other) -> NotBoolable:
|
||||||
|
3 | return NotBoolable()
|
||||||
|
4 |
|
||||||
|
5 | class NotBoolable:
|
||||||
|
6 | __bool__ = None
|
||||||
|
7 |
|
||||||
|
8 | # error: [unsupported-bool-conversion]
|
||||||
|
9 | (A(),) == (A(),)
|
||||||
|
```
|
||||||
|
|
||||||
|
# Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lint:unsupported-bool-conversion
|
||||||
|
--> /src/mdtest_snippet.py:9:1
|
||||||
|
|
|
||||||
|
8 | # error: [unsupported-bool-conversion]
|
||||||
|
9 | (A(),) == (A(),)
|
||||||
|
| ^^^^^^^^^^^^^^^^ Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||||
|
|
|
||||||
|
|
||||||
|
```
|
|
@ -223,7 +223,7 @@ class InvalidBoolDunder:
|
||||||
def __bool__(self) -> int:
|
def __bool__(self) -> int:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# error: "Static assertion error: argument of type `InvalidBoolDunder` has an ambiguous static truthiness"
|
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `InvalidBoolDunder`; the return type of its bool method (`int`) isn't assignable to `bool"
|
||||||
static_assert(InvalidBoolDunder())
|
static_assert(InvalidBoolDunder())
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -183,12 +183,11 @@ class WithBothLenAndBool2:
|
||||||
# revealed: Literal[False]
|
# revealed: Literal[False]
|
||||||
reveal_type(not WithBothLenAndBool2())
|
reveal_type(not WithBothLenAndBool2())
|
||||||
|
|
||||||
# TODO: raise diagnostic when __bool__ method is not valid: [unsupported-operator] "Method __bool__ for type `MethodBoolInvalid` should return `bool`, returned type `int`"
|
|
||||||
# https://docs.python.org/3/reference/datamodel.html#object.__bool__
|
|
||||||
class MethodBoolInvalid:
|
class MethodBoolInvalid:
|
||||||
def __bool__(self) -> int:
|
def __bool__(self) -> int:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MethodBoolInvalid`; the return type of its bool method (`int`) isn't assignable to `bool"
|
||||||
# revealed: bool
|
# revealed: bool
|
||||||
reveal_type(not MethodBoolInvalid())
|
reveal_type(not MethodBoolInvalid())
|
||||||
|
|
||||||
|
@ -204,3 +203,15 @@ class PossiblyUnboundBool:
|
||||||
# revealed: bool
|
# revealed: bool
|
||||||
reveal_type(not PossiblyUnboundBool())
|
reveal_type(not PossiblyUnboundBool())
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Object that implements `__bool__` incorrectly
|
||||||
|
|
||||||
|
<!-- snapshot-diagnostics -->
|
||||||
|
|
||||||
|
```py
|
||||||
|
class NotBoolable:
|
||||||
|
__bool__ = 3
|
||||||
|
|
||||||
|
# error: [unsupported-bool-conversion]
|
||||||
|
not NotBoolable()
|
||||||
|
```
|
||||||
|
|
|
@ -9,6 +9,7 @@ use itertools::Itertools;
|
||||||
use ruff_db::files::File;
|
use ruff_db::files::File;
|
||||||
use ruff_python_ast as ast;
|
use ruff_python_ast as ast;
|
||||||
use ruff_python_ast::PythonVersion;
|
use ruff_python_ast::PythonVersion;
|
||||||
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
use type_ordering::union_elements_ordering;
|
use type_ordering::union_elements_ordering;
|
||||||
|
|
||||||
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
|
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
|
||||||
|
@ -36,9 +37,9 @@ use crate::symbol::{
|
||||||
imported_symbol, known_module_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
|
imported_symbol, known_module_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
|
||||||
Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers,
|
Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers,
|
||||||
};
|
};
|
||||||
use crate::types::call::{bind_call, CallArguments, CallBinding, CallOutcome};
|
use crate::types::call::{bind_call, CallArguments, CallBinding, CallOutcome, UnionCallError};
|
||||||
use crate::types::class_base::ClassBase;
|
use crate::types::class_base::ClassBase;
|
||||||
use crate::types::diagnostic::INVALID_TYPE_FORM;
|
use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION};
|
||||||
use crate::types::infer::infer_unpack_types;
|
use crate::types::infer::infer_unpack_types;
|
||||||
use crate::types::mro::{Mro, MroError, MroIterator};
|
use crate::types::mro::{Mro, MroError, MroIterator};
|
||||||
pub(crate) use crate::types::narrow::narrowing_constraint;
|
pub(crate) use crate::types::narrow::narrowing_constraint;
|
||||||
|
@ -1681,27 +1682,66 @@ impl<'db> Type<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves the boolean value of the type and falls back to [`Truthiness::Ambiguous`] if the type doesn't implement `__bool__` correctly.
|
||||||
|
///
|
||||||
|
/// This method should only be used outside type checking or when evaluating if a type
|
||||||
|
/// is truthy or falsy in a context where Python doesn't make an implicit `bool` call.
|
||||||
|
/// Use [`try_bool`](Self::try_bool) for type checking or implicit `bool` calls.
|
||||||
|
pub(crate) fn bool(&self, db: &'db dyn Db) -> Truthiness {
|
||||||
|
self.try_bool_impl(db, true)
|
||||||
|
.unwrap_or_else(|err| err.fallback_truthiness())
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolves the boolean value of a type.
|
/// Resolves the boolean value of a type.
|
||||||
///
|
///
|
||||||
/// This is used to determine the value that would be returned
|
/// This is used to determine the value that would be returned
|
||||||
/// when `bool(x)` is called on an object `x`.
|
/// when `bool(x)` is called on an object `x`.
|
||||||
pub(crate) fn bool(&self, db: &'db dyn Db) -> Truthiness {
|
///
|
||||||
match self {
|
/// Returns an error if the type doesn't implement `__bool__` correctly.
|
||||||
|
pub(crate) fn try_bool(&self, db: &'db dyn Db) -> Result<Truthiness, BoolError<'db>> {
|
||||||
|
self.try_bool_impl(db, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the boolean value of a type.
|
||||||
|
///
|
||||||
|
/// Setting `allow_short_circuit` to `true` allows the implementation to
|
||||||
|
/// early return if the bool value of any union variant is `Truthiness::Ambiguous`.
|
||||||
|
/// Early returning shows a 1-2% perf improvement on our benchmarks because
|
||||||
|
/// `bool` (which doesn't care about errors) is used heavily when evaluating statically known branches.
|
||||||
|
///
|
||||||
|
/// An alternative to this flag is to implement a trait similar to Rust's `Try` trait.
|
||||||
|
/// The advantage of that is that it would allow collecting the errors as well. However,
|
||||||
|
/// it is significantly more complex and duplicating the logic into `bool` without the error
|
||||||
|
/// handling didn't show any significant performance difference to when using the `allow_short_circuit` flag.
|
||||||
|
#[inline]
|
||||||
|
fn try_bool_impl(
|
||||||
|
&self,
|
||||||
|
db: &'db dyn Db,
|
||||||
|
allow_short_circuit: bool,
|
||||||
|
) -> Result<Truthiness, BoolError<'db>> {
|
||||||
|
let truthiness = match self {
|
||||||
Type::Dynamic(_) | Type::Never => Truthiness::Ambiguous,
|
Type::Dynamic(_) | Type::Never => Truthiness::Ambiguous,
|
||||||
Type::FunctionLiteral(_) => Truthiness::AlwaysTrue,
|
Type::FunctionLiteral(_) => Truthiness::AlwaysTrue,
|
||||||
Type::Callable(_) => Truthiness::AlwaysTrue,
|
Type::Callable(_) => Truthiness::AlwaysTrue,
|
||||||
Type::ModuleLiteral(_) => Truthiness::AlwaysTrue,
|
Type::ModuleLiteral(_) => Truthiness::AlwaysTrue,
|
||||||
Type::ClassLiteral(ClassLiteralType { class }) => {
|
Type::ClassLiteral(ClassLiteralType { class }) => {
|
||||||
class.metaclass(db).to_instance(db).bool(db)
|
return class
|
||||||
|
.metaclass(db)
|
||||||
|
.to_instance(db)
|
||||||
|
.try_bool_impl(db, allow_short_circuit);
|
||||||
}
|
}
|
||||||
Type::SubclassOf(subclass_of_ty) => subclass_of_ty
|
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
|
||||||
.subclass_of()
|
ClassBase::Dynamic(_) => Truthiness::Ambiguous,
|
||||||
.into_class()
|
ClassBase::Class(class) => {
|
||||||
.map(|class| Type::class_literal(class).bool(db))
|
return class
|
||||||
.unwrap_or(Truthiness::Ambiguous),
|
.metaclass(db)
|
||||||
|
.to_instance(db)
|
||||||
|
.try_bool_impl(db, allow_short_circuit);
|
||||||
|
}
|
||||||
|
},
|
||||||
Type::AlwaysTruthy => Truthiness::AlwaysTrue,
|
Type::AlwaysTruthy => Truthiness::AlwaysTrue,
|
||||||
Type::AlwaysFalsy => Truthiness::AlwaysFalse,
|
Type::AlwaysFalsy => Truthiness::AlwaysFalse,
|
||||||
Type::Instance(InstanceType { class }) => {
|
instance_ty @ Type::Instance(InstanceType { class }) => {
|
||||||
if class.is_known(db, KnownClass::Bool) {
|
if class.is_known(db, KnownClass::Bool) {
|
||||||
Truthiness::Ambiguous
|
Truthiness::Ambiguous
|
||||||
} else if class.is_known(db, KnownClass::NoneType) {
|
} else if class.is_known(db, KnownClass::NoneType) {
|
||||||
|
@ -1711,32 +1751,119 @@ impl<'db> Type<'db> {
|
||||||
// runtime there is a fallback to `__len__`, since `__bool__` takes precedence
|
// runtime there is a fallback to `__len__`, since `__bool__` takes precedence
|
||||||
// and a subclass could add a `__bool__` method.
|
// and a subclass could add a `__bool__` method.
|
||||||
|
|
||||||
if let Ok(Type::BooleanLiteral(bool_val)) = self
|
let type_to_truthiness = |ty| {
|
||||||
.try_call_dunder(db, "__bool__", &CallArguments::none())
|
if let Type::BooleanLiteral(bool_val) = ty {
|
||||||
.map(|outcome| outcome.return_type(db))
|
Truthiness::from(bool_val)
|
||||||
{
|
|
||||||
bool_val.into()
|
|
||||||
} else {
|
} else {
|
||||||
// TODO diagnostic if not assignable to bool
|
|
||||||
Truthiness::Ambiguous
|
Truthiness::Ambiguous
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.try_call_dunder(db, "__bool__", &CallArguments::none()) {
|
||||||
|
ref result @ (Ok(ref outcome)
|
||||||
|
| Err(CallDunderError::PossiblyUnbound(ref outcome))) => {
|
||||||
|
let return_type = outcome.return_type(db);
|
||||||
|
|
||||||
|
// The type has a `__bool__` method, but it doesn't return a boolean.
|
||||||
|
if !return_type.is_assignable_to(db, KnownClass::Bool.to_instance(db)) {
|
||||||
|
return Err(BoolError::IncorrectReturnType {
|
||||||
|
return_type: outcome.return_type(db),
|
||||||
|
not_boolable_type: *instance_ty,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.is_ok() {
|
||||||
|
type_to_truthiness(return_type)
|
||||||
|
} else {
|
||||||
|
// Don't trust possibly unbound `__bool__` method.
|
||||||
|
Truthiness::Ambiguous
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(CallDunderError::MethodNotAvailable) => Truthiness::Ambiguous,
|
||||||
|
Err(CallDunderError::Call(err)) => {
|
||||||
|
let err = match err {
|
||||||
|
// Unwrap call errors where only a single variant isn't callable.
|
||||||
|
// E.g. in the case of `Unknown & T`
|
||||||
|
// TODO: Improve handling of unions. While this improves messages overall,
|
||||||
|
// it still results in loosing information. Or should the information
|
||||||
|
// be recomputed when rendering the diagnostic?
|
||||||
|
CallError::Union(union_error) => {
|
||||||
|
if let Type::Union(_) = union_error.called_ty {
|
||||||
|
if union_error.errors.len() == 1 {
|
||||||
|
union_error.errors.into_vec().pop().unwrap()
|
||||||
|
} else {
|
||||||
|
CallError::Union(union_error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CallError::Union(union_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err => err,
|
||||||
|
};
|
||||||
|
|
||||||
|
match err {
|
||||||
|
CallError::BindingError { binding } => {
|
||||||
|
return Err(BoolError::IncorrectArguments {
|
||||||
|
truthiness: type_to_truthiness(binding.return_type()),
|
||||||
|
not_boolable_type: *instance_ty,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
CallError::NotCallable { .. } => {
|
||||||
|
return Err(BoolError::NotCallable {
|
||||||
|
not_boolable_type: *instance_ty,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
CallError::PossiblyUnboundDunderCall { .. }
|
||||||
|
| CallError::Union(..) => {
|
||||||
|
return Err(BoolError::Other {
|
||||||
|
not_boolable_type: *self,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Type::KnownInstance(known_instance) => known_instance.bool(),
|
Type::KnownInstance(known_instance) => known_instance.bool(),
|
||||||
Type::Union(union) => {
|
Type::Union(union) => {
|
||||||
let union_elements = union.elements(db);
|
let mut truthiness = None;
|
||||||
let first_element_truthiness = union_elements[0].bool(db);
|
let mut all_not_callable = true;
|
||||||
if first_element_truthiness.is_ambiguous() {
|
let mut has_errors = false;
|
||||||
return Truthiness::Ambiguous;
|
|
||||||
|
for element in union.elements(db) {
|
||||||
|
let element_truthiness = match element.try_bool_impl(db, allow_short_circuit) {
|
||||||
|
Ok(truthiness) => truthiness,
|
||||||
|
Err(err) => {
|
||||||
|
has_errors = true;
|
||||||
|
all_not_callable &= matches!(err, BoolError::NotCallable { .. });
|
||||||
|
err.fallback_truthiness()
|
||||||
}
|
}
|
||||||
if !union_elements
|
};
|
||||||
.iter()
|
|
||||||
.skip(1)
|
truthiness.get_or_insert(element_truthiness);
|
||||||
.all(|element| element.bool(db) == first_element_truthiness)
|
|
||||||
{
|
if Some(element_truthiness) != truthiness {
|
||||||
return Truthiness::Ambiguous;
|
truthiness = Some(Truthiness::Ambiguous);
|
||||||
|
|
||||||
|
if allow_short_circuit {
|
||||||
|
return Ok(Truthiness::Ambiguous);
|
||||||
}
|
}
|
||||||
first_element_truthiness
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_errors {
|
||||||
|
if all_not_callable {
|
||||||
|
return Err(BoolError::NotCallable {
|
||||||
|
not_boolable_type: *self,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Err(BoolError::Union {
|
||||||
|
union: *union,
|
||||||
|
truthiness: truthiness.unwrap_or(Truthiness::Ambiguous),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
truthiness.unwrap_or(Truthiness::Ambiguous)
|
||||||
}
|
}
|
||||||
Type::Intersection(_) => {
|
Type::Intersection(_) => {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -1749,7 +1876,9 @@ impl<'db> Type<'db> {
|
||||||
Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()),
|
Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()),
|
||||||
Type::SliceLiteral(_) => Truthiness::AlwaysTrue,
|
Type::SliceLiteral(_) => Truthiness::AlwaysTrue,
|
||||||
Type::Tuple(items) => Truthiness::from(!items.elements(db).is_empty()),
|
Type::Tuple(items) => Truthiness::from(!items.elements(db).is_empty()),
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Ok(truthiness)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the type of `len()` on a type if it is known more precisely than `int`,
|
/// Return the type of `len()` on a type if it is known more precisely than `int`,
|
||||||
|
@ -2081,6 +2210,9 @@ impl<'db> Type<'db> {
|
||||||
Type::ClassLiteral(ClassLiteralType { class }) => {
|
Type::ClassLiteral(ClassLiteralType { class }) => {
|
||||||
Ok(CallOutcome::Single(CallBinding::from_return_type(
|
Ok(CallOutcome::Single(CallBinding::from_return_type(
|
||||||
match class.known(db) {
|
match class.known(db) {
|
||||||
|
// TODO: We should check the call signature and error if the bool call doesn't have the
|
||||||
|
// right signature and return a binding error.
|
||||||
|
|
||||||
// If the class is the builtin-bool class (for example `bool(1)`), we try to
|
// 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
|
// return the specific truthiness value of the input arg, `Literal[True]` for
|
||||||
// the example above.
|
// the example above.
|
||||||
|
@ -2112,15 +2244,15 @@ impl<'db> Type<'db> {
|
||||||
not_callable_ty: self,
|
not_callable_ty: self,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CallDunderError::Call(CallError::Union {
|
CallDunderError::Call(CallError::Union(UnionCallError {
|
||||||
called_ty: _,
|
called_ty: _,
|
||||||
bindings,
|
bindings,
|
||||||
errors,
|
errors,
|
||||||
}) => CallError::Union {
|
})) => CallError::Union(UnionCallError {
|
||||||
called_ty: self,
|
called_ty: self,
|
||||||
bindings,
|
bindings,
|
||||||
errors,
|
errors,
|
||||||
},
|
}),
|
||||||
CallDunderError::Call(error) => error,
|
CallDunderError::Call(error) => error,
|
||||||
// Turn "possibly unbound object of type `Literal['__call__']`"
|
// Turn "possibly unbound object of type `Literal['__call__']`"
|
||||||
// into "`X` not callable (possibly unbound `__call__` method)"
|
// into "`X` not callable (possibly unbound `__call__` method)"
|
||||||
|
@ -2195,6 +2327,9 @@ impl<'db> Type<'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look up a dunder method on the meta type of `self` and call it.
|
/// Look up a dunder method on the meta type of `self` and call it.
|
||||||
|
///
|
||||||
|
/// Returns an `Err` if the dunder method can't be called,
|
||||||
|
/// or the given arguments are not valid.
|
||||||
fn try_call_dunder(
|
fn try_call_dunder(
|
||||||
self,
|
self,
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
|
@ -2213,6 +2348,15 @@ impl<'db> Type<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the element type when iterating over `self`.
|
||||||
|
///
|
||||||
|
/// This method should only be used outside of type checking because it omits any errors.
|
||||||
|
/// For type checking, use [`try_iterate`](Self::try_iterate) instead.
|
||||||
|
fn iterate(self, db: &'db dyn Db) -> Type<'db> {
|
||||||
|
self.try_iterate(db)
|
||||||
|
.unwrap_or_else(|err| err.fallback_element_type())
|
||||||
|
}
|
||||||
|
|
||||||
/// Given the type of an object that is iterated over in some way,
|
/// Given the type of an object that is iterated over in some way,
|
||||||
/// return the type of objects that are yielded by that iteration.
|
/// return the type of objects that are yielded by that iteration.
|
||||||
///
|
///
|
||||||
|
@ -2221,11 +2365,9 @@ impl<'db> Type<'db> {
|
||||||
/// for y in x:
|
/// for y in x:
|
||||||
/// pass
|
/// pass
|
||||||
/// ```
|
/// ```
|
||||||
fn iterate(self, db: &'db dyn Db) -> IterationOutcome<'db> {
|
fn try_iterate(self, db: &'db dyn Db) -> Result<Type<'db>, IterateError<'db>> {
|
||||||
if let Type::Tuple(tuple_type) = self {
|
if let Type::Tuple(tuple_type) = self {
|
||||||
return IterationOutcome::Iterable {
|
return Ok(UnionType::from_elements(db, tuple_type.elements(db)));
|
||||||
element_ty: UnionType::from_elements(db, tuple_type.elements(db)),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let dunder_iter_result = self.try_call_dunder(db, "__iter__", &CallArguments::none());
|
let dunder_iter_result = self.try_call_dunder(db, "__iter__", &CallArguments::none());
|
||||||
|
@ -2239,33 +2381,31 @@ impl<'db> Type<'db> {
|
||||||
dunder_iter_result,
|
dunder_iter_result,
|
||||||
Err(CallDunderError::PossiblyUnbound { .. })
|
Err(CallDunderError::PossiblyUnbound { .. })
|
||||||
) {
|
) {
|
||||||
IterationOutcome::PossiblyUnboundDunderIter {
|
Err(IterateError::PossiblyUnbound {
|
||||||
iterable_ty: self,
|
iterable_ty: self,
|
||||||
element_ty: outcome.return_type(db),
|
element_ty: outcome.return_type(db),
|
||||||
}
|
})
|
||||||
} else {
|
} else {
|
||||||
IterationOutcome::Iterable {
|
Ok(outcome.return_type(db))
|
||||||
element_ty: outcome.return_type(db),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(CallDunderError::PossiblyUnbound(outcome)) => {
|
Err(CallDunderError::PossiblyUnbound(outcome)) => {
|
||||||
IterationOutcome::PossiblyUnboundDunderIter {
|
Err(IterateError::PossiblyUnbound {
|
||||||
iterable_ty: self,
|
iterable_ty: self,
|
||||||
element_ty: outcome.return_type(db),
|
element_ty: outcome.return_type(db),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
Err(_) => Err(IterateError::NotIterable {
|
||||||
Err(_) => IterationOutcome::NotIterable {
|
|
||||||
not_iterable_ty: self,
|
not_iterable_ty: self,
|
||||||
},
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// If `__iter__` exists but can't be called or doesn't have the expected signature,
|
// If `__iter__` exists but can't be called or doesn't have the expected signature,
|
||||||
// return not iterable over falling back to `__getitem__`.
|
// return not iterable over falling back to `__getitem__`.
|
||||||
Err(CallDunderError::Call(_)) => {
|
Err(CallDunderError::Call(_)) => {
|
||||||
return IterationOutcome::NotIterable {
|
return Err(IterateError::NotIterable {
|
||||||
not_iterable_ty: self,
|
not_iterable_ty: self,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
Err(CallDunderError::MethodNotAvailable) => {
|
Err(CallDunderError::MethodNotAvailable) => {
|
||||||
// No `__iter__` attribute, try `__getitem__` next.
|
// No `__iter__` attribute, try `__getitem__` next.
|
||||||
|
@ -2283,18 +2423,14 @@ impl<'db> Type<'db> {
|
||||||
"__getitem__",
|
"__getitem__",
|
||||||
&CallArguments::positional([KnownClass::Int.to_instance(db)]),
|
&CallArguments::positional([KnownClass::Int.to_instance(db)]),
|
||||||
) {
|
) {
|
||||||
Ok(outcome) => IterationOutcome::Iterable {
|
Ok(outcome) => Ok(outcome.return_type(db)),
|
||||||
element_ty: outcome.return_type(db),
|
Err(CallDunderError::PossiblyUnbound(outcome)) => Err(IterateError::PossiblyUnbound {
|
||||||
},
|
|
||||||
Err(CallDunderError::PossiblyUnbound(outcome)) => {
|
|
||||||
IterationOutcome::PossiblyUnboundDunderIter {
|
|
||||||
iterable_ty: self,
|
iterable_ty: self,
|
||||||
element_ty: outcome.return_type(db),
|
element_ty: outcome.return_type(db),
|
||||||
}
|
}),
|
||||||
}
|
Err(_) => Err(IterateError::NotIterable {
|
||||||
Err(_) => IterationOutcome::NotIterable {
|
|
||||||
not_iterable_ty: self,
|
not_iterable_ty: self,
|
||||||
},
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3469,47 +3605,182 @@ pub enum TypeVarBoundOrConstraints<'db> {
|
||||||
Constraints(TupleType<'db>),
|
Constraints(TupleType<'db>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Error returned if a type isn't iterable.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum IterationOutcome<'db> {
|
enum IterateError<'db> {
|
||||||
Iterable {
|
/// The type isn't iterable because it doesn't implement the new-style or old-style iteration protocol
|
||||||
element_ty: Type<'db>,
|
///
|
||||||
},
|
/// The new-style iteration protocol requires a type being iterated over to have an `__iter__`
|
||||||
NotIterable {
|
/// method that returns something with a `__next__` method. The old-style iteration
|
||||||
not_iterable_ty: Type<'db>,
|
/// protocol requires a type being iterated over to have a `__getitem__` method that accepts
|
||||||
},
|
/// a positive-integer argument.
|
||||||
PossiblyUnboundDunderIter {
|
NotIterable { not_iterable_ty: Type<'db> },
|
||||||
|
|
||||||
|
/// The type is iterable but the methods aren't always bound.
|
||||||
|
PossiblyUnbound {
|
||||||
iterable_ty: Type<'db>,
|
iterable_ty: Type<'db>,
|
||||||
element_ty: Type<'db>,
|
element_ty: Type<'db>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'db> IterationOutcome<'db> {
|
impl<'db> IterateError<'db> {
|
||||||
fn unwrap_with_diagnostic(
|
/// Reports the diagnostic for this error.
|
||||||
self,
|
fn report_diagnostic(&self, context: &InferContext<'db>, iterable_node: ast::AnyNodeRef) {
|
||||||
context: &InferContext<'db>,
|
|
||||||
iterable_node: ast::AnyNodeRef,
|
|
||||||
) -> Type<'db> {
|
|
||||||
match self {
|
match self {
|
||||||
Self::Iterable { element_ty } => element_ty,
|
|
||||||
Self::NotIterable { not_iterable_ty } => {
|
Self::NotIterable { not_iterable_ty } => {
|
||||||
report_not_iterable(context, iterable_node, not_iterable_ty);
|
report_not_iterable(context, iterable_node, *not_iterable_ty);
|
||||||
Type::unknown()
|
|
||||||
}
|
}
|
||||||
Self::PossiblyUnboundDunderIter {
|
Self::PossiblyUnbound {
|
||||||
iterable_ty,
|
iterable_ty,
|
||||||
element_ty,
|
element_ty: _,
|
||||||
} => {
|
} => {
|
||||||
report_not_iterable_possibly_unbound(context, iterable_node, iterable_ty);
|
report_not_iterable_possibly_unbound(context, iterable_node, *iterable_ty);
|
||||||
element_ty
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unwrap_without_diagnostic(self) -> Type<'db> {
|
/// Returns the element type if it is known, or `None` if the type is never iterable.
|
||||||
|
fn element_type(&self) -> Option<Type<'db>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Iterable { element_ty } => element_ty,
|
IterateError::NotIterable { .. } => None,
|
||||||
Self::NotIterable { .. } => Type::unknown(),
|
IterateError::PossiblyUnbound { element_ty, .. } => Some(*element_ty),
|
||||||
Self::PossiblyUnboundDunderIter { element_ty, .. } => element_ty,
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the element type if it is known, or `Type::unknown()` if it is not.
|
||||||
|
fn fallback_element_type(&self) -> Type<'db> {
|
||||||
|
self.element_type().unwrap_or(Type::unknown())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub(super) enum BoolError<'db> {
|
||||||
|
/// The type has a `__bool__` attribute but it can't be called.
|
||||||
|
NotCallable { not_boolable_type: Type<'db> },
|
||||||
|
|
||||||
|
/// The type has a callable `__bool__` attribute, but it isn't callable
|
||||||
|
/// with the given arguments.
|
||||||
|
IncorrectArguments {
|
||||||
|
not_boolable_type: Type<'db>,
|
||||||
|
truthiness: Truthiness,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The type has a `__bool__` method, is callable with the given arguments,
|
||||||
|
/// but the return type isn't assignable to `bool`.
|
||||||
|
IncorrectReturnType {
|
||||||
|
not_boolable_type: Type<'db>,
|
||||||
|
return_type: Type<'db>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A union type doesn't implement `__bool__` correctly.
|
||||||
|
Union {
|
||||||
|
union: UnionType<'db>,
|
||||||
|
truthiness: Truthiness,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Any other reason why the type can't be converted to a bool.
|
||||||
|
/// E.g. because calling `__bool__` returns in a union type and not all variants support `__bool__` or
|
||||||
|
/// because `__bool__` points to a type that has a possibly unbound `__call__` method.
|
||||||
|
Other { not_boolable_type: Type<'db> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'db> BoolError<'db> {
|
||||||
|
pub(super) fn fallback_truthiness(&self) -> Truthiness {
|
||||||
|
match self {
|
||||||
|
BoolError::NotCallable { .. }
|
||||||
|
| BoolError::IncorrectReturnType { .. }
|
||||||
|
| BoolError::Other { .. } => Truthiness::Ambiguous,
|
||||||
|
BoolError::IncorrectArguments { truthiness, .. }
|
||||||
|
| BoolError::Union { truthiness, .. } => *truthiness,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_boolable_type(&self) -> Type<'db> {
|
||||||
|
match self {
|
||||||
|
BoolError::NotCallable {
|
||||||
|
not_boolable_type, ..
|
||||||
|
}
|
||||||
|
| BoolError::IncorrectArguments {
|
||||||
|
not_boolable_type, ..
|
||||||
|
}
|
||||||
|
| BoolError::Other { not_boolable_type }
|
||||||
|
| BoolError::IncorrectReturnType {
|
||||||
|
not_boolable_type, ..
|
||||||
|
} => *not_boolable_type,
|
||||||
|
BoolError::Union { union, .. } => Type::Union(*union),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn report_diagnostic(&self, context: &InferContext, condition: impl Ranged) {
|
||||||
|
self.report_diagnostic_impl(context, condition.range());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report_diagnostic_impl(&self, context: &InferContext, condition: TextRange) {
|
||||||
|
match self {
|
||||||
|
Self::IncorrectArguments {
|
||||||
|
not_boolable_type, ..
|
||||||
|
} => {
|
||||||
|
context.report_lint(
|
||||||
|
&UNSUPPORTED_BOOL_CONVERSION,
|
||||||
|
condition,
|
||||||
|
format_args!(
|
||||||
|
"Boolean conversion is unsupported for type `{}`; it incorrectly implements `__bool__`",
|
||||||
|
not_boolable_type.display(context.db())
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self::IncorrectReturnType {
|
||||||
|
not_boolable_type,
|
||||||
|
return_type,
|
||||||
|
} => {
|
||||||
|
context.report_lint(
|
||||||
|
&UNSUPPORTED_BOOL_CONVERSION,
|
||||||
|
condition,
|
||||||
|
format_args!(
|
||||||
|
"Boolean conversion is unsupported for type `{not_boolable}`; the return type of its bool method (`{return_type}`) isn't assignable to `bool",
|
||||||
|
not_boolable = not_boolable_type.display(context.db()),
|
||||||
|
return_type = return_type.display(context.db())
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self::NotCallable { not_boolable_type } => {
|
||||||
|
context.report_lint(
|
||||||
|
&UNSUPPORTED_BOOL_CONVERSION,
|
||||||
|
condition,
|
||||||
|
format_args!(
|
||||||
|
"Boolean conversion is unsupported for type `{}`; its `__bool__` method isn't callable",
|
||||||
|
not_boolable_type.display(context.db())
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self::Union { union, .. } => {
|
||||||
|
let first_error = union
|
||||||
|
.elements(context.db())
|
||||||
|
.iter()
|
||||||
|
.find_map(|element| element.try_bool(context.db()).err())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
context.report_lint(
|
||||||
|
&UNSUPPORTED_BOOL_CONVERSION,
|
||||||
|
condition,
|
||||||
|
format_args!(
|
||||||
|
"Boolean conversion is unsupported for union `{}` because `{}` doesn't implement `__bool__` correctly",
|
||||||
|
Type::Union(*union).display(context.db()),
|
||||||
|
first_error.not_boolable_type().display(context.db()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::Other { not_boolable_type } => {
|
||||||
|
context.report_lint(
|
||||||
|
&UNSUPPORTED_BOOL_CONVERSION,
|
||||||
|
condition,
|
||||||
|
format_args!(
|
||||||
|
"Boolean conversion is unsupported for type `{}`; it incorrectly implements `__bool__`",
|
||||||
|
not_boolable_type.display(context.db())
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4159,11 +4430,11 @@ impl<'db> Class<'db> {
|
||||||
kind: MetaclassErrorKind::NotCallable(not_callable_ty),
|
kind: MetaclassErrorKind::NotCallable(not_callable_ty),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Err(CallError::Union {
|
Err(CallError::Union(UnionCallError {
|
||||||
called_ty,
|
called_ty,
|
||||||
errors,
|
errors,
|
||||||
bindings,
|
bindings,
|
||||||
}) => {
|
})) => {
|
||||||
let mut partly_not_callable = false;
|
let mut partly_not_callable = false;
|
||||||
|
|
||||||
let return_ty = errors
|
let return_ty = errors
|
||||||
|
@ -4389,10 +4660,9 @@ impl<'db> Class<'db> {
|
||||||
//
|
//
|
||||||
// for self.name in <iterable>:
|
// for self.name in <iterable>:
|
||||||
|
|
||||||
// TODO: Potential diagnostics resulting from the iterable are currently not reported.
|
|
||||||
|
|
||||||
let iterable_ty = infer_expression_type(db, *iterable);
|
let iterable_ty = infer_expression_type(db, *iterable);
|
||||||
let inferred_ty = iterable_ty.iterate(db).unwrap_without_diagnostic();
|
// TODO: Potential diagnostics resulting from the iterable are currently not reported.
|
||||||
|
let inferred_ty = iterable_ty.iterate(db);
|
||||||
|
|
||||||
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
|
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,11 +57,11 @@ impl<'db> CallOutcome<'db> {
|
||||||
not_callable_ty: Type::Union(union),
|
not_callable_ty: Type::Union(union),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(CallError::Union {
|
Err(CallError::Union(UnionCallError {
|
||||||
errors: errors.into(),
|
errors: errors.into(),
|
||||||
bindings: bindings.into(),
|
bindings: bindings.into(),
|
||||||
called_ty: Type::Union(union),
|
called_ty: Type::Union(union),
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,16 +96,7 @@ pub(super) enum CallError<'db> {
|
||||||
/// can't be called with the given arguments.
|
/// can't be called with the given arguments.
|
||||||
///
|
///
|
||||||
/// A union where all variants are not callable is represented as a `NotCallable` error.
|
/// A union where all variants are not callable is represented as a `NotCallable` error.
|
||||||
Union {
|
Union(UnionCallError<'db>),
|
||||||
/// 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>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The type has a `__call__` method but it isn't always bound.
|
/// The type has a `__call__` method but it isn't always bound.
|
||||||
PossiblyUnboundDunderCall {
|
PossiblyUnboundDunderCall {
|
||||||
|
@ -126,9 +117,9 @@ impl<'db> CallError<'db> {
|
||||||
CallError::NotCallable { .. } => None,
|
CallError::NotCallable { .. } => None,
|
||||||
// If some variants are callable, and some are not, return the union of the return types of the callable variants
|
// If some variants are callable, and some are not, return the union of the return types of the callable variants
|
||||||
// combined with `Type::Unknown`
|
// combined with `Type::Unknown`
|
||||||
CallError::Union {
|
CallError::Union(UnionCallError {
|
||||||
errors, bindings, ..
|
bindings, errors, ..
|
||||||
} => Some(UnionType::from_elements(
|
}) => Some(UnionType::from_elements(
|
||||||
db,
|
db,
|
||||||
bindings
|
bindings
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -158,7 +149,7 @@ impl<'db> CallError<'db> {
|
||||||
Self::NotCallable {
|
Self::NotCallable {
|
||||||
not_callable_ty, ..
|
not_callable_ty, ..
|
||||||
} => *not_callable_ty,
|
} => *not_callable_ty,
|
||||||
Self::Union { called_ty, .. } => *called_ty,
|
Self::Union(UnionCallError { called_ty, .. }) => *called_ty,
|
||||||
Self::PossiblyUnboundDunderCall { called_type, .. } => *called_type,
|
Self::PossiblyUnboundDunderCall { called_type, .. } => *called_type,
|
||||||
Self::BindingError { binding } => binding.callable_type(),
|
Self::BindingError { binding } => binding.callable_type(),
|
||||||
}
|
}
|
||||||
|
@ -169,6 +160,18 @@ impl<'db> CallError<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub(super) struct UnionCallError<'db> {
|
||||||
|
/// The variants that can't be called with the given arguments.
|
||||||
|
pub(super) errors: Box<[CallError<'db>]>,
|
||||||
|
|
||||||
|
/// The bindings for the callable variants (that have no binding errors).
|
||||||
|
pub(super) bindings: Box<[CallBinding<'db>]>,
|
||||||
|
|
||||||
|
/// The union type that we tried calling.
|
||||||
|
pub(super) called_ty: Type<'db>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub(super) enum CallDunderError<'db> {
|
pub(super) enum CallDunderError<'db> {
|
||||||
/// The dunder attribute exists but it can't be called with the given arguments.
|
/// The dunder attribute exists but it can't be called with the given arguments.
|
||||||
|
|
|
@ -44,6 +44,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||||
registry.register_lint(&MISSING_ARGUMENT);
|
registry.register_lint(&MISSING_ARGUMENT);
|
||||||
registry.register_lint(&NON_SUBSCRIPTABLE);
|
registry.register_lint(&NON_SUBSCRIPTABLE);
|
||||||
registry.register_lint(&NOT_ITERABLE);
|
registry.register_lint(&NOT_ITERABLE);
|
||||||
|
registry.register_lint(&UNSUPPORTED_BOOL_CONVERSION);
|
||||||
registry.register_lint(&PARAMETER_ALREADY_ASSIGNED);
|
registry.register_lint(&PARAMETER_ALREADY_ASSIGNED);
|
||||||
registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE);
|
registry.register_lint(&POSSIBLY_UNBOUND_ATTRIBUTE);
|
||||||
registry.register_lint(&POSSIBLY_UNBOUND_IMPORT);
|
registry.register_lint(&POSSIBLY_UNBOUND_IMPORT);
|
||||||
|
@ -490,6 +491,37 @@ declare_lint! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare_lint! {
|
||||||
|
/// ## What it does
|
||||||
|
/// Checks for bool conversions where the object doesn't correctly implement `__bool__`.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// If an exception is raised when you attempt to evaluate the truthiness of an object,
|
||||||
|
/// using the object in a boolean context will fail at runtime.
|
||||||
|
///
|
||||||
|
/// ## Examples
|
||||||
|
///
|
||||||
|
/// ```python
|
||||||
|
/// class NotBoolable:
|
||||||
|
/// __bool__ = None
|
||||||
|
///
|
||||||
|
/// b1 = NotBoolable()
|
||||||
|
/// b2 = NotBoolable()
|
||||||
|
///
|
||||||
|
/// if b1: # exception raised here
|
||||||
|
/// pass
|
||||||
|
///
|
||||||
|
/// b1 and b2 # exception raised here
|
||||||
|
/// not b1 # exception raised here
|
||||||
|
/// b1 < b2 < b1 # exception raised here
|
||||||
|
/// ```
|
||||||
|
pub(crate) static UNSUPPORTED_BOOL_CONVERSION = {
|
||||||
|
summary: "detects boolean conversion where the object incorrectly implements `__bool__`",
|
||||||
|
status: LintStatus::preview("1.0.0"),
|
||||||
|
default_level: Level::Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
declare_lint! {
|
declare_lint! {
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
/// Checks for calls which provide more than one argument for a single parameter.
|
/// Checks for calls which provide more than one argument for a single parameter.
|
||||||
|
|
|
@ -33,7 +33,7 @@ use ruff_db::diagnostic::{DiagnosticId, Severity};
|
||||||
use ruff_db::files::File;
|
use ruff_db::files::File;
|
||||||
use ruff_db::parsed::parsed_module;
|
use ruff_db::parsed::parsed_module;
|
||||||
use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext};
|
use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext};
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
use salsa;
|
use salsa;
|
||||||
use salsa::plumbing::AsId;
|
use salsa::plumbing::AsId;
|
||||||
|
@ -54,7 +54,7 @@ use crate::symbol::{
|
||||||
module_type_implicit_global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
|
module_type_implicit_global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
|
||||||
typing_extensions_symbol, LookupError,
|
typing_extensions_symbol, LookupError,
|
||||||
};
|
};
|
||||||
use crate::types::call::{Argument, CallArguments};
|
use crate::types::call::{Argument, CallArguments, UnionCallError};
|
||||||
use crate::types::diagnostic::{
|
use crate::types::diagnostic::{
|
||||||
report_invalid_arguments_to_annotated, report_invalid_assignment,
|
report_invalid_arguments_to_annotated, report_invalid_assignment,
|
||||||
report_invalid_attribute_assignment, report_unresolved_module, TypeCheckDiagnostics,
|
report_invalid_attribute_assignment, report_unresolved_module, TypeCheckDiagnostics,
|
||||||
|
@ -69,9 +69,9 @@ use crate::types::mro::MroErrorKind;
|
||||||
use crate::types::unpacker::{UnpackResult, Unpacker};
|
use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
todo_type, Boundness, Class, ClassLiteralType, DynamicType, FunctionType, InstanceType,
|
todo_type, Boundness, Class, ClassLiteralType, DynamicType, FunctionType, InstanceType,
|
||||||
IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction,
|
IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType,
|
||||||
KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, SubclassOfType,
|
MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, SubclassOfType, Symbol,
|
||||||
Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
|
SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
|
||||||
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder,
|
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder,
|
||||||
UnionType,
|
UnionType,
|
||||||
};
|
};
|
||||||
|
@ -1481,7 +1481,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
elif_else_clauses,
|
elif_else_clauses,
|
||||||
} = if_statement;
|
} = if_statement;
|
||||||
|
|
||||||
self.infer_standalone_expression(test);
|
let test_ty = self.infer_standalone_expression(test);
|
||||||
|
|
||||||
|
if let Err(err) = test_ty.try_bool(self.db()) {
|
||||||
|
err.report_diagnostic(&self.context, &**test);
|
||||||
|
}
|
||||||
|
|
||||||
self.infer_body(body);
|
self.infer_body(body);
|
||||||
|
|
||||||
for clause in elif_else_clauses {
|
for clause in elif_else_clauses {
|
||||||
|
@ -1492,7 +1497,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
} = clause;
|
} = clause;
|
||||||
|
|
||||||
if let Some(test) = &test {
|
if let Some(test) = &test {
|
||||||
self.infer_standalone_expression(test);
|
let test_ty = self.infer_standalone_expression(test);
|
||||||
|
|
||||||
|
if let Err(err) = test_ty.try_bool(self.db()) {
|
||||||
|
err.report_diagnostic(&self.context, test);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.infer_body(body);
|
self.infer_body(body);
|
||||||
|
@ -1888,9 +1897,15 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
guard,
|
guard,
|
||||||
} = case;
|
} = case;
|
||||||
self.infer_match_pattern(pattern);
|
self.infer_match_pattern(pattern);
|
||||||
guard
|
|
||||||
.as_deref()
|
if let Some(guard) = guard.as_deref() {
|
||||||
.map(|guard| self.infer_standalone_expression(guard));
|
let guard_ty = self.infer_standalone_expression(guard);
|
||||||
|
|
||||||
|
if let Err(err) = guard_ty.try_bool(self.db()) {
|
||||||
|
err.report_diagnostic(&self.context, guard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.infer_body(body);
|
self.infer_body(body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2359,7 +2374,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
} = for_statement;
|
} = for_statement;
|
||||||
|
|
||||||
self.infer_target(target, iter, |db, iter_ty| {
|
self.infer_target(target, iter, |db, iter_ty| {
|
||||||
iter_ty.iterate(db).unwrap_without_diagnostic()
|
// TODO: `infer_for_statement_definition` reports a diagnostic if `iter_ty` isn't iterable
|
||||||
|
// but only if the target is a name. We should report a diagnostic here if the target isn't a name:
|
||||||
|
// `for a.x in not_iterable: ...
|
||||||
|
iter_ty.iterate(db)
|
||||||
});
|
});
|
||||||
|
|
||||||
self.infer_body(body);
|
self.infer_body(body);
|
||||||
|
@ -2388,9 +2406,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
|
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
|
||||||
unpacked.expression_type(name_ast_id)
|
unpacked.expression_type(name_ast_id)
|
||||||
}
|
}
|
||||||
TargetKind::Name => iterable_ty
|
TargetKind::Name => iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| {
|
||||||
.iterate(self.db())
|
err.report_diagnostic(&self.context, iterable.into());
|
||||||
.unwrap_with_diagnostic(&self.context, iterable.into()),
|
err.fallback_element_type()
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2406,7 +2425,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
orelse,
|
orelse,
|
||||||
} = while_statement;
|
} = while_statement;
|
||||||
|
|
||||||
self.infer_standalone_expression(test);
|
let test_ty = self.infer_standalone_expression(test);
|
||||||
|
|
||||||
|
if let Err(err) = test_ty.try_bool(self.db()) {
|
||||||
|
err.report_diagnostic(&self.context, &**test);
|
||||||
|
}
|
||||||
|
|
||||||
self.infer_body(body);
|
self.infer_body(body);
|
||||||
self.infer_body(orelse);
|
self.infer_body(orelse);
|
||||||
}
|
}
|
||||||
|
@ -2488,7 +2512,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
msg,
|
msg,
|
||||||
} = assert;
|
} = assert;
|
||||||
|
|
||||||
self.infer_expression(test);
|
let test_ty = self.infer_expression(test);
|
||||||
|
|
||||||
|
if let Err(err) = test_ty.try_bool(self.db()) {
|
||||||
|
err.report_diagnostic(&self.context, &**test);
|
||||||
|
}
|
||||||
|
|
||||||
self.infer_optional_expression(msg.as_deref());
|
self.infer_optional_expression(msg.as_deref());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3172,9 +3201,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
// TODO: async iterables/iterators! -- Alex
|
// TODO: async iterables/iterators! -- Alex
|
||||||
todo_type!("async iterables/iterators")
|
todo_type!("async iterables/iterators")
|
||||||
} else {
|
} else {
|
||||||
iterable_ty
|
iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| {
|
||||||
.iterate(self.db())
|
err.report_diagnostic(&self.context, iterable.into());
|
||||||
.unwrap_with_diagnostic(&self.context, iterable.into())
|
err.fallback_element_type()
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
self.types.expressions.insert(
|
self.types.expressions.insert(
|
||||||
|
@ -3230,7 +3260,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
let body_ty = self.infer_expression(body);
|
let body_ty = self.infer_expression(body);
|
||||||
let orelse_ty = self.infer_expression(orelse);
|
let orelse_ty = self.infer_expression(orelse);
|
||||||
|
|
||||||
match test_ty.bool(self.db()) {
|
match test_ty.try_bool(self.db()).unwrap_or_else(|err| {
|
||||||
|
err.report_diagnostic(&self.context, &**test);
|
||||||
|
err.fallback_truthiness()
|
||||||
|
}) {
|
||||||
Truthiness::AlwaysTrue => body_ty,
|
Truthiness::AlwaysTrue => body_ty,
|
||||||
Truthiness::AlwaysFalse => orelse_ty,
|
Truthiness::AlwaysFalse => orelse_ty,
|
||||||
Truthiness::Ambiguous => UnionType::from_elements(self.db(), [body_ty, orelse_ty]),
|
Truthiness::Ambiguous => UnionType::from_elements(self.db(), [body_ty, orelse_ty]),
|
||||||
|
@ -3323,7 +3356,26 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
}
|
}
|
||||||
KnownFunction::StaticAssert => {
|
KnownFunction::StaticAssert => {
|
||||||
if let Some((parameter_ty, message)) = binding.two_parameter_types() {
|
if let Some((parameter_ty, message)) = binding.two_parameter_types() {
|
||||||
let truthiness = parameter_ty.bool(self.db());
|
let truthiness = match parameter_ty.try_bool(self.db()) {
|
||||||
|
Ok(truthiness) => truthiness,
|
||||||
|
Err(err) => {
|
||||||
|
let condition = arguments
|
||||||
|
.find_argument("condition", 0)
|
||||||
|
.map(|argument| match argument {
|
||||||
|
ruff_python_ast::ArgOrKeyword::Arg(expr) => {
|
||||||
|
ast::AnyNodeRef::from(expr)
|
||||||
|
}
|
||||||
|
ruff_python_ast::ArgOrKeyword::Keyword(keyword) => {
|
||||||
|
ast::AnyNodeRef::from(keyword)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(ast::AnyNodeRef::from(call_expression));
|
||||||
|
|
||||||
|
err.report_diagnostic(&self.context, condition);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if !truthiness.is_always_true() {
|
if !truthiness.is_always_true() {
|
||||||
if let Some(message) =
|
if let Some(message) =
|
||||||
|
@ -3389,14 +3441,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
CallError::Union {
|
CallError::Union(UnionCallError { errors, .. }) => {
|
||||||
called_ty: _,
|
if let Some(first) = IntoIterator::into_iter(errors).next() {
|
||||||
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);
|
report_call_error(context, first, call_expression);
|
||||||
} else {
|
} else {
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
|
@ -3438,9 +3484,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
} = starred;
|
} = starred;
|
||||||
|
|
||||||
let iterable_ty = self.infer_expression(value);
|
let iterable_ty = self.infer_expression(value);
|
||||||
iterable_ty
|
iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| {
|
||||||
.iterate(self.db())
|
err.report_diagnostic(&self.context, value.as_ref().into());
|
||||||
.unwrap_with_diagnostic(&self.context, value.as_ref().into());
|
err.fallback_element_type()
|
||||||
|
});
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
todo_type!("starred expression")
|
todo_type!("starred expression")
|
||||||
|
@ -3456,9 +3503,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
let ast::ExprYieldFrom { range: _, value } = yield_from;
|
let ast::ExprYieldFrom { range: _, value } = yield_from;
|
||||||
|
|
||||||
let iterable_ty = self.infer_expression(value);
|
let iterable_ty = self.infer_expression(value);
|
||||||
iterable_ty
|
iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| {
|
||||||
.iterate(self.db())
|
err.report_diagnostic(&self.context, value.as_ref().into());
|
||||||
.unwrap_with_diagnostic(&self.context, value.as_ref().into());
|
err.fallback_element_type()
|
||||||
|
});
|
||||||
|
|
||||||
// TODO get type from `ReturnType` of generator
|
// TODO get type from `ReturnType` of generator
|
||||||
todo_type!("Generic `typing.Generator` type")
|
todo_type!("Generic `typing.Generator` type")
|
||||||
|
@ -3754,7 +3802,14 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
Type::IntLiteral(!i64::from(bool))
|
Type::IntLiteral(!i64::from(bool))
|
||||||
}
|
}
|
||||||
|
|
||||||
(ast::UnaryOp::Not, ty) => ty.bool(self.db()).negate().into_type(self.db()),
|
(ast::UnaryOp::Not, ty) => ty
|
||||||
|
.try_bool(self.db())
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
err.report_diagnostic(&self.context, unary);
|
||||||
|
err.fallback_truthiness()
|
||||||
|
})
|
||||||
|
.negate()
|
||||||
|
.into_type(self.db()),
|
||||||
(
|
(
|
||||||
op @ (ast::UnaryOp::UAdd | ast::UnaryOp::USub | ast::UnaryOp::Invert),
|
op @ (ast::UnaryOp::UAdd | ast::UnaryOp::USub | ast::UnaryOp::Invert),
|
||||||
Type::FunctionLiteral(_)
|
Type::FunctionLiteral(_)
|
||||||
|
@ -4099,11 +4154,13 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
*op,
|
*op,
|
||||||
values.iter().enumerate(),
|
values.iter().enumerate(),
|
||||||
|builder, (index, value)| {
|
|builder, (index, value)| {
|
||||||
if index == values.len() - 1 {
|
let ty = if index == values.len() - 1 {
|
||||||
builder.infer_expression(value)
|
builder.infer_expression(value)
|
||||||
} else {
|
} else {
|
||||||
builder.infer_standalone_expression(value)
|
builder.infer_standalone_expression(value)
|
||||||
}
|
};
|
||||||
|
|
||||||
|
(ty, value.range())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4120,7 +4177,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
) -> Type<'db>
|
) -> Type<'db>
|
||||||
where
|
where
|
||||||
Iterator: IntoIterator<Item = Item>,
|
Iterator: IntoIterator<Item = Item>,
|
||||||
F: Fn(&mut Self, Item) -> Type<'db>,
|
F: Fn(&mut Self, Item) -> (Type<'db>, TextRange),
|
||||||
{
|
{
|
||||||
let mut done = false;
|
let mut done = false;
|
||||||
let db = self.db();
|
let db = self.db();
|
||||||
|
@ -4128,37 +4185,48 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
let elements = operations
|
let elements = operations
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.with_position()
|
.with_position()
|
||||||
.map(|(position, ty)| {
|
.map(|(position, item)| {
|
||||||
let ty = infer_ty(self, ty);
|
let (ty, range) = infer_ty(self, item);
|
||||||
|
|
||||||
if done {
|
|
||||||
return Type::Never;
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_last = matches!(
|
let is_last = matches!(
|
||||||
position,
|
position,
|
||||||
itertools::Position::Last | itertools::Position::Only
|
itertools::Position::Last | itertools::Position::Only
|
||||||
);
|
);
|
||||||
|
|
||||||
match (ty.bool(db), is_last, op) {
|
if is_last {
|
||||||
(Truthiness::AlwaysTrue, false, ast::BoolOp::And) => Type::Never,
|
if done {
|
||||||
(Truthiness::AlwaysFalse, false, ast::BoolOp::Or) => Type::Never,
|
Type::Never
|
||||||
|
} else {
|
||||||
|
ty
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let truthiness = ty.try_bool(self.db()).unwrap_or_else(|err| {
|
||||||
|
err.report_diagnostic(&self.context, range);
|
||||||
|
err.fallback_truthiness()
|
||||||
|
});
|
||||||
|
|
||||||
(Truthiness::AlwaysFalse, _, ast::BoolOp::And)
|
if done {
|
||||||
| (Truthiness::AlwaysTrue, _, ast::BoolOp::Or) => {
|
return Type::Never;
|
||||||
|
};
|
||||||
|
|
||||||
|
match (truthiness, op) {
|
||||||
|
(Truthiness::AlwaysTrue, ast::BoolOp::And) => Type::Never,
|
||||||
|
(Truthiness::AlwaysFalse, ast::BoolOp::Or) => Type::Never,
|
||||||
|
|
||||||
|
(Truthiness::AlwaysFalse, ast::BoolOp::And)
|
||||||
|
| (Truthiness::AlwaysTrue, ast::BoolOp::Or) => {
|
||||||
done = true;
|
done = true;
|
||||||
ty
|
ty
|
||||||
}
|
}
|
||||||
|
|
||||||
(Truthiness::Ambiguous, false, _) => IntersectionBuilder::new(db)
|
(Truthiness::Ambiguous, _) => IntersectionBuilder::new(db)
|
||||||
.add_positive(ty)
|
.add_positive(ty)
|
||||||
.add_negative(match op {
|
.add_negative(match op {
|
||||||
ast::BoolOp::And => Type::AlwaysTruthy,
|
ast::BoolOp::And => Type::AlwaysTruthy,
|
||||||
ast::BoolOp::Or => Type::AlwaysFalsy,
|
ast::BoolOp::Or => Type::AlwaysFalsy,
|
||||||
})
|
})
|
||||||
.build(),
|
.build(),
|
||||||
|
}
|
||||||
(_, true, _) => ty,
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4174,9 +4242,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
} = compare;
|
} = compare;
|
||||||
|
|
||||||
self.infer_expression(left);
|
self.infer_expression(left);
|
||||||
for right in comparators {
|
|
||||||
self.infer_expression(right);
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://docs.python.org/3/reference/expressions.html#comparisons
|
// https://docs.python.org/3/reference/expressions.html#comparisons
|
||||||
// > Formally, if `a, b, c, …, y, z` are expressions and `op1, op2, …, opN` are comparison
|
// > Formally, if `a, b, c, …, y, z` are expressions and `op1, op2, …, opN` are comparison
|
||||||
|
@ -4193,15 +4258,17 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
.zip(ops),
|
.zip(ops),
|
||||||
|builder, ((left, right), op)| {
|
|builder, ((left, right), op)| {
|
||||||
let left_ty = builder.expression_type(left);
|
let left_ty = builder.expression_type(left);
|
||||||
let right_ty = builder.expression_type(right);
|
let right_ty = builder.infer_expression(right);
|
||||||
|
|
||||||
builder
|
let range = TextRange::new(left.start(), right.end());
|
||||||
.infer_binary_type_comparison(left_ty, *op, right_ty)
|
|
||||||
|
let ty = builder
|
||||||
|
.infer_binary_type_comparison(left_ty, *op, right_ty, range)
|
||||||
.unwrap_or_else(|error| {
|
.unwrap_or_else(|error| {
|
||||||
// Handle unsupported operators (diagnostic, `bool`/`Unknown` outcome)
|
// Handle unsupported operators (diagnostic, `bool`/`Unknown` outcome)
|
||||||
builder.context.report_lint(
|
builder.context.report_lint(
|
||||||
&UNSUPPORTED_OPERATOR,
|
&UNSUPPORTED_OPERATOR,
|
||||||
AnyNodeRef::ExprCompare(compare),
|
range,
|
||||||
format_args!(
|
format_args!(
|
||||||
"Operator `{}` is not supported for types `{}` and `{}`{}",
|
"Operator `{}` is not supported for types `{}` and `{}`{}",
|
||||||
error.op,
|
error.op,
|
||||||
|
@ -4228,7 +4295,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
// Other operators can return arbitrary types
|
// Other operators can return arbitrary types
|
||||||
_ => Type::unknown(),
|
_ => Type::unknown(),
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
(ty, range)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4239,14 +4308,19 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
op: ast::CmpOp,
|
op: ast::CmpOp,
|
||||||
other: Type<'db>,
|
other: Type<'db>,
|
||||||
intersection_on: IntersectionOn,
|
intersection_on: IntersectionOn,
|
||||||
|
range: TextRange,
|
||||||
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
||||||
// If a comparison yields a definitive true/false answer on a (positive) part
|
// If a comparison yields a definitive true/false answer on a (positive) part
|
||||||
// of an intersection type, it will also yield a definitive answer on the full
|
// of an intersection type, it will also yield a definitive answer on the full
|
||||||
// intersection type, which is even more specific.
|
// intersection type, which is even more specific.
|
||||||
for pos in intersection.positive(self.db()) {
|
for pos in intersection.positive(self.db()) {
|
||||||
let result = match intersection_on {
|
let result = match intersection_on {
|
||||||
IntersectionOn::Left => self.infer_binary_type_comparison(*pos, op, other)?,
|
IntersectionOn::Left => {
|
||||||
IntersectionOn::Right => self.infer_binary_type_comparison(other, op, *pos)?,
|
self.infer_binary_type_comparison(*pos, op, other, range)?
|
||||||
|
}
|
||||||
|
IntersectionOn::Right => {
|
||||||
|
self.infer_binary_type_comparison(other, op, *pos, range)?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if let Type::BooleanLiteral(b) = result {
|
if let Type::BooleanLiteral(b) = result {
|
||||||
return Ok(Type::BooleanLiteral(b));
|
return Ok(Type::BooleanLiteral(b));
|
||||||
|
@ -4257,8 +4331,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
// special cases that allow us to narrow down the result type of the comparison.
|
// special cases that allow us to narrow down the result type of the comparison.
|
||||||
for neg in intersection.negative(self.db()) {
|
for neg in intersection.negative(self.db()) {
|
||||||
let result = match intersection_on {
|
let result = match intersection_on {
|
||||||
IntersectionOn::Left => self.infer_binary_type_comparison(*neg, op, other).ok(),
|
IntersectionOn::Left => self
|
||||||
IntersectionOn::Right => self.infer_binary_type_comparison(other, op, *neg).ok(),
|
.infer_binary_type_comparison(*neg, op, other, range)
|
||||||
|
.ok(),
|
||||||
|
IntersectionOn::Right => self
|
||||||
|
.infer_binary_type_comparison(other, op, *neg, range)
|
||||||
|
.ok(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match (op, result) {
|
match (op, result) {
|
||||||
|
@ -4319,8 +4397,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
let mut builder = IntersectionBuilder::new(self.db());
|
let mut builder = IntersectionBuilder::new(self.db());
|
||||||
for pos in intersection.positive(self.db()) {
|
for pos in intersection.positive(self.db()) {
|
||||||
let result = match intersection_on {
|
let result = match intersection_on {
|
||||||
IntersectionOn::Left => self.infer_binary_type_comparison(*pos, op, other)?,
|
IntersectionOn::Left => {
|
||||||
IntersectionOn::Right => self.infer_binary_type_comparison(other, op, *pos)?,
|
self.infer_binary_type_comparison(*pos, op, other, range)?
|
||||||
|
}
|
||||||
|
IntersectionOn::Right => {
|
||||||
|
self.infer_binary_type_comparison(other, op, *pos, range)?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
builder = builder.add_positive(result);
|
builder = builder.add_positive(result);
|
||||||
}
|
}
|
||||||
|
@ -4339,6 +4421,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
left: Type<'db>,
|
left: Type<'db>,
|
||||||
op: ast::CmpOp,
|
op: ast::CmpOp,
|
||||||
right: Type<'db>,
|
right: Type<'db>,
|
||||||
|
range: TextRange,
|
||||||
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
||||||
// Note: identity (is, is not) for equal builtin types is unreliable and not part of the
|
// Note: identity (is, is not) for equal builtin types is unreliable and not part of the
|
||||||
// language spec.
|
// language spec.
|
||||||
|
@ -4348,14 +4431,16 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
(Type::Union(union), other) => {
|
(Type::Union(union), other) => {
|
||||||
let mut builder = UnionBuilder::new(self.db());
|
let mut builder = UnionBuilder::new(self.db());
|
||||||
for element in union.elements(self.db()) {
|
for element in union.elements(self.db()) {
|
||||||
builder = builder.add(self.infer_binary_type_comparison(*element, op, other)?);
|
builder =
|
||||||
|
builder.add(self.infer_binary_type_comparison(*element, op, other, range)?);
|
||||||
}
|
}
|
||||||
Ok(builder.build())
|
Ok(builder.build())
|
||||||
}
|
}
|
||||||
(other, Type::Union(union)) => {
|
(other, Type::Union(union)) => {
|
||||||
let mut builder = UnionBuilder::new(self.db());
|
let mut builder = UnionBuilder::new(self.db());
|
||||||
for element in union.elements(self.db()) {
|
for element in union.elements(self.db()) {
|
||||||
builder = builder.add(self.infer_binary_type_comparison(other, op, *element)?);
|
builder =
|
||||||
|
builder.add(self.infer_binary_type_comparison(other, op, *element, range)?);
|
||||||
}
|
}
|
||||||
Ok(builder.build())
|
Ok(builder.build())
|
||||||
}
|
}
|
||||||
|
@ -4366,6 +4451,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
op,
|
op,
|
||||||
right,
|
right,
|
||||||
IntersectionOn::Left,
|
IntersectionOn::Left,
|
||||||
|
range,
|
||||||
),
|
),
|
||||||
(left, Type::Intersection(intersection)) => self
|
(left, Type::Intersection(intersection)) => self
|
||||||
.infer_binary_intersection_type_comparison(
|
.infer_binary_intersection_type_comparison(
|
||||||
|
@ -4373,6 +4459,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
op,
|
op,
|
||||||
left,
|
left,
|
||||||
IntersectionOn::Right,
|
IntersectionOn::Right,
|
||||||
|
range,
|
||||||
),
|
),
|
||||||
|
|
||||||
(Type::IntLiteral(n), Type::IntLiteral(m)) => match op {
|
(Type::IntLiteral(n), Type::IntLiteral(m)) => match op {
|
||||||
|
@ -4403,29 +4490,38 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
right_ty: right,
|
right_ty: right,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
(Type::IntLiteral(_), Type::Instance(_)) => {
|
(Type::IntLiteral(_), Type::Instance(_)) => self.infer_binary_type_comparison(
|
||||||
self.infer_binary_type_comparison(KnownClass::Int.to_instance(self.db()), op, right)
|
KnownClass::Int.to_instance(self.db()),
|
||||||
}
|
op,
|
||||||
(Type::Instance(_), Type::IntLiteral(_)) => {
|
right,
|
||||||
self.infer_binary_type_comparison(left, op, KnownClass::Int.to_instance(self.db()))
|
range,
|
||||||
}
|
),
|
||||||
|
(Type::Instance(_), Type::IntLiteral(_)) => self.infer_binary_type_comparison(
|
||||||
|
left,
|
||||||
|
op,
|
||||||
|
KnownClass::Int.to_instance(self.db()),
|
||||||
|
range,
|
||||||
|
),
|
||||||
|
|
||||||
// Booleans are coded as integers (False = 0, True = 1)
|
// Booleans are coded as integers (False = 0, True = 1)
|
||||||
(Type::IntLiteral(n), Type::BooleanLiteral(b)) => self.infer_binary_type_comparison(
|
(Type::IntLiteral(n), Type::BooleanLiteral(b)) => self.infer_binary_type_comparison(
|
||||||
Type::IntLiteral(n),
|
Type::IntLiteral(n),
|
||||||
op,
|
op,
|
||||||
Type::IntLiteral(i64::from(b)),
|
Type::IntLiteral(i64::from(b)),
|
||||||
|
range,
|
||||||
),
|
),
|
||||||
(Type::BooleanLiteral(b), Type::IntLiteral(m)) => self.infer_binary_type_comparison(
|
(Type::BooleanLiteral(b), Type::IntLiteral(m)) => self.infer_binary_type_comparison(
|
||||||
Type::IntLiteral(i64::from(b)),
|
Type::IntLiteral(i64::from(b)),
|
||||||
op,
|
op,
|
||||||
Type::IntLiteral(m),
|
Type::IntLiteral(m),
|
||||||
|
range,
|
||||||
),
|
),
|
||||||
(Type::BooleanLiteral(a), Type::BooleanLiteral(b)) => self
|
(Type::BooleanLiteral(a), Type::BooleanLiteral(b)) => self
|
||||||
.infer_binary_type_comparison(
|
.infer_binary_type_comparison(
|
||||||
Type::IntLiteral(i64::from(a)),
|
Type::IntLiteral(i64::from(a)),
|
||||||
op,
|
op,
|
||||||
Type::IntLiteral(i64::from(b)),
|
Type::IntLiteral(i64::from(b)),
|
||||||
|
range,
|
||||||
),
|
),
|
||||||
|
|
||||||
(Type::StringLiteral(salsa_s1), Type::StringLiteral(salsa_s2)) => {
|
(Type::StringLiteral(salsa_s1), Type::StringLiteral(salsa_s2)) => {
|
||||||
|
@ -4456,19 +4552,31 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Type::StringLiteral(_), _) => {
|
(Type::StringLiteral(_), _) => self.infer_binary_type_comparison(
|
||||||
self.infer_binary_type_comparison(KnownClass::Str.to_instance(self.db()), op, right)
|
KnownClass::Str.to_instance(self.db()),
|
||||||
}
|
op,
|
||||||
(_, Type::StringLiteral(_)) => {
|
right,
|
||||||
self.infer_binary_type_comparison(left, op, KnownClass::Str.to_instance(self.db()))
|
range,
|
||||||
}
|
),
|
||||||
|
(_, Type::StringLiteral(_)) => self.infer_binary_type_comparison(
|
||||||
|
left,
|
||||||
|
op,
|
||||||
|
KnownClass::Str.to_instance(self.db()),
|
||||||
|
range,
|
||||||
|
),
|
||||||
|
|
||||||
(Type::LiteralString, _) => {
|
(Type::LiteralString, _) => self.infer_binary_type_comparison(
|
||||||
self.infer_binary_type_comparison(KnownClass::Str.to_instance(self.db()), op, right)
|
KnownClass::Str.to_instance(self.db()),
|
||||||
}
|
op,
|
||||||
(_, Type::LiteralString) => {
|
right,
|
||||||
self.infer_binary_type_comparison(left, op, KnownClass::Str.to_instance(self.db()))
|
range,
|
||||||
}
|
),
|
||||||
|
(_, Type::LiteralString) => self.infer_binary_type_comparison(
|
||||||
|
left,
|
||||||
|
op,
|
||||||
|
KnownClass::Str.to_instance(self.db()),
|
||||||
|
range,
|
||||||
|
),
|
||||||
|
|
||||||
(Type::BytesLiteral(salsa_b1), Type::BytesLiteral(salsa_b2)) => {
|
(Type::BytesLiteral(salsa_b1), Type::BytesLiteral(salsa_b2)) => {
|
||||||
let b1 = &**salsa_b1.value(self.db());
|
let b1 = &**salsa_b1.value(self.db());
|
||||||
|
@ -4506,21 +4614,33 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
KnownClass::Bytes.to_instance(self.db()),
|
KnownClass::Bytes.to_instance(self.db()),
|
||||||
op,
|
op,
|
||||||
right,
|
right,
|
||||||
|
range,
|
||||||
),
|
),
|
||||||
(_, Type::BytesLiteral(_)) => self.infer_binary_type_comparison(
|
(_, Type::BytesLiteral(_)) => self.infer_binary_type_comparison(
|
||||||
left,
|
left,
|
||||||
op,
|
op,
|
||||||
KnownClass::Bytes.to_instance(self.db()),
|
KnownClass::Bytes.to_instance(self.db()),
|
||||||
|
range,
|
||||||
),
|
),
|
||||||
(Type::Tuple(_), Type::Instance(InstanceType { class }))
|
(Type::Tuple(_), Type::Instance(InstanceType { class }))
|
||||||
if class.is_known(self.db(), KnownClass::VersionInfo) =>
|
if class.is_known(self.db(), KnownClass::VersionInfo) =>
|
||||||
{
|
{
|
||||||
self.infer_binary_type_comparison(left, op, Type::version_info_tuple(self.db()))
|
self.infer_binary_type_comparison(
|
||||||
|
left,
|
||||||
|
op,
|
||||||
|
Type::version_info_tuple(self.db()),
|
||||||
|
range,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
(Type::Instance(InstanceType { class }), Type::Tuple(_))
|
(Type::Instance(InstanceType { class }), Type::Tuple(_))
|
||||||
if class.is_known(self.db(), KnownClass::VersionInfo) =>
|
if class.is_known(self.db(), KnownClass::VersionInfo) =>
|
||||||
{
|
{
|
||||||
self.infer_binary_type_comparison(Type::version_info_tuple(self.db()), op, right)
|
self.infer_binary_type_comparison(
|
||||||
|
Type::version_info_tuple(self.db()),
|
||||||
|
op,
|
||||||
|
right,
|
||||||
|
range,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
(Type::Tuple(lhs), Type::Tuple(rhs)) => {
|
(Type::Tuple(lhs), Type::Tuple(rhs)) => {
|
||||||
// Note: This only works on heterogeneous tuple types.
|
// Note: This only works on heterogeneous tuple types.
|
||||||
|
@ -4528,7 +4648,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
let rhs_elements = rhs.elements(self.db());
|
let rhs_elements = rhs.elements(self.db());
|
||||||
|
|
||||||
let mut tuple_rich_comparison =
|
let mut tuple_rich_comparison =
|
||||||
|op| self.infer_tuple_rich_comparison(lhs_elements, op, rhs_elements);
|
|op| self.infer_tuple_rich_comparison(lhs_elements, op, rhs_elements, range);
|
||||||
|
|
||||||
match op {
|
match op {
|
||||||
ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq),
|
ast::CmpOp::Eq => tuple_rich_comparison(RichCompareOperator::Eq),
|
||||||
|
@ -4546,10 +4666,14 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
Type::Tuple(lhs),
|
Type::Tuple(lhs),
|
||||||
ast::CmpOp::Eq,
|
ast::CmpOp::Eq,
|
||||||
*ty,
|
*ty,
|
||||||
|
range,
|
||||||
).expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`");
|
).expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`");
|
||||||
|
|
||||||
match eq_result {
|
match eq_result {
|
||||||
todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo),
|
todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo),
|
||||||
|
// It's okay to ignore errors here because Python doesn't call `__bool__`
|
||||||
|
// for different union variants. Instead, this is just for us to
|
||||||
|
// evaluate a possibly truthy value to `false` or `true`.
|
||||||
ty => match ty.bool(self.db()) {
|
ty => match ty.bool(self.db()) {
|
||||||
Truthiness::AlwaysTrue => eq_count += 1,
|
Truthiness::AlwaysTrue => eq_count += 1,
|
||||||
Truthiness::AlwaysFalse => not_eq_count += 1,
|
Truthiness::AlwaysFalse => not_eq_count += 1,
|
||||||
|
@ -4575,6 +4699,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
|
|
||||||
Ok(match eq_result {
|
Ok(match eq_result {
|
||||||
todo @ Type::Dynamic(DynamicType::Todo(_)) => todo,
|
todo @ Type::Dynamic(DynamicType::Todo(_)) => todo,
|
||||||
|
// It's okay to ignore errors here because Python doesn't call `__bool__`
|
||||||
|
// for `is` and `is not` comparisons. This is an implementation detail
|
||||||
|
// for how we determine the truthiness of a type.
|
||||||
ty => match ty.bool(self.db()) {
|
ty => match ty.bool(self.db()) {
|
||||||
Truthiness::AlwaysFalse => Type::BooleanLiteral(op.is_is_not()),
|
Truthiness::AlwaysFalse => Type::BooleanLiteral(op.is_is_not()),
|
||||||
_ => KnownClass::Bool.to_instance(self.db()),
|
_ => KnownClass::Bool.to_instance(self.db()),
|
||||||
|
@ -4588,8 +4715,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
(Type::Instance(left_instance), Type::Instance(right_instance)) => {
|
(Type::Instance(left_instance), Type::Instance(right_instance)) => {
|
||||||
let rich_comparison =
|
let rich_comparison =
|
||||||
|op| self.infer_rich_comparison(left_instance, right_instance, op);
|
|op| self.infer_rich_comparison(left_instance, right_instance, op);
|
||||||
let membership_test_comparison =
|
let membership_test_comparison = |op, range: TextRange| {
|
||||||
|op| self.infer_membership_test_comparison(left_instance, right_instance, op);
|
self.infer_membership_test_comparison(left_instance, right_instance, op, range)
|
||||||
|
};
|
||||||
match op {
|
match op {
|
||||||
ast::CmpOp::Eq => rich_comparison(RichCompareOperator::Eq),
|
ast::CmpOp::Eq => rich_comparison(RichCompareOperator::Eq),
|
||||||
ast::CmpOp::NotEq => rich_comparison(RichCompareOperator::Ne),
|
ast::CmpOp::NotEq => rich_comparison(RichCompareOperator::Ne),
|
||||||
|
@ -4597,9 +4725,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
ast::CmpOp::LtE => rich_comparison(RichCompareOperator::Le),
|
ast::CmpOp::LtE => rich_comparison(RichCompareOperator::Le),
|
||||||
ast::CmpOp::Gt => rich_comparison(RichCompareOperator::Gt),
|
ast::CmpOp::Gt => rich_comparison(RichCompareOperator::Gt),
|
||||||
ast::CmpOp::GtE => rich_comparison(RichCompareOperator::Ge),
|
ast::CmpOp::GtE => rich_comparison(RichCompareOperator::Ge),
|
||||||
ast::CmpOp::In => membership_test_comparison(MembershipTestCompareOperator::In),
|
ast::CmpOp::In => {
|
||||||
|
membership_test_comparison(MembershipTestCompareOperator::In, range)
|
||||||
|
}
|
||||||
ast::CmpOp::NotIn => {
|
ast::CmpOp::NotIn => {
|
||||||
membership_test_comparison(MembershipTestCompareOperator::NotIn)
|
membership_test_comparison(MembershipTestCompareOperator::NotIn, range)
|
||||||
}
|
}
|
||||||
ast::CmpOp::Is => {
|
ast::CmpOp::Is => {
|
||||||
if left.is_disjoint_from(self.db(), right) {
|
if left.is_disjoint_from(self.db(), right) {
|
||||||
|
@ -4648,7 +4778,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
let call_dunder = |op: RichCompareOperator,
|
let call_dunder = |op: RichCompareOperator,
|
||||||
left: InstanceType<'db>,
|
left: InstanceType<'db>,
|
||||||
right: InstanceType<'db>| {
|
right: InstanceType<'db>| {
|
||||||
// TODO: How do we want to handle possibly unbound dunder methods?
|
|
||||||
match left.class.class_member(db, op.dunder()) {
|
match left.class.class_member(db, op.dunder()) {
|
||||||
Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder
|
Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder
|
||||||
.try_call(
|
.try_call(
|
||||||
|
@ -4693,6 +4822,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
left: InstanceType<'db>,
|
left: InstanceType<'db>,
|
||||||
right: InstanceType<'db>,
|
right: InstanceType<'db>,
|
||||||
op: MembershipTestCompareOperator,
|
op: MembershipTestCompareOperator,
|
||||||
|
range: TextRange,
|
||||||
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
||||||
let db = self.db();
|
let db = self.db();
|
||||||
|
|
||||||
|
@ -4710,11 +4840,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// iteration-based membership test
|
// iteration-based membership test
|
||||||
match Type::Instance(right).iterate(db) {
|
Type::Instance(right)
|
||||||
IterationOutcome::Iterable { .. } => Some(KnownClass::Bool.to_instance(db)),
|
.try_iterate(db)
|
||||||
IterationOutcome::NotIterable { .. }
|
.map(|_| KnownClass::Bool.to_instance(db))
|
||||||
| IterationOutcome::PossiblyUnboundDunderIter { .. } => None,
|
.ok()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4724,7 +4853,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
return ty;
|
return ty;
|
||||||
}
|
}
|
||||||
|
|
||||||
let truthiness = ty.bool(db);
|
let truthiness = ty.try_bool(db).unwrap_or_else(|err| {
|
||||||
|
err.report_diagnostic(&self.context, range);
|
||||||
|
err.fallback_truthiness()
|
||||||
|
});
|
||||||
|
|
||||||
match op {
|
match op {
|
||||||
MembershipTestCompareOperator::In => truthiness.into_type(db),
|
MembershipTestCompareOperator::In => truthiness.into_type(db),
|
||||||
|
@ -4748,6 +4880,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
left: &[Type<'db>],
|
left: &[Type<'db>],
|
||||||
op: RichCompareOperator,
|
op: RichCompareOperator,
|
||||||
right: &[Type<'db>],
|
right: &[Type<'db>],
|
||||||
|
range: TextRange,
|
||||||
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
||||||
let left_iter = left.iter().copied();
|
let left_iter = left.iter().copied();
|
||||||
let right_iter = right.iter().copied();
|
let right_iter = right.iter().copied();
|
||||||
|
@ -4756,13 +4889,17 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
|
|
||||||
for (l_ty, r_ty) in left_iter.zip(right_iter) {
|
for (l_ty, r_ty) in left_iter.zip(right_iter) {
|
||||||
let pairwise_eq_result = self
|
let pairwise_eq_result = self
|
||||||
.infer_binary_type_comparison(l_ty, ast::CmpOp::Eq, r_ty)
|
.infer_binary_type_comparison(l_ty, ast::CmpOp::Eq, r_ty, range)
|
||||||
.expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`");
|
.expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`");
|
||||||
|
|
||||||
match pairwise_eq_result {
|
match pairwise_eq_result
|
||||||
// If propagation is required, return the result as is
|
.try_bool(self.db())
|
||||||
todo @ Type::Dynamic(DynamicType::Todo(_)) => return Ok(todo),
|
.unwrap_or_else(|err| {
|
||||||
ty => match ty.bool(self.db()) {
|
// TODO: We should, whenever possible, pass the range of the left and right elements
|
||||||
|
// instead of the range of the whole tuple.
|
||||||
|
err.report_diagnostic(&self.context, range);
|
||||||
|
err.fallback_truthiness()
|
||||||
|
}) {
|
||||||
// - AlwaysTrue : Continue to the next pair for lexicographic comparison
|
// - AlwaysTrue : Continue to the next pair for lexicographic comparison
|
||||||
Truthiness::AlwaysTrue => continue,
|
Truthiness::AlwaysTrue => continue,
|
||||||
// - AlwaysFalse:
|
// - AlwaysFalse:
|
||||||
|
@ -4778,7 +4915,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
| RichCompareOperator::Le
|
| RichCompareOperator::Le
|
||||||
| RichCompareOperator::Gt
|
| RichCompareOperator::Gt
|
||||||
| RichCompareOperator::Ge => {
|
| RichCompareOperator::Ge => {
|
||||||
self.infer_binary_type_comparison(l_ty, op.into(), r_ty)?
|
self.infer_binary_type_comparison(l_ty, op.into(), r_ty, range)?
|
||||||
}
|
}
|
||||||
// For `==` and `!=`, we already figure out the result from `pairwise_eq_result`
|
// For `==` and `!=`, we already figure out the result from `pairwise_eq_result`
|
||||||
// NOTE: The CPython implementation does not account for non-boolean return types
|
// NOTE: The CPython implementation does not account for non-boolean return types
|
||||||
|
@ -4795,7 +4932,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
|
|
||||||
return Ok(builder.build());
|
return Ok(builder.build());
|
||||||
}
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,9 +57,10 @@ impl<'db> Unpacker<'db> {
|
||||||
if value.is_iterable() {
|
if value.is_iterable() {
|
||||||
// If the value is an iterable, then the type that needs to be unpacked is the iterator
|
// If the value is an iterable, then the type that needs to be unpacked is the iterator
|
||||||
// type.
|
// type.
|
||||||
value_ty = value_ty
|
value_ty = value_ty.try_iterate(self.db()).unwrap_or_else(|err| {
|
||||||
.iterate(self.db())
|
err.report_diagnostic(&self.context, value.as_any_node_ref(self.db()));
|
||||||
.unwrap_with_diagnostic(&self.context, value.as_any_node_ref(self.db()));
|
err.fallback_element_type()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.unpack_inner(target, value.as_any_node_ref(self.db()), value_ty);
|
self.unpack_inner(target, value.as_any_node_ref(self.db()), value_ty);
|
||||||
|
@ -155,8 +156,10 @@ impl<'db> Unpacker<'db> {
|
||||||
let ty = if ty.is_literal_string() {
|
let ty = if ty.is_literal_string() {
|
||||||
Type::LiteralString
|
Type::LiteralString
|
||||||
} else {
|
} else {
|
||||||
ty.iterate(self.db())
|
ty.try_iterate(self.db()).unwrap_or_else(|err| {
|
||||||
.unwrap_with_diagnostic(&self.context, value_expr)
|
err.report_diagnostic(&self.context, value_expr);
|
||||||
|
err.fallback_element_type()
|
||||||
|
})
|
||||||
};
|
};
|
||||||
for target_type in &mut target_types {
|
for target_type in &mut target_types {
|
||||||
target_type.push(ty);
|
target_type.push(ty);
|
||||||
|
|
|
@ -253,10 +253,15 @@ fn run_test(
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !snapshot_diagnostics.is_empty() {
|
if snapshot_diagnostics.is_empty() && test.should_snapshot_diagnostics() {
|
||||||
|
panic!(
|
||||||
|
"Test `{}` requested snapshotting diagnostics but it didn't produce any.",
|
||||||
|
test.name()
|
||||||
|
);
|
||||||
|
} else if !snapshot_diagnostics.is_empty() {
|
||||||
let snapshot =
|
let snapshot =
|
||||||
create_diagnostic_snapshot(db, relative_fixture_path, test, snapshot_diagnostics);
|
create_diagnostic_snapshot(db, relative_fixture_path, test, snapshot_diagnostics);
|
||||||
let name = test.name().replace(' ', "_");
|
let name = test.name().replace(' ', "_").replace(':', "__");
|
||||||
insta::with_settings!(
|
insta::with_settings!(
|
||||||
{
|
{
|
||||||
snapshot_path => snapshot_path,
|
snapshot_path => snapshot_path,
|
||||||
|
|
|
@ -595,6 +595,10 @@ impl<'s> Parser<'s> {
|
||||||
return self.process_config_block(code);
|
return self.process_config_block(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if lang == "ignore" {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(explicit_path) = self.explicit_path {
|
if let Some(explicit_path) = self.explicit_path {
|
||||||
if !lang.is_empty()
|
if !lang.is_empty()
|
||||||
&& lang != "text"
|
&& lang != "text"
|
||||||
|
@ -618,7 +622,7 @@ impl<'s> Parser<'s> {
|
||||||
EmbeddedFilePath::Explicit(path)
|
EmbeddedFilePath::Explicit(path)
|
||||||
}
|
}
|
||||||
None => match lang {
|
None => match lang {
|
||||||
"py" => EmbeddedFilePath::Autogenerated(PySourceType::Python),
|
"py" | "python" => EmbeddedFilePath::Autogenerated(PySourceType::Python),
|
||||||
"pyi" => EmbeddedFilePath::Autogenerated(PySourceType::Stub),
|
"pyi" => EmbeddedFilePath::Autogenerated(PySourceType::Stub),
|
||||||
"" => {
|
"" => {
|
||||||
bail!("Cannot auto-generate file name for code block with empty language specifier in test `{test_name}`");
|
bail!("Cannot auto-generate file name for code block with empty language specifier in test `{test_name}`");
|
||||||
|
|
|
@ -651,6 +651,16 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"unsupported-bool-conversion": {
|
||||||
|
"title": "detects boolean conversion where the object incorrectly implements `__bool__`",
|
||||||
|
"description": "## What it does\nChecks for bool conversions where the object doesn't correctly implement `__bool__`.\n\n## Why is this bad?\nIf an exception is raised when you attempt to evaluate the truthiness of an object,\nusing the object in a boolean context will fail at runtime.\n\n## Examples\n\n```python\nclass NotBoolable:\n __bool__ = None\n\nb1 = NotBoolable()\nb2 = NotBoolable()\n\nif b1: # exception raised here\n pass\n\nb1 and b2 # exception raised here\nnot b1 # exception raised here\nb1 < b2 < b1 # exception raised here\n```",
|
||||||
|
"default": "error",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Level"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"unsupported-operator": {
|
"unsupported-operator": {
|
||||||
"title": "detects binary, unary, or comparison expressions where the operands don't support the operator",
|
"title": "detects binary, unary, or comparison expressions where the operands don't support the operator",
|
||||||
"description": "## What it does\nChecks for binary expressions, comparisons, and unary expressions where the operands don't support the operator.\n\nTODO #14889",
|
"description": "## What it does\nChecks for binary expressions, comparisons, and unary expressions where the operands don't support the operator.\n\nTODO #14889",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue