mirror of https://github.com/astral-sh/ruff
red_knot_python_semantic: improve diagnostics for unsupported boolean conversions
This mostly only improves things for incorrect arguments and for an incorrect return type. It doesn't do much to improve the case where `__bool__` isn't callable and leaves the union/other cases untouched completely. I picked this one because, at first glance, this _looked_ like a lower hanging fruit. The conceptual improvement here is pretty straight-forward: add annotations for relevant data. But it took me a bit to figure out how to connect all of the pieces.
This commit is contained in:
parent
eb1d2518c1
commit
0f47810768
|
|
@ -42,6 +42,6 @@ def _(flag: bool):
|
|||
class NotBoolable:
|
||||
__bool__: int = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
3 if NotBoolable() else 4
|
||||
```
|
||||
|
|
|
|||
|
|
@ -154,10 +154,10 @@ def _(flag: bool):
|
|||
class NotBoolable:
|
||||
__bool__: int = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
if NotBoolable():
|
||||
...
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
elif NotBoolable():
|
||||
...
|
||||
```
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ class NotBoolable:
|
|||
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"
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
case 1 if flag:
|
||||
y = 2
|
||||
case 2:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@
|
|||
class NotBoolable:
|
||||
__bool__: int = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
assert NotBoolable()
|
||||
```
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ if NotBoolable():
|
|||
class NotBoolable:
|
||||
__bool__: None = None
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
if NotBoolable():
|
||||
...
|
||||
```
|
||||
|
|
@ -135,7 +135,7 @@ def test(cond: bool):
|
|||
class NotBoolable:
|
||||
__bool__: int | None = None if cond else 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
if NotBoolable():
|
||||
...
|
||||
```
|
||||
|
|
@ -149,7 +149,7 @@ def test(cond: bool):
|
|||
|
||||
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"
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`"
|
||||
if a:
|
||||
...
|
||||
```
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ def _(flag: bool, flag2: bool):
|
|||
class NotBoolable:
|
||||
__bool__: int = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
while NotBoolable():
|
||||
...
|
||||
```
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ def _(
|
|||
if af:
|
||||
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`; the return type of its bool method (`MetaAmbiguous`) isn't assignable to `bool"
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MetaDeferred`"
|
||||
if d:
|
||||
# TODO: Should be `Unknown`
|
||||
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy
|
||||
|
|
|
|||
|
|
@ -24,12 +24,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/binary/instances.m
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
|
||||
--> /src/mdtest_snippet.py:7:8
|
||||
|
|
||||
6 | # error: [unsupported-bool-conversion]
|
||||
7 | 10 and a and True
|
||||
| ^
|
||||
|
|
||||
info: `__bool__` on `NotBoolable` must be callable
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
|
||||
--> /src/mdtest_snippet.py:9:1
|
||||
|
|
||||
8 | # error: [unsupported-bool-conversion]
|
||||
|
|
@ -37,11 +37,12 @@ error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for t
|
|||
10 | # error: [unsupported-bool-conversion]
|
||||
11 | 10 not in WithContains()
|
||||
|
|
||||
info: `__bool__` on `NotBoolable` must be callable
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
|
||||
--> /src/mdtest_snippet.py:11:1
|
||||
|
|
||||
9 | 10 in WithContains()
|
||||
|
|
@ -49,5 +50,6 @@ error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for t
|
|||
11 | 10 not in WithContains()
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
info: `__bool__` on `NotBoolable` must be callable
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -22,12 +22,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/unary/not.md
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
|
||||
--> /src/mdtest_snippet.py:5:1
|
||||
|
|
||||
4 | # error: [unsupported-bool-conversion]
|
||||
5 | not NotBoolable()
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
info: `__bool__` on `NotBoolable` must be callable
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
|
||||
--> /src/mdtest_snippet.py:12:1
|
||||
|
|
||||
11 | # error: [unsupported-bool-conversion]
|
||||
|
|
@ -42,11 +42,12 @@ error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for t
|
|||
13 | # error: [unsupported-bool-conversion]
|
||||
14 | 10 < Comparable() < Comparable()
|
||||
|
|
||||
info: `__bool__` on `NotBoolable` must be callable
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
|
||||
--> /src/mdtest_snippet.py:14:1
|
||||
|
|
||||
12 | 10 < Comparable() < 20
|
||||
|
|
@ -56,5 +57,6 @@ error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for t
|
|||
15 |
|
||||
16 | Comparable() < Comparable() # fine
|
||||
|
|
||||
info: `__bool__` on `NotBoolable` must be callable
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable | Literal[False]`; its `__bool__` method isn't callable
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable | Literal[False]`
|
||||
--> /src/mdtest_snippet.py:15:1
|
||||
|
|
||||
14 | # error: [unsupported-bool-conversion]
|
||||
|
|
@ -43,5 +43,6 @@ error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for t
|
|||
16 |
|
||||
17 | a < b # fine
|
||||
|
|
||||
info: `__bool__` on `NotBoolable | Literal[False]` must be callable
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -26,12 +26,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
|
||||
--> /src/mdtest_snippet.py:9:1
|
||||
|
|
||||
8 | # error: [unsupported-bool-conversion]
|
||||
9 | (A(),) == (A(),)
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
|
|
||||
info: `__bool__` on `NotBoolable` must be callable
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -24,12 +24,13 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupp
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
|
||||
--> /src/mdtest_snippet.py:7:8
|
||||
|
|
||||
6 | # error: [unsupported-bool-conversion]
|
||||
7 | 10 and a and True
|
||||
| ^
|
||||
|
|
||||
info: `__bool__` must be callable
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -25,12 +25,22 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupp
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; the return type of its bool method (`str`) isn't assignable to `bool
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
|
||||
--> /src/mdtest_snippet.py:8:8
|
||||
|
|
||||
7 | # error: [unsupported-bool-conversion]
|
||||
8 | 10 and a and True
|
||||
| ^
|
||||
|
|
||||
info: `str` is not assignable to `bool`
|
||||
--> /src/mdtest_snippet.py:2:9
|
||||
|
|
||||
1 | class NotBoolable:
|
||||
2 | def __bool__(self) -> str:
|
||||
| -------- ^^^ Incorrect return type
|
||||
| |
|
||||
| Method defined here
|
||||
3 | return "wat"
|
||||
|
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -25,12 +25,22 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unsupp
|
|||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`; it incorrectly implements `__bool__`
|
||||
error: lint:unsupported-bool-conversion: Boolean conversion is unsupported for type `NotBoolable`
|
||||
--> /src/mdtest_snippet.py:8:8
|
||||
|
|
||||
7 | # error: [unsupported-bool-conversion]
|
||||
8 | 10 and a and True
|
||||
| ^
|
||||
|
|
||||
info: `__bool__` methods must only have a `self` parameter
|
||||
--> /src/mdtest_snippet.py:2:9
|
||||
|
|
||||
1 | class NotBoolable:
|
||||
2 | def __bool__(self, foo):
|
||||
| --------^^^^^^^^^^^ Incorrect parameters
|
||||
| |
|
||||
| Method defined here
|
||||
3 | return False
|
||||
|
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ class InvalidBoolDunder:
|
|||
def __bool__(self) -> int:
|
||||
return 1
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `InvalidBoolDunder`; the return type of its bool method (`int`) isn't assignable to `bool"
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `InvalidBoolDunder`"
|
||||
static_assert(InvalidBoolDunder())
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ class MethodBoolInvalid:
|
|||
def __bool__(self) -> int:
|
||||
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"
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `MethodBoolInvalid`"
|
||||
# revealed: bool
|
||||
reveal_type(not MethodBoolInvalid())
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ use diagnostic::{
|
|||
CALL_POSSIBLY_UNBOUND_METHOD, INVALID_CONTEXT_MANAGER, INVALID_SUPER_ARGUMENT, NOT_ITERABLE,
|
||||
UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS,
|
||||
};
|
||||
use ruff_db::diagnostic::create_semantic_syntax_diagnostic;
|
||||
use ruff_db::diagnostic::{
|
||||
create_semantic_syntax_diagnostic, Annotation, Severity, Span, SubDiagnostic,
|
||||
};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
|
|
@ -5763,30 +5765,71 @@ impl<'db> BoolError<'db> {
|
|||
Self::IncorrectArguments {
|
||||
not_boolable_type, ..
|
||||
} => {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Boolean conversion is unsupported for type `{}`; \
|
||||
it incorrectly implements `__bool__`",
|
||||
let mut diag = builder.into_diagnostic(format_args!(
|
||||
"Boolean conversion is unsupported for type `{}`",
|
||||
not_boolable_type.display(context.db())
|
||||
));
|
||||
let mut sub = SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
"`__bool__` methods must only have a `self` parameter",
|
||||
);
|
||||
if let Some((func_span, parameter_span)) = not_boolable_type
|
||||
.member(context.db(), "__bool__")
|
||||
.into_lookup_result()
|
||||
.ok()
|
||||
.and_then(|quals| quals.inner_type().parameter_span(context.db(), None))
|
||||
{
|
||||
sub.annotate(
|
||||
Annotation::primary(parameter_span).message("Incorrect parameters"),
|
||||
);
|
||||
sub.annotate(Annotation::secondary(func_span).message("Method defined here"));
|
||||
}
|
||||
diag.sub(sub);
|
||||
}
|
||||
Self::IncorrectReturnType {
|
||||
not_boolable_type,
|
||||
return_type,
|
||||
} => {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Boolean conversion is unsupported for type `{not_boolable}`; \
|
||||
the return type of its bool method (`{return_type}`) \
|
||||
isn't assignable to `bool",
|
||||
let mut diag = builder.into_diagnostic(format_args!(
|
||||
"Boolean conversion is unsupported for type `{not_boolable}`",
|
||||
not_boolable = not_boolable_type.display(context.db()),
|
||||
return_type = return_type.display(context.db())
|
||||
));
|
||||
let mut sub = SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
format_args!(
|
||||
"`{return_type}` is not assignable to `bool`",
|
||||
return_type = return_type.display(context.db()),
|
||||
),
|
||||
);
|
||||
if let Some((func_span, return_type_span)) = not_boolable_type
|
||||
.member(context.db(), "__bool__")
|
||||
.into_lookup_result()
|
||||
.ok()
|
||||
.and_then(|quals| quals.inner_type().return_type_span(context.db()))
|
||||
{
|
||||
sub.annotate(
|
||||
Annotation::primary(return_type_span).message("Incorrect return type"),
|
||||
);
|
||||
sub.annotate(Annotation::secondary(func_span).message("Method defined here"));
|
||||
}
|
||||
diag.sub(sub);
|
||||
}
|
||||
Self::NotCallable { not_boolable_type } => {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Boolean conversion is unsupported for type `{}`; \
|
||||
its `__bool__` method isn't callable",
|
||||
let mut diag = builder.into_diagnostic(format_args!(
|
||||
"Boolean conversion is unsupported for type `{}`",
|
||||
not_boolable_type.display(context.db())
|
||||
));
|
||||
let sub = SubDiagnostic::new(
|
||||
Severity::Info,
|
||||
format_args!(
|
||||
"`__bool__` on `{}` must be callable",
|
||||
not_boolable_type.display(context.db())
|
||||
),
|
||||
);
|
||||
// TODO: It would be nice to create an annotation here for
|
||||
// where `__bool__` is defined. At time of writing, I couldn't
|
||||
// figure out a straight-forward way of doing this. ---AG
|
||||
diag.sub(sub);
|
||||
}
|
||||
Self::Union { union, .. } => {
|
||||
let first_error = union
|
||||
|
|
|
|||
Loading…
Reference in New Issue