[red-knot] detect invalid return type (#16540)

## Summary

This PR closes #16248.

If the return type of the function isn't assignable to the one
specified, an `invalid-return-type` error occurs.
I thought it would be better to report this as a different kind of error
than the `invalid-assignment` error, so I defined this as a new error.

## Test Plan

All type inconsistencies in the test cases have been replaced with
appropriate ones.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Shunsuke Shibayama
2025-03-12 10:58:59 +09:00
committed by GitHub
parent e17cd350b6
commit 78b5f0b165
43 changed files with 983 additions and 103 deletions

View File

@@ -0,0 +1,246 @@
# Function return type
When a function's return type is annotated, all return statements are checked to ensure that the
type of the returned value is assignable to the annotated return type. A `raise` is equivalent to a
return of `Never`, which is assignable to any annotated return type.
## Basic examples
A return value assignable to the annotated return type is valid.
```py
def f() -> int:
return 1
```
The type of the value obtained by calling a function is the annotated return type, not the inferred
return type.
```py
reveal_type(f()) # revealed: int
```
A `raise` is equivalent to a return of `Never`, which is assignable to any annotated return type.
```py
def f() -> str:
raise ValueError()
reveal_type(f()) # revealed: str
```
## Stub functions
"Stub" function definitions (that is, function definitions with an empty body) are permissible in
stub files, or in a few other locations: Protocol method definitions, abstract methods, and
overloads. In this case the function body is considered to be omitted (thus no return type checking
is performed on it), not assumed to implicitly return `None`.
A stub function's "empty" body may contain only an optional docstring, followed (optionally) by an
ellipsis (`...`) or `pass`.
### In stub file
```pyi
def f() -> int: ...
def f() -> int:
pass
def f() -> int:
"""Some docstring"""
def f() -> int:
"""Some docstring"""
...
```
### In Protocol
```py
from typing import Protocol
class Bar(Protocol):
# TODO: no error
# error: [invalid-return-type]
def f(self) -> int: ...
```
### In abstract method
```py
from abc import ABC, abstractmethod
class Foo(ABC):
@abstractmethod
# TODO: no error
# error: [invalid-return-type]
def f(self) -> int: ...
@abstractmethod
# error: [invalid-return-type]
def g[T](self, x: T) -> T: ...
```
### In overload
```py
from typing import overload
@overload
def f(x: int) -> int: ...
@overload
def f(x: str) -> str: ...
def f(x: int | str):
return x
```
## Conditional return type
```py
def f(cond: bool) -> int:
if cond:
return 1
else:
return 2
def f(cond: bool) -> int | None:
if cond:
return 1
else:
return
def f(cond: bool) -> int:
if cond:
return 1
else:
raise ValueError()
def f(cond: bool) -> str | int:
if cond:
return "a"
else:
return 1
```
## Implicit return type
```py
def f(cond: bool) -> int | None:
if cond:
return 1
# no implicit return
def f() -> int:
if True:
return 1
# no implicit return
def f(cond: bool) -> int:
cond = True
if cond:
return 1
def f(cond: bool) -> int:
if cond:
cond = True
else:
return 1
if cond:
return 2
```
## Invalid return type
<!-- snapshot-diagnostics -->
```py
# error: [invalid-return-type]
def f() -> int:
1
def f() -> str:
# error: [invalid-return-type]
return 1
def f() -> int:
# error: [invalid-return-type]
return
from typing import TypeVar
T = TypeVar("T")
# TODO: `invalid-return-type` error should be emitted
def m(x: T) -> T: ...
```
## Invalid return type in stub file
<!-- snapshot-diagnostics -->
```pyi
def f() -> int:
# error: [invalid-return-type]
return ...
# error: [invalid-return-type]
def foo() -> int:
print("...")
...
# error: [invalid-return-type]
def foo() -> int:
f"""{foo} is a function that ..."""
...
```
## Invalid conditional return type
<!-- snapshot-diagnostics -->
```py
def f(cond: bool) -> str:
if cond:
return "a"
else:
# error: [invalid-return-type]
return 1
def f(cond: bool) -> str:
if cond:
# error: [invalid-return-type]
return 1
else:
# error: [invalid-return-type]
return 2
```
## Invalid implicit return type
<!-- snapshot-diagnostics -->
```py
def f() -> None:
if False:
# error: [invalid-return-type]
return 1
# error: [invalid-return-type]
def f(cond: bool) -> int:
if cond:
return 1
# error: [invalid-return-type]
def f(cond: bool) -> int:
if cond:
raise ValueError()
# error: [invalid-return-type]
def f(cond: bool) -> int:
if cond:
cond = False
else:
return 1
if cond:
return 2
```