From 0f4781076864e60db0dabd52c8e0cd8955b7e2a9 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Wed, 23 Apr 2025 12:28:49 -0400 Subject: [PATCH] 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. --- .../mdtest/conditional/if_expression.md | 2 +- .../mdtest/conditional/if_statement.md | 4 +- .../resources/mdtest/conditional/match.md | 2 +- .../resources/mdtest/expression/assert.md | 2 +- .../resources/mdtest/expression/boolean.md | 6 +- .../resources/mdtest/loops/while_loop.md | 2 +- .../resources/mdtest/narrow/truthiness.md | 2 +- ...types_with_invalid_`__bool__`_methods.snap | 3 +- ...oesn't_implement_`__bool__`_correctly.snap | 6 +- ...hat_implements_`__bool__`_incorrectly.snap | 3 +- ..._don't_implement_`__bool__`_correctly.snap | 6 +- ...that_incorrectly_implement_`__bool__`.snap | 3 +- ...that_incorrectly_implement_`__bool__`.snap | 3 +- ...l__`_attribute,_but_it's_not_callable.snap | 3 +- ...hod,_but_has_an_incorrect_return_type.snap | 12 +++- ..._method,_but_has_incorrect_parameters.snap | 12 +++- .../resources/mdtest/type_api.md | 2 +- .../resources/mdtest/unary/not.md | 2 +- crates/red_knot_python_semantic/src/types.rs | 67 +++++++++++++++---- 19 files changed, 107 insertions(+), 35 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md index b14d358ea0..48b912cce1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md @@ -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 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md index 9a3fc4f8f4..c7a8c7732b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md @@ -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(): ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md b/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md index 8b8a3dca34..4947de76b9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md @@ -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: diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/assert.md b/crates/red_knot_python_semantic/resources/mdtest/expression/assert.md index 54073f9170..ddb429a576 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/assert.md +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/assert.md @@ -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() ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md b/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md index 160189ef0c..7a9f25f637 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md @@ -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: ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md b/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md index 397a06b742..5a5784b85d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md @@ -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(): ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md index d9bf54e8f8..d6a8684b38 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap index df0e16c766..9366971e47 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/instances.md_-_Binary_operations_on_instances_-_Operations_involving_types_with_invalid_`__bool__`_methods.snap @@ -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 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap index 4412f143f7..6dc5ab6afd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/membership_test.md_-_Comparison___Membership_Test_-_Return_type_that_doesn't_implement_`__bool__`_correctly.snap @@ -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 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap index b02543d3a5..21d48d1a7a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/not.md_-_Unary_not_-_Object_that_implements_`__bool__`_incorrectly.snap @@ -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 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap index ff2b61237c..baf8e5b0e0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/rich_comparison.md_-_Comparison___Rich_Comparison_-_Chained_comparisons_with_objects_that_don't_implement_`__bool__`_correctly.snap @@ -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 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap index f7ad96efef..db99489b41 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Chained_comparisons_with_elements_that_incorrectly_implement_`__bool__`.snap @@ -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 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap index 99de8491fc..972e7ed6d8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Equality_with_elements_that_incorrectly_implement_`__bool__`.snap @@ -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 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap index 2ef879a16f..d239ebaaea 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_attribute,_but_it's_not_callable.snap @@ -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 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap index 2b69db7bae..7cbee215c3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_an_incorrect_return_type.snap @@ -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" + | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap index a47dc7ee5f..6a2b93d51e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unsupported_bool_conversion.md_-_Different_ways_that_`UNSUPPORTED_BOOL_CONVERSION`_can_occur_-_Has_a_`__bool__`_method,_but_has_incorrect_parameters.snap @@ -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 + | ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_api.md b/crates/red_knot_python_semantic/resources/mdtest/type_api.md index c76f0f1374..c767fe98a0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_api.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_api.md @@ -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()) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/not.md b/crates/red_knot_python_semantic/resources/mdtest/unary/not.md index 82f589517a..e01796a9f7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/unary/not.md +++ b/crates/red_knot_python_semantic/resources/mdtest/unary/not.md @@ -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()) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 48792f95ca..4297b55221 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -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