mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 06:11:43 +00:00
Don't special-case class instances in binary expression inference (#15161)
Just like in #15045 for unary expressions: In binary expressions, we were only looking for dunder expressions for `Type::Instance` types. We had some special cases for coercing the various `Literal` types into their corresponding `Instance` types before doing the lookup. But we can side-step all of that by using the existing `Type::to_meta_type` and `Type::to_instance` methods.
This commit is contained in:
parent
d45c1ee44f
commit
5e9259c96c
8 changed files with 555 additions and 60 deletions
|
@ -46,3 +46,50 @@ reveal_type(a | b) # revealed: Literal[True]
|
||||||
reveal_type(b | a) # revealed: Literal[True]
|
reveal_type(b | a) # revealed: Literal[True]
|
||||||
reveal_type(b | b) # revealed: Literal[False]
|
reveal_type(b | b) # revealed: Literal[False]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Arithmetic with a variable
|
||||||
|
|
||||||
|
```py
|
||||||
|
a = True
|
||||||
|
b = False
|
||||||
|
|
||||||
|
def lhs_is_int(x: int):
|
||||||
|
reveal_type(x + a) # revealed: int
|
||||||
|
reveal_type(x - a) # revealed: int
|
||||||
|
reveal_type(x * a) # revealed: int
|
||||||
|
reveal_type(x // a) # revealed: int
|
||||||
|
reveal_type(x / a) # revealed: float
|
||||||
|
reveal_type(x % a) # revealed: int
|
||||||
|
|
||||||
|
def rhs_is_int(x: int):
|
||||||
|
reveal_type(a + x) # revealed: int
|
||||||
|
reveal_type(a - x) # revealed: int
|
||||||
|
reveal_type(a * x) # revealed: int
|
||||||
|
reveal_type(a // x) # revealed: int
|
||||||
|
reveal_type(a / x) # revealed: float
|
||||||
|
reveal_type(a % x) # revealed: int
|
||||||
|
|
||||||
|
def lhs_is_bool(x: bool):
|
||||||
|
reveal_type(x + a) # revealed: int
|
||||||
|
reveal_type(x - a) # revealed: int
|
||||||
|
reveal_type(x * a) # revealed: int
|
||||||
|
reveal_type(x // a) # revealed: int
|
||||||
|
reveal_type(x / a) # revealed: float
|
||||||
|
reveal_type(x % a) # revealed: int
|
||||||
|
|
||||||
|
def rhs_is_bool(x: bool):
|
||||||
|
reveal_type(a + x) # revealed: int
|
||||||
|
reveal_type(a - x) # revealed: int
|
||||||
|
reveal_type(a * x) # revealed: int
|
||||||
|
reveal_type(a // x) # revealed: int
|
||||||
|
reveal_type(a / x) # revealed: float
|
||||||
|
reveal_type(a % x) # revealed: int
|
||||||
|
|
||||||
|
def both_are_bool(x: bool, y: bool):
|
||||||
|
reveal_type(x + y) # revealed: int
|
||||||
|
reveal_type(x - y) # revealed: int
|
||||||
|
reveal_type(x * y) # revealed: int
|
||||||
|
reveal_type(x // y) # revealed: int
|
||||||
|
reveal_type(x / y) # revealed: float
|
||||||
|
reveal_type(x % y) # revealed: int
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Binary operations on classes
|
||||||
|
|
||||||
|
## Union of two classes
|
||||||
|
|
||||||
|
Unioning two classes via the `|` operator is only available in Python 3.10 and later.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.10"
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A: ...
|
||||||
|
class B: ...
|
||||||
|
|
||||||
|
reveal_type(A | B) # revealed: UnionType
|
||||||
|
```
|
||||||
|
|
||||||
|
## Union of two classes (prior to 3.10)
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A: ...
|
||||||
|
class B: ...
|
||||||
|
|
||||||
|
# error: "Operator `|` is unsupported between objects of type `Literal[A]` and `Literal[B]`"
|
||||||
|
reveal_type(A | B) # revealed: Unknown
|
||||||
|
```
|
|
@ -0,0 +1,371 @@
|
||||||
|
# Custom binary operations
|
||||||
|
|
||||||
|
## Class instances
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Yes:
|
||||||
|
def __add__(self, other) -> Literal["+"]:
|
||||||
|
return "+"
|
||||||
|
|
||||||
|
def __sub__(self, other) -> Literal["-"]:
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
def __mul__(self, other) -> Literal["*"]:
|
||||||
|
return "*"
|
||||||
|
|
||||||
|
def __matmul__(self, other) -> Literal["@"]:
|
||||||
|
return "@"
|
||||||
|
|
||||||
|
def __truediv__(self, other) -> Literal["/"]:
|
||||||
|
return "/"
|
||||||
|
|
||||||
|
def __mod__(self, other) -> Literal["%"]:
|
||||||
|
return "%"
|
||||||
|
|
||||||
|
def __pow__(self, other) -> Literal["**"]:
|
||||||
|
return "**"
|
||||||
|
|
||||||
|
def __lshift__(self, other) -> Literal["<<"]:
|
||||||
|
return "<<"
|
||||||
|
|
||||||
|
def __rshift__(self, other) -> Literal[">>"]:
|
||||||
|
return ">>"
|
||||||
|
|
||||||
|
def __or__(self, other) -> Literal["|"]:
|
||||||
|
return "|"
|
||||||
|
|
||||||
|
def __xor__(self, other) -> Literal["^"]:
|
||||||
|
return "^"
|
||||||
|
|
||||||
|
def __and__(self, other) -> Literal["&"]:
|
||||||
|
return "&"
|
||||||
|
|
||||||
|
def __floordiv__(self, other) -> Literal["//"]:
|
||||||
|
return "//"
|
||||||
|
|
||||||
|
class Sub(Yes): ...
|
||||||
|
class No: ...
|
||||||
|
|
||||||
|
# Yes implements all of the dunder methods.
|
||||||
|
reveal_type(Yes() + Yes()) # revealed: Literal["+"]
|
||||||
|
reveal_type(Yes() - Yes()) # revealed: Literal["-"]
|
||||||
|
reveal_type(Yes() * Yes()) # revealed: Literal["*"]
|
||||||
|
reveal_type(Yes() @ Yes()) # revealed: Literal["@"]
|
||||||
|
reveal_type(Yes() / Yes()) # revealed: Literal["/"]
|
||||||
|
reveal_type(Yes() % Yes()) # revealed: Literal["%"]
|
||||||
|
reveal_type(Yes() ** Yes()) # revealed: Literal["**"]
|
||||||
|
reveal_type(Yes() << Yes()) # revealed: Literal["<<"]
|
||||||
|
reveal_type(Yes() >> Yes()) # revealed: Literal[">>"]
|
||||||
|
reveal_type(Yes() | Yes()) # revealed: Literal["|"]
|
||||||
|
reveal_type(Yes() ^ Yes()) # revealed: Literal["^"]
|
||||||
|
reveal_type(Yes() & Yes()) # revealed: Literal["&"]
|
||||||
|
reveal_type(Yes() // Yes()) # revealed: Literal["//"]
|
||||||
|
|
||||||
|
# Sub inherits Yes's implementation of the dunder methods.
|
||||||
|
reveal_type(Sub() + Sub()) # revealed: Literal["+"]
|
||||||
|
reveal_type(Sub() - Sub()) # revealed: Literal["-"]
|
||||||
|
reveal_type(Sub() * Sub()) # revealed: Literal["*"]
|
||||||
|
reveal_type(Sub() @ Sub()) # revealed: Literal["@"]
|
||||||
|
reveal_type(Sub() / Sub()) # revealed: Literal["/"]
|
||||||
|
reveal_type(Sub() % Sub()) # revealed: Literal["%"]
|
||||||
|
reveal_type(Sub() ** Sub()) # revealed: Literal["**"]
|
||||||
|
reveal_type(Sub() << Sub()) # revealed: Literal["<<"]
|
||||||
|
reveal_type(Sub() >> Sub()) # revealed: Literal[">>"]
|
||||||
|
reveal_type(Sub() | Sub()) # revealed: Literal["|"]
|
||||||
|
reveal_type(Sub() ^ Sub()) # revealed: Literal["^"]
|
||||||
|
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 unsupported between objects of type `No` and `No`"
|
||||||
|
reveal_type(No() + No()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `No`"
|
||||||
|
reveal_type(No() - No()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `No`"
|
||||||
|
reveal_type(No() * No()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `No`"
|
||||||
|
reveal_type(No() @ No()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `No`"
|
||||||
|
reveal_type(No() / No()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `No`"
|
||||||
|
reveal_type(No() % No()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `No`"
|
||||||
|
reveal_type(No() ** No()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `No`"
|
||||||
|
reveal_type(No() << No()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `No`"
|
||||||
|
reveal_type(No() >> No()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `No`"
|
||||||
|
reveal_type(No() | No()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `No`"
|
||||||
|
reveal_type(No() ^ No()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `No`"
|
||||||
|
reveal_type(No() & No()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `No`"
|
||||||
|
reveal_type(No() // No()) # revealed: Unknown
|
||||||
|
|
||||||
|
# Yes does not implement any of the reflected dunder methods.
|
||||||
|
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `Yes`"
|
||||||
|
reveal_type(No() + Yes()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `Yes`"
|
||||||
|
reveal_type(No() - Yes()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `Yes`"
|
||||||
|
reveal_type(No() * Yes()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `Yes`"
|
||||||
|
reveal_type(No() @ Yes()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `Yes`"
|
||||||
|
reveal_type(No() / Yes()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `Yes`"
|
||||||
|
reveal_type(No() % Yes()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `Yes`"
|
||||||
|
reveal_type(No() ** Yes()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `Yes`"
|
||||||
|
reveal_type(No() << Yes()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `Yes`"
|
||||||
|
reveal_type(No() >> Yes()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `Yes`"
|
||||||
|
reveal_type(No() | Yes()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `Yes`"
|
||||||
|
reveal_type(No() ^ Yes()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `Yes`"
|
||||||
|
reveal_type(No() & Yes()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `Yes`"
|
||||||
|
reveal_type(No() // Yes()) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subclass reflections override superclass dunders
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Yes:
|
||||||
|
def __add__(self, other) -> Literal["+"]:
|
||||||
|
return "+"
|
||||||
|
|
||||||
|
def __sub__(self, other) -> Literal["-"]:
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
def __mul__(self, other) -> Literal["*"]:
|
||||||
|
return "*"
|
||||||
|
|
||||||
|
def __matmul__(self, other) -> Literal["@"]:
|
||||||
|
return "@"
|
||||||
|
|
||||||
|
def __truediv__(self, other) -> Literal["/"]:
|
||||||
|
return "/"
|
||||||
|
|
||||||
|
def __mod__(self, other) -> Literal["%"]:
|
||||||
|
return "%"
|
||||||
|
|
||||||
|
def __pow__(self, other) -> Literal["**"]:
|
||||||
|
return "**"
|
||||||
|
|
||||||
|
def __lshift__(self, other) -> Literal["<<"]:
|
||||||
|
return "<<"
|
||||||
|
|
||||||
|
def __rshift__(self, other) -> Literal[">>"]:
|
||||||
|
return ">>"
|
||||||
|
|
||||||
|
def __or__(self, other) -> Literal["|"]:
|
||||||
|
return "|"
|
||||||
|
|
||||||
|
def __xor__(self, other) -> Literal["^"]:
|
||||||
|
return "^"
|
||||||
|
|
||||||
|
def __and__(self, other) -> Literal["&"]:
|
||||||
|
return "&"
|
||||||
|
|
||||||
|
def __floordiv__(self, other) -> Literal["//"]:
|
||||||
|
return "//"
|
||||||
|
|
||||||
|
class Sub(Yes):
|
||||||
|
def __radd__(self, other) -> Literal["r+"]:
|
||||||
|
return "r+"
|
||||||
|
|
||||||
|
def __rsub__(self, other) -> Literal["r-"]:
|
||||||
|
return "r-"
|
||||||
|
|
||||||
|
def __rmul__(self, other) -> Literal["r*"]:
|
||||||
|
return "r*"
|
||||||
|
|
||||||
|
def __rmatmul__(self, other) -> Literal["r@"]:
|
||||||
|
return "r@"
|
||||||
|
|
||||||
|
def __rtruediv__(self, other) -> Literal["r/"]:
|
||||||
|
return "r/"
|
||||||
|
|
||||||
|
def __rmod__(self, other) -> Literal["r%"]:
|
||||||
|
return "r%"
|
||||||
|
|
||||||
|
def __rpow__(self, other) -> Literal["r**"]:
|
||||||
|
return "r**"
|
||||||
|
|
||||||
|
def __rlshift__(self, other) -> Literal["r<<"]:
|
||||||
|
return "r<<"
|
||||||
|
|
||||||
|
def __rrshift__(self, other) -> Literal["r>>"]:
|
||||||
|
return "r>>"
|
||||||
|
|
||||||
|
def __ror__(self, other) -> Literal["r|"]:
|
||||||
|
return "r|"
|
||||||
|
|
||||||
|
def __rxor__(self, other) -> Literal["r^"]:
|
||||||
|
return "r^"
|
||||||
|
|
||||||
|
def __rand__(self, other) -> Literal["r&"]:
|
||||||
|
return "r&"
|
||||||
|
|
||||||
|
def __rfloordiv__(self, other) -> Literal["r//"]:
|
||||||
|
return "r//"
|
||||||
|
|
||||||
|
class No:
|
||||||
|
def __radd__(self, other) -> Literal["r+"]:
|
||||||
|
return "r+"
|
||||||
|
|
||||||
|
def __rsub__(self, other) -> Literal["r-"]:
|
||||||
|
return "r-"
|
||||||
|
|
||||||
|
def __rmul__(self, other) -> Literal["r*"]:
|
||||||
|
return "r*"
|
||||||
|
|
||||||
|
def __rmatmul__(self, other) -> Literal["r@"]:
|
||||||
|
return "r@"
|
||||||
|
|
||||||
|
def __rtruediv__(self, other) -> Literal["r/"]:
|
||||||
|
return "r/"
|
||||||
|
|
||||||
|
def __rmod__(self, other) -> Literal["r%"]:
|
||||||
|
return "r%"
|
||||||
|
|
||||||
|
def __rpow__(self, other) -> Literal["r**"]:
|
||||||
|
return "r**"
|
||||||
|
|
||||||
|
def __rlshift__(self, other) -> Literal["r<<"]:
|
||||||
|
return "r<<"
|
||||||
|
|
||||||
|
def __rrshift__(self, other) -> Literal["r>>"]:
|
||||||
|
return "r>>"
|
||||||
|
|
||||||
|
def __ror__(self, other) -> Literal["r|"]:
|
||||||
|
return "r|"
|
||||||
|
|
||||||
|
def __rxor__(self, other) -> Literal["r^"]:
|
||||||
|
return "r^"
|
||||||
|
|
||||||
|
def __rand__(self, other) -> Literal["r&"]:
|
||||||
|
return "r&"
|
||||||
|
|
||||||
|
def __rfloordiv__(self, other) -> Literal["r//"]:
|
||||||
|
return "r//"
|
||||||
|
|
||||||
|
# Subclass reflected dunder methods take precedence over the superclass's regular dunders.
|
||||||
|
reveal_type(Yes() + Sub()) # revealed: Literal["r+"]
|
||||||
|
reveal_type(Yes() - Sub()) # revealed: Literal["r-"]
|
||||||
|
reveal_type(Yes() * Sub()) # revealed: Literal["r*"]
|
||||||
|
reveal_type(Yes() @ Sub()) # revealed: Literal["r@"]
|
||||||
|
reveal_type(Yes() / Sub()) # revealed: Literal["r/"]
|
||||||
|
reveal_type(Yes() % Sub()) # revealed: Literal["r%"]
|
||||||
|
reveal_type(Yes() ** Sub()) # revealed: Literal["r**"]
|
||||||
|
reveal_type(Yes() << Sub()) # revealed: Literal["r<<"]
|
||||||
|
reveal_type(Yes() >> Sub()) # revealed: Literal["r>>"]
|
||||||
|
reveal_type(Yes() | Sub()) # revealed: Literal["r|"]
|
||||||
|
reveal_type(Yes() ^ Sub()) # revealed: Literal["r^"]
|
||||||
|
reveal_type(Yes() & Sub()) # revealed: Literal["r&"]
|
||||||
|
reveal_type(Yes() // Sub()) # revealed: Literal["r//"]
|
||||||
|
|
||||||
|
# But for an unrelated class, the superclass regular dunders are used.
|
||||||
|
reveal_type(Yes() + No()) # revealed: Literal["+"]
|
||||||
|
reveal_type(Yes() - No()) # revealed: Literal["-"]
|
||||||
|
reveal_type(Yes() * No()) # revealed: Literal["*"]
|
||||||
|
reveal_type(Yes() @ No()) # revealed: Literal["@"]
|
||||||
|
reveal_type(Yes() / No()) # revealed: Literal["/"]
|
||||||
|
reveal_type(Yes() % No()) # revealed: Literal["%"]
|
||||||
|
reveal_type(Yes() ** No()) # revealed: Literal["**"]
|
||||||
|
reveal_type(Yes() << No()) # revealed: Literal["<<"]
|
||||||
|
reveal_type(Yes() >> No()) # revealed: Literal[">>"]
|
||||||
|
reveal_type(Yes() | No()) # revealed: Literal["|"]
|
||||||
|
reveal_type(Yes() ^ No()) # revealed: Literal["^"]
|
||||||
|
reveal_type(Yes() & No()) # revealed: Literal["&"]
|
||||||
|
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`.)
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Yes:
|
||||||
|
def __add__(self, other) -> Literal["+"]:
|
||||||
|
return "+"
|
||||||
|
|
||||||
|
class Sub(Yes): ...
|
||||||
|
class No: ...
|
||||||
|
|
||||||
|
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Yes]` and `Literal[Yes]`"
|
||||||
|
reveal_type(Yes + Yes) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Sub]` and `Literal[Sub]`"
|
||||||
|
reveal_type(Sub + Sub) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[No]` and `Literal[No]`"
|
||||||
|
reveal_type(No + No) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subclass
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Yes:
|
||||||
|
def __add__(self, other) -> Literal["+"]:
|
||||||
|
return "+"
|
||||||
|
|
||||||
|
class Sub(Yes): ...
|
||||||
|
class No: ...
|
||||||
|
|
||||||
|
def yes() -> type[Yes]:
|
||||||
|
return Yes
|
||||||
|
|
||||||
|
def sub() -> type[Sub]:
|
||||||
|
return Sub
|
||||||
|
|
||||||
|
def no() -> type[No]:
|
||||||
|
return No
|
||||||
|
|
||||||
|
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Yes]` and `type[Yes]`"
|
||||||
|
reveal_type(yes() + yes()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Sub]` and `type[Sub]`"
|
||||||
|
reveal_type(sub() + sub()) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[No]` and `type[No]`"
|
||||||
|
reveal_type(no() + no()) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
## Function literals
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
pass
|
||||||
|
|
||||||
|
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||||
|
reveal_type(f + f) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||||
|
reveal_type(f - f) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||||
|
reveal_type(f * f) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||||
|
reveal_type(f @ f) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||||
|
reveal_type(f / f) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||||
|
reveal_type(f % f) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||||
|
reveal_type(f**f) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||||
|
reveal_type(f << f) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||||
|
reveal_type(f >> f) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||||
|
reveal_type(f | f) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||||
|
reveal_type(f ^ f) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||||
|
reveal_type(f & f) # revealed: Unknown
|
||||||
|
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `Literal[f]` and `Literal[f]`"
|
||||||
|
reveal_type(f // f) # revealed: Unknown
|
||||||
|
```
|
|
@ -9,6 +9,34 @@ reveal_type(3 * -1) # revealed: Literal[-3]
|
||||||
reveal_type(-3 // 3) # revealed: Literal[-1]
|
reveal_type(-3 // 3) # revealed: Literal[-1]
|
||||||
reveal_type(-3 / 3) # revealed: float
|
reveal_type(-3 / 3) # revealed: float
|
||||||
reveal_type(5 % 3) # revealed: Literal[2]
|
reveal_type(5 % 3) # revealed: Literal[2]
|
||||||
|
|
||||||
|
# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared
|
||||||
|
# formal parameter type.
|
||||||
|
reveal_type(2 + "f") # revealed: int
|
||||||
|
|
||||||
|
def lhs(x: int):
|
||||||
|
reveal_type(x + 1) # revealed: int
|
||||||
|
reveal_type(x - 4) # revealed: int
|
||||||
|
reveal_type(x * -1) # revealed: int
|
||||||
|
reveal_type(x // 3) # revealed: int
|
||||||
|
reveal_type(x / 3) # revealed: float
|
||||||
|
reveal_type(x % 3) # revealed: int
|
||||||
|
|
||||||
|
def rhs(x: int):
|
||||||
|
reveal_type(2 + x) # revealed: int
|
||||||
|
reveal_type(3 - x) # revealed: int
|
||||||
|
reveal_type(3 * x) # revealed: int
|
||||||
|
reveal_type(-3 // x) # revealed: int
|
||||||
|
reveal_type(-3 / x) # revealed: float
|
||||||
|
reveal_type(5 % x) # revealed: int
|
||||||
|
|
||||||
|
def both(x: int):
|
||||||
|
reveal_type(x + x) # revealed: int
|
||||||
|
reveal_type(x - x) # revealed: int
|
||||||
|
reveal_type(x * x) # revealed: int
|
||||||
|
reveal_type(x // x) # revealed: int
|
||||||
|
reveal_type(x / x) # revealed: float
|
||||||
|
reveal_type(x % x) # revealed: int
|
||||||
```
|
```
|
||||||
|
|
||||||
## Power
|
## Power
|
||||||
|
@ -21,6 +49,11 @@ largest_u32 = 4_294_967_295
|
||||||
reveal_type(2**2) # revealed: Literal[4]
|
reveal_type(2**2) # revealed: Literal[4]
|
||||||
reveal_type(1 ** (largest_u32 + 1)) # revealed: int
|
reveal_type(1 ** (largest_u32 + 1)) # revealed: int
|
||||||
reveal_type(2**largest_u32) # revealed: int
|
reveal_type(2**largest_u32) # revealed: int
|
||||||
|
|
||||||
|
def variable(x: int):
|
||||||
|
reveal_type(x**2) # revealed: @Todo(return type)
|
||||||
|
reveal_type(2**x) # revealed: @Todo(return type)
|
||||||
|
reveal_type(x**x) # revealed: @Todo(return type)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Division by Zero
|
## Division by Zero
|
||||||
|
|
|
@ -167,7 +167,7 @@ class A:
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
__slots__ += ("a", "b")
|
__slots__ += ("a", "b")
|
||||||
|
|
||||||
reveal_type(A.__slots__) # revealed: @Todo(Support for more binary expressions)
|
reveal_type(A.__slots__) # revealed: @Todo(return type)
|
||||||
|
|
||||||
class B:
|
class B:
|
||||||
__slots__ = ("c", "d")
|
__slots__ = ("c", "d")
|
||||||
|
|
|
@ -34,6 +34,10 @@ reveal_type(~No()) # revealed: Unknown
|
||||||
|
|
||||||
## Classes
|
## 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`.)
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class Yes:
|
class Yes:
|
||||||
def __pos__(self) -> bool:
|
def __pos__(self) -> bool:
|
||||||
|
|
|
@ -3398,6 +3398,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
// When interacting with Todo, Any and Unknown should propagate (as if we fix this
|
// When interacting with Todo, Any and Unknown should propagate (as if we fix this
|
||||||
// `Todo` in the future, the result would then become Any or Unknown, respectively.)
|
// `Todo` in the future, the result would then become Any or Unknown, respectively.)
|
||||||
(Type::Any, _, _) | (_, Type::Any, _) => Some(Type::Any),
|
(Type::Any, _, _) | (_, Type::Any, _) => Some(Type::Any),
|
||||||
|
(todo @ Type::Todo(_), _, _) | (_, todo @ Type::Todo(_), _) => Some(todo),
|
||||||
|
(Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never),
|
||||||
(Type::Unknown, _, _) | (_, Type::Unknown, _) => Some(Type::Unknown),
|
(Type::Unknown, _, _) | (_, Type::Unknown, _) => Some(Type::Unknown),
|
||||||
|
|
||||||
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some(
|
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some(
|
||||||
|
@ -3496,54 +3498,78 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
Some(ty)
|
Some(ty)
|
||||||
}
|
}
|
||||||
|
|
||||||
(Type::Instance(_), Type::IntLiteral(_), op) => self.infer_binary_expression_type(
|
(Type::BooleanLiteral(b1), Type::BooleanLiteral(b2), ast::Operator::BitOr) => {
|
||||||
left_ty,
|
Some(Type::BooleanLiteral(b1 | b2))
|
||||||
KnownClass::Int.to_instance(self.db()),
|
}
|
||||||
|
|
||||||
|
(Type::BooleanLiteral(bool_value), right, op) => self.infer_binary_expression_type(
|
||||||
|
Type::IntLiteral(i64::from(bool_value)),
|
||||||
|
right,
|
||||||
op,
|
op,
|
||||||
),
|
),
|
||||||
|
(left, Type::BooleanLiteral(bool_value), op) => {
|
||||||
|
self.infer_binary_expression_type(left, Type::IntLiteral(i64::from(bool_value)), op)
|
||||||
|
}
|
||||||
|
|
||||||
(Type::IntLiteral(_), Type::Instance(_), op) => self.infer_binary_expression_type(
|
// We've handled all of the special cases that we support for literals, so we need to
|
||||||
KnownClass::Int.to_instance(self.db()),
|
// fall back on looking for dunder methods on one of the operand types.
|
||||||
right_ty,
|
(
|
||||||
|
Type::FunctionLiteral(_)
|
||||||
|
| Type::ModuleLiteral(_)
|
||||||
|
| Type::ClassLiteral(_)
|
||||||
|
| Type::SubclassOf(_)
|
||||||
|
| Type::Instance(_)
|
||||||
|
| Type::KnownInstance(_)
|
||||||
|
| Type::Union(_)
|
||||||
|
| Type::Intersection(_)
|
||||||
|
| Type::AlwaysTruthy
|
||||||
|
| Type::AlwaysFalsy
|
||||||
|
| Type::IntLiteral(_)
|
||||||
|
| Type::StringLiteral(_)
|
||||||
|
| Type::LiteralString
|
||||||
|
| Type::BytesLiteral(_)
|
||||||
|
| Type::SliceLiteral(_)
|
||||||
|
| Type::Tuple(_),
|
||||||
|
Type::FunctionLiteral(_)
|
||||||
|
| Type::ModuleLiteral(_)
|
||||||
|
| Type::ClassLiteral(_)
|
||||||
|
| Type::SubclassOf(_)
|
||||||
|
| Type::Instance(_)
|
||||||
|
| Type::KnownInstance(_)
|
||||||
|
| Type::Union(_)
|
||||||
|
| Type::Intersection(_)
|
||||||
|
| Type::AlwaysTruthy
|
||||||
|
| Type::AlwaysFalsy
|
||||||
|
| Type::IntLiteral(_)
|
||||||
|
| Type::StringLiteral(_)
|
||||||
|
| Type::LiteralString
|
||||||
|
| Type::BytesLiteral(_)
|
||||||
|
| Type::SliceLiteral(_)
|
||||||
|
| Type::Tuple(_),
|
||||||
op,
|
op,
|
||||||
),
|
) => {
|
||||||
|
// We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from
|
||||||
|
// the Python spec [1] is:
|
||||||
|
//
|
||||||
|
// - If rhs is a (proper) subclass of lhs, and it provides a different
|
||||||
|
// implementation of __rop__, use that.
|
||||||
|
// - Otherwise, if lhs implements __op__, use that.
|
||||||
|
// - Otherwise, if lhs and rhs are different types, and rhs implements __rop__,
|
||||||
|
// use that.
|
||||||
|
//
|
||||||
|
// [1] https://docs.python.org/3/reference/datamodel.html#object.__radd__
|
||||||
|
|
||||||
(Type::Instance(_), Type::Tuple(_), op) => self.infer_binary_expression_type(
|
// Technically we don't have to check left_ty != right_ty here, since if the types
|
||||||
left_ty,
|
// are the same, they will trivially have the same implementation of the reflected
|
||||||
KnownClass::Tuple.to_instance(self.db()),
|
// dunder, and so we'll fail the inner check. But the type equality check will be
|
||||||
op,
|
// faster for the common case, and allow us to skip the (two) class member lookups.
|
||||||
),
|
let left_class = left_ty.to_meta_type(self.db());
|
||||||
|
let right_class = right_ty.to_meta_type(self.db());
|
||||||
(Type::Tuple(_), Type::Instance(_), op) => self.infer_binary_expression_type(
|
if left_ty != right_ty && right_ty.is_subtype_of(self.db(), left_ty) {
|
||||||
KnownClass::Tuple.to_instance(self.db()),
|
|
||||||
right_ty,
|
|
||||||
op,
|
|
||||||
),
|
|
||||||
|
|
||||||
(Type::Instance(_), Type::StringLiteral(_) | Type::LiteralString, op) => self
|
|
||||||
.infer_binary_expression_type(left_ty, KnownClass::Str.to_instance(self.db()), op),
|
|
||||||
|
|
||||||
(Type::StringLiteral(_) | Type::LiteralString, Type::Instance(_), op) => self
|
|
||||||
.infer_binary_expression_type(KnownClass::Str.to_instance(self.db()), right_ty, op),
|
|
||||||
|
|
||||||
(Type::Instance(_), Type::BytesLiteral(_), op) => self.infer_binary_expression_type(
|
|
||||||
left_ty,
|
|
||||||
KnownClass::Bytes.to_instance(self.db()),
|
|
||||||
op,
|
|
||||||
),
|
|
||||||
|
|
||||||
(Type::BytesLiteral(_), Type::Instance(_), op) => self.infer_binary_expression_type(
|
|
||||||
KnownClass::Bytes.to_instance(self.db()),
|
|
||||||
right_ty,
|
|
||||||
op,
|
|
||||||
),
|
|
||||||
|
|
||||||
(Type::Instance(left), Type::Instance(right), op) => {
|
|
||||||
if left != right && right.is_subtype_of(self.db(), left) {
|
|
||||||
let reflected_dunder = op.reflected_dunder();
|
let reflected_dunder = op.reflected_dunder();
|
||||||
let rhs_reflected = right.class.class_member(self.db(), reflected_dunder);
|
let rhs_reflected = right_class.member(self.db(), reflected_dunder);
|
||||||
if !rhs_reflected.is_unbound()
|
if !rhs_reflected.is_unbound()
|
||||||
&& rhs_reflected != left.class.class_member(self.db(), reflected_dunder)
|
&& rhs_reflected != left_class.member(self.db(), reflected_dunder)
|
||||||
{
|
{
|
||||||
return right_ty
|
return right_ty
|
||||||
.call_dunder(self.db(), reflected_dunder, &[right_ty, left_ty])
|
.call_dunder(self.db(), reflected_dunder, &[right_ty, left_ty])
|
||||||
|
@ -3557,7 +3583,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let call_on_left_instance = if let Symbol::Type(class_member, _) =
|
let call_on_left_instance = if let Symbol::Type(class_member, _) =
|
||||||
left.class.class_member(self.db(), op.dunder())
|
left_class.member(self.db(), op.dunder())
|
||||||
{
|
{
|
||||||
class_member
|
class_member
|
||||||
.call(self.db(), &[left_ty, right_ty])
|
.call(self.db(), &[left_ty, right_ty])
|
||||||
|
@ -3567,11 +3593,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
};
|
};
|
||||||
|
|
||||||
call_on_left_instance.or_else(|| {
|
call_on_left_instance.or_else(|| {
|
||||||
if left == right {
|
if left_ty == right_ty {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
if let Symbol::Type(class_member, _) =
|
if let Symbol::Type(class_member, _) =
|
||||||
right.class.class_member(self.db(), op.reflected_dunder())
|
right_class.member(self.db(), op.reflected_dunder())
|
||||||
{
|
{
|
||||||
class_member
|
class_member
|
||||||
.call(self.db(), &[right_ty, left_ty])
|
.call(self.db(), &[right_ty, left_ty])
|
||||||
|
@ -3582,20 +3608,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
(Type::BooleanLiteral(b1), Type::BooleanLiteral(b2), ast::Operator::BitOr) => {
|
|
||||||
Some(Type::BooleanLiteral(b1 | b2))
|
|
||||||
}
|
|
||||||
|
|
||||||
(Type::BooleanLiteral(bool_value), right, op) => self.infer_binary_expression_type(
|
|
||||||
Type::IntLiteral(i64::from(bool_value)),
|
|
||||||
right,
|
|
||||||
op,
|
|
||||||
),
|
|
||||||
(left, Type::BooleanLiteral(bool_value), op) => {
|
|
||||||
self.infer_binary_expression_type(left, Type::IntLiteral(i64::from(bool_value)), op)
|
|
||||||
}
|
|
||||||
_ => Some(todo_type!("Support for more binary expressions")),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -272,7 +272,8 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
|
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
|
||||||
// related to circular references in class definitions
|
// related to circular references in class definitions
|
||||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, false),
|
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, true),
|
||||||
|
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py", true, true),
|
||||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F811_19.py", true, false),
|
("crates/ruff_linter/resources/test/fixtures/pyflakes/F811_19.py", true, false),
|
||||||
("crates/ruff_linter/resources/test/fixtures/pyupgrade/UP039.py", true, false),
|
("crates/ruff_linter/resources/test/fixtures/pyupgrade/UP039.py", true, false),
|
||||||
// related to circular references in type aliases (salsa cycle panic):
|
// related to circular references in type aliases (salsa cycle panic):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue