From 5a2aba237b6c76ceda59a8f0f155cc5cebb0b89b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 12 Dec 2025 21:53:29 +0000 Subject: [PATCH] [ty] Improve diagnostics for unsupported binary operations and unsupported augmented assignments (#21947) ## Summary This PR takes the improvements we made to unsupported-comparison diagnostics in https://github.com/astral-sh/ruff/pull/21737, and extends them to other `unsupported-operator` diagnostics. ## Test Plan Mdtests and snapshots --- .../resources/mdtest/assignment/augmented.md | 2 + .../resources/mdtest/binary/custom.md | 90 +++++++++----- .../resources/mdtest/binary/instances.md | 2 +- .../resources/mdtest/binary/unions.md | 2 +- .../mdtest/generics/legacy/functions.md | 4 +- .../mdtest/generics/pep695/functions.md | 4 +- .../resources/mdtest/implicit_type_aliases.md | 2 +- ...-…_-_Earlier_versions_(f2859c9800f37c7).snap | 7 +- ..._Unsupported_types_(a041d9e40c83a8ac).snap | 44 +++++++ ..._operat…_-_Classes_(93f2f1c488e06f53).snap | 80 ++++++++++++ ...lasses_from_differe…_(2890e4875c9b9c1e).snap | 44 +++++++ .../resources/mdtest/unary/custom.md | 54 ++++---- .../resources/mdtest/unary/invert_add_usub.md | 6 +- .../src/types/diagnostic.rs | 116 +++++++++++++++++- .../src/types/infer/builder.rs | 51 +++----- 15 files changed, 401 insertions(+), 107 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/augmented.md_-_Augmented_assignment_-_Unsupported_types_(a041d9e40c83a8ac).snap create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/custom.md_-_Custom_binary_operat…_-_Classes_(93f2f1c488e06f53).snap create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/custom.md_-_Custom_binary_operat…_-_Classes_from_differe…_(2890e4875c9b9c1e).snap diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md index cec8c6de43..fc4af5a52e 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/augmented.md @@ -38,6 +38,8 @@ reveal_type(x) # revealed: int ## Unsupported types + + ```py class C: def __isub__(self, other: str) -> int: diff --git a/crates/ty_python_semantic/resources/mdtest/binary/custom.md b/crates/ty_python_semantic/resources/mdtest/binary/custom.md index 9bd0852253..ad2b837195 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/custom.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/custom.md @@ -79,31 +79,31 @@ reveal_type(Sub() & Sub()) # revealed: Literal["&"] reveal_type(Sub() // Sub()) # revealed: Literal["//"] # No does not implement any of the dunder methods. -# error: [unsupported-operator] "Operator `+` is not supported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `+` is not supported between two objects of type `No`" reveal_type(No() + No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `-` is not supported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `-` is not supported between two objects of type `No`" reveal_type(No() - No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `*` is not supported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `*` is not supported between two objects of type `No`" reveal_type(No() * No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `@` is not supported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `@` is not supported between two objects of type `No`" reveal_type(No() @ No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `/` is not supported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `/` is not supported between two objects of type `No`" reveal_type(No() / No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `%` is not supported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `%` is not supported between two objects of type `No`" reveal_type(No() % No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `**` is not supported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `**` is not supported between two objects of type `No`" reveal_type(No() ** No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `<<` is not supported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `<<` is not supported between two objects of type `No`" reveal_type(No() << No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `>>` is not supported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `>>` is not supported between two objects of type `No`" reveal_type(No() >> No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `|` is not supported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `|` is not supported between two objects of type `No`" reveal_type(No() | No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `^` is not supported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `^` is not supported between two objects of type `No`" reveal_type(No() ^ No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `&` is not supported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `&` is not supported between two objects of type `No`" reveal_type(No() & No()) # revealed: Unknown -# error: [unsupported-operator] "Operator `//` is not supported between objects of type `No` and `No`" +# error: [unsupported-operator] "Operator `//` is not supported between two objects of type `No`" reveal_type(No() // No()) # revealed: Unknown # Yes does not implement any of the reflected dunder methods. @@ -293,6 +293,8 @@ reveal_type(Yes() // No()) # revealed: Literal["//"] ## Classes + + Dunder methods defined in a class are available to instances of that class, but not to the class itself. (For these operators to work on the class itself, they would have to be defined on the class's type, i.e. `type`.) @@ -307,11 +309,11 @@ class Yes: class Sub(Yes): ... class No: ... -# error: [unsupported-operator] "Operator `+` is not supported between objects of type `` and ``" +# error: [unsupported-operator] "Operator `+` is not supported between two objects of type ``" reveal_type(Yes + Yes) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is not supported between objects of type `` and ``" +# error: [unsupported-operator] "Operator `+` is not supported between two objects of type ``" reveal_type(Sub + Sub) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is not supported between objects of type `` and ``" +# error: [unsupported-operator] "Operator `+` is not supported between two objects of type ``" reveal_type(No + No) # revealed: Unknown ``` @@ -336,11 +338,11 @@ def sub() -> type[Sub]: def no() -> type[No]: return No -# error: [unsupported-operator] "Operator `+` is not supported between objects of type `type[Yes]` and `type[Yes]`" +# error: [unsupported-operator] "Operator `+` is not supported between two objects of type `type[Yes]`" reveal_type(yes() + yes()) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is not supported between objects of type `type[Sub]` and `type[Sub]`" +# error: [unsupported-operator] "Operator `+` is not supported between two objects of type `type[Sub]`" reveal_type(sub() + sub()) # revealed: Unknown -# error: [unsupported-operator] "Operator `+` is not supported between objects of type `type[No]` and `type[No]`" +# error: [unsupported-operator] "Operator `+` is not supported between two objects of type `type[No]`" reveal_type(no() + no()) # revealed: Unknown ``` @@ -350,30 +352,54 @@ reveal_type(no() + no()) # revealed: Unknown def f(): pass -# error: [unsupported-operator] "Operator `+` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `+` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f + f) # revealed: Unknown -# error: [unsupported-operator] "Operator `-` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `-` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f - f) # revealed: Unknown -# error: [unsupported-operator] "Operator `*` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `*` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f * f) # revealed: Unknown -# error: [unsupported-operator] "Operator `@` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `@` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f @ f) # revealed: Unknown -# error: [unsupported-operator] "Operator `/` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `/` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f / f) # revealed: Unknown -# error: [unsupported-operator] "Operator `%` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `%` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f % f) # revealed: Unknown -# error: [unsupported-operator] "Operator `**` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `**` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f**f) # revealed: Unknown -# error: [unsupported-operator] "Operator `<<` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `<<` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f << f) # revealed: Unknown -# error: [unsupported-operator] "Operator `>>` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `>>` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f >> f) # revealed: Unknown -# error: [unsupported-operator] "Operator `|` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `|` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f | f) # revealed: Unknown -# error: [unsupported-operator] "Operator `^` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `^` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f ^ f) # revealed: Unknown -# error: [unsupported-operator] "Operator `&` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `&` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f & f) # revealed: Unknown -# error: [unsupported-operator] "Operator `//` is not supported between objects of type `def f() -> Unknown` and `def f() -> Unknown`" +# error: [unsupported-operator] "Operator `//` is not supported between two objects of type `def f() -> Unknown`" reveal_type(f // f) # revealed: Unknown ``` + +## Classes from different modules with the same name + +We use the fully qualified names in diagnostics if the two classes have the same unqualified name, +but are nonetheless different. + + + +`mod1.py`: + +```py +class A: ... +``` + +`mod2.py`: + +```py +import mod1 + +class A: ... + +# error: [unsupported-operator] "Operator `+` is not supported between objects of type `mod2.A` and `mod1.A`" +A() + mod1.A() +``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/instances.md b/crates/ty_python_semantic/resources/mdtest/binary/instances.md index c1cc6f924e..1106bfbb74 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/instances.md @@ -412,7 +412,7 @@ class A: def __init__(self): self.__add__ = add_impl -# error: [unsupported-operator] "Operator `+` is not supported between objects of type `A` and `A`" +# error: [unsupported-operator] "Operator `+` is not supported between two objects of type `A`" # revealed: Unknown reveal_type(A() + A()) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/binary/unions.md b/crates/ty_python_semantic/resources/mdtest/binary/unions.md index 1b980170d4..c450d8d8de 100644 --- a/crates/ty_python_semantic/resources/mdtest/binary/unions.md +++ b/crates/ty_python_semantic/resources/mdtest/binary/unions.md @@ -18,7 +18,7 @@ cannot be added, because that would require addition of `int` and `str` or vice def f2(i: int, s: str, int_or_str: int | str): i + i s + s - # error: [unsupported-operator] "Operator `+` is not supported between objects of type `int | str` and `int | str`" + # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `int | str`" reveal_type(int_or_str + int_or_str) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index f883d1fb1a..77e3c23c78 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -277,7 +277,7 @@ T = TypeVar("T", int, str) def same_constrained_types(t1: T, t2: T) -> T: # TODO: no error - # error: [unsupported-operator] "Operator `+` is not supported between objects of type `T@same_constrained_types` and `T@same_constrained_types`" + # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `T@same_constrained_types`" return t1 + t2 ``` @@ -287,7 +287,7 @@ and an `int` and a `str` cannot be added together: ```py def unions_are_different(t1: int | str, t2: int | str) -> int | str: - # error: [unsupported-operator] "Operator `+` is not supported between objects of type `int | str` and `int | str`" + # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `int | str`" return t1 + t2 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index cb8f7dbc90..9d3cca6b57 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -246,7 +246,7 @@ methods that are compatible with the return type, so the `return` expression is ```py def same_constrained_types[T: (int, str)](t1: T, t2: T) -> T: # TODO: no error - # error: [unsupported-operator] "Operator `+` is not supported between objects of type `T@same_constrained_types` and `T@same_constrained_types`" + # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `T@same_constrained_types`" return t1 + t2 ``` @@ -256,7 +256,7 @@ and an `int` and a `str` cannot be added together: ```py def unions_are_different(t1: int | str, t2: int | str) -> int | str: - # error: [unsupported-operator] "Operator `+` is not supported between objects of type `int | str` and `int | str`" + # error: [unsupported-operator] "Operator `+` is not supported between two objects of type `int | str`" return t1 + t2 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index c2569023f1..ca0c4f6d8b 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -214,7 +214,7 @@ def _(int_or_int: IntOrInt, list_of_int_or_list_of_int: ListOfIntOrListOfInt): `NoneType` has no special or-operator behavior, so this is an error: ```py -None | None # error: [unsupported-operator] "Operator `|` is not supported between objects of type `None` and `None`" +None | None # error: [unsupported-operator] "Operator `|` is not supported between two objects of type `None`" ``` When constructing something nonsensical like `int | 1`, we emit a diagnostic for the expression diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno…_-_PEP-604_in_non-type-…_-_Earlier_versions_(f2859c9800f37c7).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno…_-_PEP-604_in_non-type-…_-_Earlier_versions_(f2859c9800f37c7).snap index 83b964676a..4435a4f727 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno…_-_PEP-604_in_non-type-…_-_Earlier_versions_(f2859c9800f37c7).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/annotations.md_-_Assignment_with_anno…_-_PEP-604_in_non-type-…_-_Earlier_versions_(f2859c9800f37c7).snap @@ -19,12 +19,15 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/assignment/annotations.m # Diagnostics ``` -error[unsupported-operator]: Operator `|` is not supported between objects of type `` and `` +error[unsupported-operator]: Unsupported `|` operation --> src/mdtest_snippet.py:2:12 | 1 | # error: [unsupported-operator] 2 | IntOrStr = int | str - | ^^^^^^^^^ + | ---^^^--- + | | | + | | Has type `` + | Has type `` | info: Note that `X | Y` PEP 604 union syntax is only available in Python 3.10 and later info: Python 3.9 was assumed when resolving types because it was specified on the command line diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/augmented.md_-_Augmented_assignment_-_Unsupported_types_(a041d9e40c83a8ac).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/augmented.md_-_Augmented_assignment_-_Unsupported_types_(a041d9e40c83a8ac).snap new file mode 100644 index 0000000000..0093cf5e4f --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/augmented.md_-_Augmented_assignment_-_Unsupported_types_(a041d9e40c83a8ac).snap @@ -0,0 +1,44 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: augmented.md - Augmented assignment - Unsupported types +mdtest path: crates/ty_python_semantic/resources/mdtest/assignment/augmented.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | class C: +2 | def __isub__(self, other: str) -> int: +3 | return 42 +4 | +5 | x = C() +6 | # error: [unsupported-operator] "Operator `-=` is not supported between objects of type `C` and `Literal[1]`" +7 | x -= 1 +8 | +9 | reveal_type(x) # revealed: int +``` + +# Diagnostics + +``` +error[unsupported-operator]: Unsupported `-=` operation + --> src/mdtest_snippet.py:7:1 + | +5 | x = C() +6 | # error: [unsupported-operator] "Operator `-=` is not supported between objects of type `C` and `Literal[1]`" +7 | x -= 1 + | -^^^^- + | | | + | | Has type `Literal[1]` + | Has type `C` +8 | +9 | reveal_type(x) # revealed: int + | +info: rule `unsupported-operator` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/custom.md_-_Custom_binary_operat…_-_Classes_(93f2f1c488e06f53).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/custom.md_-_Custom_binary_operat…_-_Classes_(93f2f1c488e06f53).snap new file mode 100644 index 0000000000..038ac1c411 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/custom.md_-_Custom_binary_operat…_-_Classes_(93f2f1c488e06f53).snap @@ -0,0 +1,80 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: custom.md - Custom binary operations - Classes +mdtest path: crates/ty_python_semantic/resources/mdtest/binary/custom.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Literal + 2 | + 3 | class Yes: + 4 | def __add__(self, other) -> Literal["+"]: + 5 | return "+" + 6 | + 7 | class Sub(Yes): ... + 8 | class No: ... + 9 | +10 | # error: [unsupported-operator] "Operator `+` is not supported between two objects of type ``" +11 | reveal_type(Yes + Yes) # revealed: Unknown +12 | # error: [unsupported-operator] "Operator `+` is not supported between two objects of type ``" +13 | reveal_type(Sub + Sub) # revealed: Unknown +14 | # error: [unsupported-operator] "Operator `+` is not supported between two objects of type ``" +15 | reveal_type(No + No) # revealed: Unknown +``` + +# Diagnostics + +``` +error[unsupported-operator]: Unsupported `+` operation + --> src/mdtest_snippet.py:11:13 + | +10 | # error: [unsupported-operator] "Operator `+` is not supported between two objects of type ``" +11 | reveal_type(Yes + Yes) # revealed: Unknown + | ---^^^--- + | | + | Both operands have type `` +12 | # error: [unsupported-operator] "Operator `+` is not supported between two objects of type ``" +13 | reveal_type(Sub + Sub) # revealed: Unknown + | +info: rule `unsupported-operator` is enabled by default + +``` + +``` +error[unsupported-operator]: Unsupported `+` operation + --> src/mdtest_snippet.py:13:13 + | +11 | reveal_type(Yes + Yes) # revealed: Unknown +12 | # error: [unsupported-operator] "Operator `+` is not supported between two objects of type ``" +13 | reveal_type(Sub + Sub) # revealed: Unknown + | ---^^^--- + | | + | Both operands have type `` +14 | # error: [unsupported-operator] "Operator `+` is not supported between two objects of type ``" +15 | reveal_type(No + No) # revealed: Unknown + | +info: rule `unsupported-operator` is enabled by default + +``` + +``` +error[unsupported-operator]: Unsupported `+` operation + --> src/mdtest_snippet.py:15:13 + | +13 | reveal_type(Sub + Sub) # revealed: Unknown +14 | # error: [unsupported-operator] "Operator `+` is not supported between two objects of type ``" +15 | reveal_type(No + No) # revealed: Unknown + | --^^^-- + | | + | Both operands have type `` + | +info: rule `unsupported-operator` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/custom.md_-_Custom_binary_operat…_-_Classes_from_differe…_(2890e4875c9b9c1e).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/custom.md_-_Custom_binary_operat…_-_Classes_from_differe…_(2890e4875c9b9c1e).snap new file mode 100644 index 0000000000..5591be03d7 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/custom.md_-_Custom_binary_operat…_-_Classes_from_differe…_(2890e4875c9b9c1e).snap @@ -0,0 +1,44 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: custom.md - Custom binary operations - Classes from different modules with the same name +mdtest path: crates/ty_python_semantic/resources/mdtest/binary/custom.md +--- + +# Python source files + +## mod1.py + +``` +1 | class A: ... +``` + +## mod2.py + +``` +1 | import mod1 +2 | +3 | class A: ... +4 | +5 | # error: [unsupported-operator] "Operator `+` is not supported between objects of type `mod2.A` and `mod1.A`" +6 | A() + mod1.A() +``` + +# Diagnostics + +``` +error[unsupported-operator]: Unsupported `+` operation + --> src/mod2.py:6:1 + | +5 | # error: [unsupported-operator] "Operator `+` is not supported between objects of type `mod2.A` and `mod1.A`" +6 | A() + mod1.A() + | ---^^^-------- + | | | + | | Has type `mod1.A` + | Has type `mod2.A` + | +info: rule `unsupported-operator` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/unary/custom.md b/crates/ty_python_semantic/resources/mdtest/unary/custom.md index 2b2eb3a619..c471ff509f 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/custom.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/custom.md @@ -24,11 +24,11 @@ reveal_type(+Sub()) # revealed: bool reveal_type(-Sub()) # revealed: str reveal_type(~Sub()) # revealed: int -# error: [unsupported-operator] "Unary operator `+` is not supported for type `No`" +# error: [unsupported-operator] "Unary operator `+` is not supported for object of type `No`" reveal_type(+No()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is not supported for type `No`" +# error: [unsupported-operator] "Unary operator `-` is not supported for object of type `No`" reveal_type(-No()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is not supported for type `No`" +# error: [unsupported-operator] "Unary operator `~` is not supported for object of type `No`" reveal_type(~No()) # revealed: Unknown ``` @@ -52,25 +52,25 @@ class Yes: class Sub(Yes): ... class No: ... -# error: [unsupported-operator] "Unary operator `+` is not supported for type ``" +# error: [unsupported-operator] "Unary operator `+` is not supported for object of type ``" reveal_type(+Yes) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is not supported for type ``" +# error: [unsupported-operator] "Unary operator `-` is not supported for object of type ``" reveal_type(-Yes) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is not supported for type ``" +# error: [unsupported-operator] "Unary operator `~` is not supported for object of type ``" reveal_type(~Yes) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `+` is not supported for type ``" +# error: [unsupported-operator] "Unary operator `+` is not supported for object of type ``" reveal_type(+Sub) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is not supported for type ``" +# error: [unsupported-operator] "Unary operator `-` is not supported for object of type ``" reveal_type(-Sub) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is not supported for type ``" +# error: [unsupported-operator] "Unary operator `~` is not supported for object of type ``" reveal_type(~Sub) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `+` is not supported for type ``" +# error: [unsupported-operator] "Unary operator `+` is not supported for object of type ``" reveal_type(+No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is not supported for type ``" +# error: [unsupported-operator] "Unary operator `-` is not supported for object of type ``" reveal_type(-No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is not supported for type ``" +# error: [unsupported-operator] "Unary operator `~` is not supported for object of type ``" reveal_type(~No) # revealed: Unknown ``` @@ -80,11 +80,11 @@ reveal_type(~No) # revealed: Unknown def f(): pass -# error: [unsupported-operator] "Unary operator `+` is not supported for type `def f() -> Unknown`" +# error: [unsupported-operator] "Unary operator `+` is not supported for object of type `def f() -> Unknown`" reveal_type(+f) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is not supported for type `def f() -> Unknown`" +# error: [unsupported-operator] "Unary operator `-` is not supported for object of type `def f() -> Unknown`" reveal_type(-f) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is not supported for type `def f() -> Unknown`" +# error: [unsupported-operator] "Unary operator `~` is not supported for object of type `def f() -> Unknown`" reveal_type(~f) # revealed: Unknown ``` @@ -113,25 +113,25 @@ def sub() -> type[Sub]: def no() -> type[No]: return No -# error: [unsupported-operator] "Unary operator `+` is not supported for type `type[Yes]`" +# error: [unsupported-operator] "Unary operator `+` is not supported for object of type `type[Yes]`" reveal_type(+yes()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is not supported for type `type[Yes]`" +# error: [unsupported-operator] "Unary operator `-` is not supported for object of type `type[Yes]`" reveal_type(-yes()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is not supported for type `type[Yes]`" +# error: [unsupported-operator] "Unary operator `~` is not supported for object of type `type[Yes]`" reveal_type(~yes()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `+` is not supported for type `type[Sub]`" +# error: [unsupported-operator] "Unary operator `+` is not supported for object of type `type[Sub]`" reveal_type(+sub()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is not supported for type `type[Sub]`" +# error: [unsupported-operator] "Unary operator `-` is not supported for object of type `type[Sub]`" reveal_type(-sub()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is not supported for type `type[Sub]`" +# error: [unsupported-operator] "Unary operator `~` is not supported for object of type `type[Sub]`" reveal_type(~sub()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `+` is not supported for type `type[No]`" +# error: [unsupported-operator] "Unary operator `+` is not supported for object of type `type[No]`" reveal_type(+no()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is not supported for type `type[No]`" +# error: [unsupported-operator] "Unary operator `-` is not supported for object of type `type[No]`" reveal_type(-no()) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is not supported for type `type[No]`" +# error: [unsupported-operator] "Unary operator `~` is not supported for object of type `type[No]`" reveal_type(~no()) # revealed: Unknown ``` @@ -160,10 +160,10 @@ reveal_type(+Sub) # revealed: bool reveal_type(-Sub) # revealed: str reveal_type(~Sub) # revealed: int -# error: [unsupported-operator] "Unary operator `+` is not supported for type ``" +# error: [unsupported-operator] "Unary operator `+` is not supported for object of type ``" reveal_type(+No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `-` is not supported for type ``" +# error: [unsupported-operator] "Unary operator `-` is not supported for object of type ``" reveal_type(-No) # revealed: Unknown -# error: [unsupported-operator] "Unary operator `~` is not supported for type ``" +# error: [unsupported-operator] "Unary operator `~` is not supported for object of type ``" reveal_type(~No) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md b/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md index 100176e1cb..53b4ca6366 100644 --- a/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md +++ b/crates/ty_python_semantic/resources/mdtest/unary/invert_add_usub.md @@ -27,7 +27,7 @@ reveal_type(~a) # revealed: Literal[True] class NoDunder: ... b = NoDunder() -+b # error: [unsupported-operator] "Unary operator `+` is not supported for type `NoDunder`" --b # error: [unsupported-operator] "Unary operator `-` is not supported for type `NoDunder`" -~b # error: [unsupported-operator] "Unary operator `~` is not supported for type `NoDunder`" ++b # error: [unsupported-operator] "Unary operator `+` is not supported for object of type `NoDunder`" +-b # error: [unsupported-operator] "Unary operator `-` is not supported for object of type `NoDunder`" +~b # error: [unsupported-operator] "Unary operator `~` is not supported for object of type `NoDunder`" ``` diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 23bb5b1a16..dcc2f6b3c5 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -40,7 +40,7 @@ use ruff_db::{ use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::name::Name; use ruff_python_ast::token::parentheses_iterator; -use ruff_python_ast::{self as ast, AnyNodeRef, StringFlags}; +use ruff_python_ast::{self as ast, AnyNodeRef, PythonVersion, StringFlags}; use ruff_text_size::{Ranged, TextRange}; use rustc_hash::FxHashSet; use std::fmt::{self, Formatter}; @@ -4155,6 +4155,120 @@ pub(super) fn report_unsupported_comparison<'db>( } } +pub(super) fn report_unsupported_augmented_assignment<'db>( + context: &InferContext<'db, '_>, + stmt: &ast::StmtAugAssign, + left_ty: Type<'db>, + right_ty: Type<'db>, +) { + report_unsupported_binary_operation_impl( + context, + stmt.range(), + &stmt.target, + &stmt.value, + left_ty, + right_ty, + OperatorDisplay { + operator: stmt.op, + is_augmented_assignment: true, + }, + ); +} + +pub(super) fn report_unsupported_binary_operation<'db>( + context: &InferContext<'db, '_>, + binary_expression: &ast::ExprBinOp, + left_ty: Type<'db>, + right_ty: Type<'db>, + operator: ast::Operator, +) { + let Some(mut diagnostic) = report_unsupported_binary_operation_impl( + context, + binary_expression.range(), + &binary_expression.left, + &binary_expression.right, + left_ty, + right_ty, + OperatorDisplay { + operator, + is_augmented_assignment: false, + }, + ) else { + return; + }; + let db = context.db(); + if operator == ast::Operator::BitOr + && (left_ty.is_subtype_of(db, KnownClass::Type.to_instance(db)) + || right_ty.is_subtype_of(db, KnownClass::Type.to_instance(db))) + && Program::get(db).python_version(db) < PythonVersion::PY310 + { + diagnostic.info( + "Note that `X | Y` PEP 604 union syntax is only available in Python 3.10 and later", + ); + add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, "resolving types"); + } +} + +#[derive(Debug, Copy, Clone)] +struct OperatorDisplay { + operator: ast::Operator, + is_augmented_assignment: bool, +} + +impl std::fmt::Display for OperatorDisplay { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_augmented_assignment { + write!(f, "{}=", self.operator) + } else { + write!(f, "{}", self.operator) + } + } +} + +fn report_unsupported_binary_operation_impl<'a>( + context: &'a InferContext<'a, 'a>, + range: TextRange, + left: &ast::Expr, + right: &ast::Expr, + left_ty: Type<'a>, + right_ty: Type<'a>, + operator: OperatorDisplay, +) -> Option> { + let db = context.db(); + let diagnostic_builder = context.report_lint(&UNSUPPORTED_OPERATOR, range)?; + let display_settings = DisplaySettings::from_possibly_ambiguous_types(db, [left_ty, right_ty]); + + let mut diagnostic = + diagnostic_builder.into_diagnostic(format_args!("Unsupported `{operator}` operation")); + + if left_ty == right_ty { + diagnostic.set_primary_message(format_args!( + "Both operands have type `{}`", + left_ty.display_with(db, display_settings.clone()) + )); + diagnostic.annotate(context.secondary(left)); + diagnostic.annotate(context.secondary(right)); + diagnostic.set_concise_message(format_args!( + "Operator `{operator}` is not supported between two objects of type `{}`", + left_ty.display_with(db, display_settings.clone()) + )); + } else { + for (ty, expr) in [(left_ty, left), (right_ty, right)] { + diagnostic.annotate(context.secondary(expr).message(format_args!( + "Has type `{}`", + ty.display_with(db, display_settings.clone()) + ))); + } + diagnostic.set_concise_message(format_args!( + "Operator `{operator}` is not supported between objects of type `{}` and `{}`", + left_ty.display_with(db, display_settings.clone()), + right_ty.display_with(db, display_settings.clone()) + )); + } + + Some(diagnostic) +} + /// This function receives an unresolved `from foo import bar` import, /// where `foo` can be resolved to a module but that module does not /// have a `bar` member or submodule. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 6df802b691..510ec1f467 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -80,7 +80,8 @@ use crate::types::diagnostic::{ report_invalid_type_checking_constant, report_named_tuple_field_with_leading_underscore, report_namedtuple_field_without_default_after_field_with_default, report_non_subscriptable, report_possibly_missing_attribute, report_possibly_unresolved_reference, - report_rebound_typevar, report_slice_step_size_zero, report_unsupported_comparison, + report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment, + report_unsupported_binary_operation, report_unsupported_comparison, }; use crate::types::function::{ FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral, @@ -5925,22 +5926,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let op = assignment.op; let db = self.db(); - let report_unsupported_augmented_op = |ctx: &mut InferContext| { - let Some(builder) = ctx.report_lint(&UNSUPPORTED_OPERATOR, assignment) else { - return; - }; - builder.into_diagnostic(format_args!( - "Operator `{op}=` is not supported between objects of type `{}` and `{}`", - target_type.display(db), - value_type.display(db) - )); - }; - // Fall back to non-augmented binary operator inference. let mut binary_return_ty = || { self.infer_binary_expression_type(assignment.into(), false, target_type, value_type, op) .unwrap_or_else(|| { - report_unsupported_augmented_op(&mut self.context); + report_unsupported_augmented_assignment( + &self.context, + assignment, + target_type, + value_type, + ); Type::unknown() }) }; @@ -5964,7 +5959,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { UnionType::from_elements(db, [outcome.return_type(db), binary_return_ty()]) } Err(CallDunderError::CallError(_, bindings)) => { - report_unsupported_augmented_op(&mut self.context); + report_unsupported_augmented_assignment( + &self.context, + assignment, + target_type, + value_type, + ); bindings.return_type(db) } } @@ -9699,7 +9699,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.context.report_lint(&UNSUPPORTED_OPERATOR, unary) { builder.into_diagnostic(format_args!( - "Unary operator `{op}` is not supported for type `{}`", + "Unary operator `{op}` is not supported for object of type `{}`", operand_type.display(self.db()), )); } @@ -9732,26 +9732,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_binary_expression_type(binary.into(), false, left_ty, right_ty, *op) .unwrap_or_else(|| { - let db = self.db(); - - if let Some(builder) = self.context.report_lint(&UNSUPPORTED_OPERATOR, binary) { - let mut diag = builder.into_diagnostic(format_args!( - "Operator `{op}` is not supported between objects of type `{}` and `{}`", - left_ty.display(db), - right_ty.display(db) - )); - - if op == &ast::Operator::BitOr - && (left_ty.is_subtype_of(db, KnownClass::Type.to_instance(db)) - || right_ty.is_subtype_of(db, KnownClass::Type.to_instance(db))) - && Program::get(db).python_version(db) < PythonVersion::PY310 - { - diag.info( - "Note that `X | Y` PEP 604 union syntax is only available in Python 3.10 and later", - ); - add_inferred_python_version_hint_to_diagnostic(db, &mut diag, "resolving types"); - } - } + report_unsupported_binary_operation(&self.context, binary, left_ty, right_ty, *op); Type::unknown() }) }