facebook / facebook/pyrefly

report unsafe operator type on subclasses

Open
#1,176 4 comments 0 reactions 0 assignees View on GitHub
typechecking
Dominant language
Rust
Stars
7k
Forks
516
PR merge metrics
No merged PRs in 30d

Description

### Describe the Bug

```py
class A:
def __add__(self, other: A) -> int:
return 1

class B(A):
def __radd__(self, other: A) -> str:
return "B"

def f(a1: A, a2: A):
x: int = a1 + a2
print(x) # static: int, runtime: "B"

f(A(), B())
```

here, the right hand subtype rule will be used, and `B.__radd__` will be called first, resulting in `"B"`, not `1`

if the supertype additionally defines a `__radd__`, then it can be incompatible with it's `__add__`, as all the information about the operation will be known

to ensure safety we must report when the subtypes `__radd__` in incompatable with the supertypes `__add__`, when the super type does not define a `__radd__`. but the right hand subtype rule complicates things...

so we should always form a union when the lower bound is not known:
```py
class A:
def __add__(self, other: object) -> int: ...
def __radd__(self, other: object) -> str: ...

def __sub__(self, other: object) -> int: ...

def __or__(self, other: object) -> int: ...

class B(A):
def __radd__(self, other: object) -> str: ... # no error, valid override of A.__radd__

def __rsub__(self, other: object) -> Literal[1]: ... # not error, compatiable with A.__add__

def __ror__(self, other: object) -> str: ... # error, incompatible with A.__or__

def f(a1: A = A(), a2: A = B()):
# when we don't know what `a2` is
a1 + a2 # static: int | str, runtime: str

# we definitely know that rhs is a subtype of lhs
A() + B() # static: str, runtime: str

# we definitely know that rhs is not a subtype of lhs
a1 + A() # static: int, runtime: int
```

# this also applies to inplace operators
```py
class A:
def __add__(self, other: A) -> int:
return 1
class B(A):
def __iadd__(self, other: A) -> str: # expect error: `__iadd__` must be compatable with `A.__add__`, as `A` does not define `__iadd__`
return "a"

def f(a1: A, a2: A):
a = a1
a += a2
print(a) # static: int, runtime: "a"
f(B(), A())
```
note that the righthand subtype rule applies after `__iadd__`, so this case is much more straightforward to support

### Sandbox Link

_No response_

### (Only applicable for extension issues) IDE Information

_No response_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.