[red-knot] revert change to emit fewer division by zero errors (#13801)

This reverts https://github.com/astral-sh/ruff/pull/13799, and restores
the previous behavior, which I think was the most pragmatic and useful
version of the divide-by-zero error, if we will emit it at all.

In general, a type checker _does_ emit diagnostics when it can detect
something that will definitely be a problem for some inhabitants of a
type, but not others. For example, `x.foo` if `x` is typed as `object`
is a type error, even though some inhabitants of the type `object` will
have a `foo` attribute! The correct fix is to make your type annotations
more precise, so that `x` is assigned a type which definitely has the
`foo` attribute.

If we will emit it divide-by-zero errors, it should follow the same
logic. Dividing an inhabitant of the type `int` by zero may not emit an
error, if the inhabitant is an instance of a subclass of `builtins.int`
that overrides division. But it may emit an error (more likely it will).
If you don't want the diagnostic, you can clarify your type annotations
to require an instance of your safe subclass.

Because the Python type system doesn't have the ability to explicitly
reflect the fact that divide-by-zero is an error in type annotations
(e.g. for `int.__truediv__`), or conversely to declare a type as safe
from divide-by-zero, or include a "nonzero integer" type which it is
always safe to divide by, the analogy doesn't fully apply. You can't
explicitly mark your subclass of `int` as safe from divide-by-zero, we
just semi-arbitrarily choose to silence the diagnostic for subclasses,
to avoid false positives.

Also, if we fully followed the above logic, we'd have to error on every
`int / int` because the RHS `int` might be zero! But this would likely
cause too many false positives, because of the lack of a "nonzero
integer" type.

So this is just a pragmatic choice to emit the diagnostic when it is
very likely to be an error. It's unclear how useful this diagnostic is
in practice, but this version of it is at least very unlikely to cause
harm.
This commit is contained in:
Carl Meyer 2024-10-17 13:17:22 -07:00 committed by GitHub
parent 5c537b6dbb
commit e2a30b71f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 24 additions and 14 deletions

View file

@ -20,28 +20,35 @@ reveal_type(f) # revealed: Literal[2]
## Division by Zero
This error is really outside the current Python type system, because e.g. `int.__truediv__` and
friends are not annotated to indicate that it's an error, and we don't even have a facility to
permit such an annotation. So arguably divide-by-zero should be a lint error rather than a type
checker error. But we choose to go ahead and error in the cases that are very likely to be an error:
dividing something typed as `int` or `float` by something known to be `Literal[0]`.
This isn't _definitely_ an error, because the object typed as `int` or `float` could be an instance
of a custom subclass which overrides division behavior to handle zero without error. But if this
unusual case occurs, the error can be avoided by explicitly typing the dividend as that safe custom
subclass; we only emit the error if the LHS type is exactly `int` or `float`, not if its a subclass.
```py
class MyInt(int):
def __truediv__(self, other):
return 100
def returns_int() -> int:
return MyInt(3)
# TODO: `a` should be `int` and `e` should be `float` once we support inference.
a = 1 / 0 # error: "Cannot divide object of type `Literal[1]` by zero"
b = 2 // 0 # error: "Cannot floor divide object of type `Literal[2]` by zero"
c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero"
# even `int` type could be a subclass of `int` with custom behavior; no error
d = returns_int() / 0
# this could be flagged as an error, if we had an ExactFloat or ExactInstance
# type, but given only a `float` type we can't issue an error for the same
# reason: could be a custom float subclass
e = 1.0 / 0
d = int() / 0 # error: "Cannot divide object of type `int` by zero"
e = 1.0 / 0 # error: "Cannot divide object of type `float` by zero"
reveal_type(a) # revealed: float
reveal_type(b) # revealed: int
reveal_type(c) # revealed: int
# TODO should be int
reveal_type(d) # revealed: @Todo
# TODO should be float
reveal_type(e) # revealed: @Todo
class MyInt(int): pass
# No error for a subclass of int
# TODO should be float
reveal_type(MyInt(3) / 0) # revealed: @Todo
```

View file

@ -540,6 +540,9 @@ impl<'db> TypeInferenceBuilder<'db> {
fn check_division_by_zero(&mut self, expr: &ast::ExprBinOp, left: Type<'db>) {
match left {
Type::IntLiteral(_) => {}
Type::Instance(cls)
if cls.is_known(self.db, KnownClass::Float)
|| cls.is_known(self.db, KnownClass::Int) => {}
_ => return,
};