[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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 983 additions and 103 deletions

View file

@ -86,7 +86,7 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
"libs/utils.py",
r#"
def add(a: int, b: int) -> int:
a + b
return a + b
"#,
),
(
@ -158,7 +158,7 @@ fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Re
"libs/utils.py",
r#"
def add(a: int, b: int) -> int:
a + b
return a + b
"#,
),
(

View file

@ -38,7 +38,8 @@ If `__future__.annotations` is imported, annotations *are* deferred.
```py
from __future__ import annotations
def get_foo() -> Foo: ...
def get_foo() -> Foo:
return Foo()
class Foo: ...

View file

@ -348,8 +348,11 @@ reveal_type(C().y) # revealed: Unknown | str
```py
class ContextManager:
def __enter__(self) -> int | None: ...
def __exit__(self, exc_type, exc_value, traceback) -> None: ...
def __enter__(self) -> int | None:
return 1
def __exit__(self, exc_type, exc_value, traceback) -> None:
pass
class C:
def __init__(self) -> None:
@ -365,8 +368,11 @@ reveal_type(c_instance.x) # revealed: Unknown | int | None
```py
class ContextManager:
def __enter__(self) -> tuple[int | None, int]: ...
def __exit__(self, exc_type, exc_value, traceback) -> None: ...
def __enter__(self) -> tuple[int | None, int]:
return 1, 2
def __exit__(self, exc_type, exc_value, traceback) -> None:
pass
class C:
def __init__(self) -> None:

View file

@ -406,10 +406,12 @@ A left-hand dunder method doesn't apply for the right-hand operand, or vice vers
```py
class A:
def __add__(self, other) -> int: ...
def __add__(self, other) -> int:
return 1
class B:
def __radd__(self, other) -> int: ...
def __radd__(self, other) -> int:
return 1
class C: ...

View file

@ -69,7 +69,8 @@ without raising an error.
from typing import Any
def any() -> Any: ...
def flag() -> bool: ...
def flag() -> bool:
return True
a: int
b: str
@ -126,7 +127,8 @@ inferred types:
from typing import Any
def any() -> Any: ...
def flag() -> bool: ...
def flag() -> bool:
return True
a = 1
b = 2
@ -164,7 +166,8 @@ error for both `a` and `b`:
```py
from typing import Any
def flag() -> bool: ...
def flag() -> bool:
return True
if flag():
a: Any = 1
@ -194,7 +197,8 @@ seems inconsistent when compared to the case just above.
`mod.py`:
```py
def flag() -> bool: ...
def flag() -> bool:
return True
if flag():
a: int
@ -248,7 +252,8 @@ inconsistent when compared to the "possibly-undeclared-and-possibly-unbound" cas
`mod.py`:
```py
def flag() -> bool: ...
def flag() -> bool:
return True
if flag:
a = 1

View file

@ -25,7 +25,8 @@ reveal_type(b) # revealed: Unknown
def _(flag: bool):
class PossiblyNotCallable:
if flag:
def __call__(self) -> int: ...
def __call__(self) -> int:
return 1
a = PossiblyNotCallable()
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
@ -38,7 +39,8 @@ def _(flag: bool):
def _(flag: bool):
if flag:
class PossiblyUnbound:
def __call__(self) -> int: ...
def __call__(self) -> int:
return 1
# error: [possibly-unresolved-reference]
a = PossiblyUnbound()
@ -64,7 +66,8 @@ def _(flag: bool):
if flag:
__call__ = 1
else:
def __call__(self) -> int: ...
def __call__(self) -> int:
return 1
a = NonCallable()
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"

View file

@ -185,7 +185,7 @@ def _(flag: bool):
return str(key)
else:
def __getitem__(self, key: int) -> bytes:
return key
return bytes()
c = C()
reveal_type(c[0]) # revealed: str | bytes
@ -198,7 +198,7 @@ def _(flag: bool):
else:
class D:
def __getitem__(self, key: int) -> bytes:
return key
return bytes()
d = D()
reveal_type(d[0]) # revealed: str | bytes

View file

@ -37,6 +37,8 @@ def foo() -> int:
return 42
def decorator(func) -> Callable[[], int]:
# TODO: no error
# error: [invalid-return-type]
return foo
@decorator

View file

@ -82,8 +82,12 @@ def _(flag: bool):
Calling a union where the arguments don't match the signature of all variants.
```py
def f1(a: int) -> int: ...
def f2(a: str) -> str: ...
def f1(a: int) -> int:
return a
def f2(a: str) -> str:
return a
def _(flag: bool):
if flag:
f = f1

View file

@ -154,7 +154,7 @@ reveal_type(B() >= A()) # revealed: LeReturnType
class C:
def __gt__(self, other: C) -> EqReturnType:
return 42
return EqReturnType()
def __ge__(self, other: C) -> NeReturnType:
return NeReturnType()

View file

@ -110,7 +110,8 @@ given operator:
```py
class Container:
def __contains__(self, x) -> bool: ...
def __contains__(self, x) -> bool:
return False
class NonContainer: ...
@ -130,7 +131,8 @@ unsupported for the given operator:
```py
class Container:
def __contains__(self, x) -> bool: ...
def __contains__(self, x) -> bool:
return False
class NonContainer: ...

View file

@ -22,14 +22,19 @@ Walking through examples:
from __future__ import annotations
class A:
def __lt__(self, other) -> A: ...
def __gt__(self, other) -> bool: ...
def __lt__(self, other) -> A:
return self
def __gt__(self, other) -> bool:
return False
class B:
def __lt__(self, other) -> B: ...
def __lt__(self, other) -> B:
return self
class C:
def __lt__(self, other) -> C: ...
def __lt__(self, other) -> C:
return self
x = A() < B() < C()
reveal_type(x) # revealed: A & ~AlwaysTruthy | B

View file

@ -197,7 +197,7 @@ class LtReturnTypeOnB: ...
class B:
def __lt__(self, o: B) -> LtReturnTypeOnB:
return set()
return LtReturnTypeOnB()
reveal_type((A(), B()) < (A(), B())) # revealed: LtReturnType | LtReturnTypeOnB | Literal[False]
```

View file

@ -104,7 +104,8 @@ class Iterator:
return 42
class Iterable:
def __iter__(self) -> Iterator: ...
def __iter__(self) -> Iterator:
return Iterator()
# This is fine:
x = [*Iterable()]

View file

@ -99,22 +99,28 @@ The returned value of `__len__` is implicitly and recursively converted to `int`
from typing import Literal
class Zero:
def __len__(self) -> Literal[0]: ...
def __len__(self) -> Literal[0]:
return 0
class ZeroOrOne:
def __len__(self) -> Literal[0, 1]: ...
def __len__(self) -> Literal[0, 1]:
return 0
class ZeroOrTrue:
def __len__(self) -> Literal[0, True]: ...
def __len__(self) -> Literal[0, True]:
return 0
class OneOrFalse:
def __len__(self) -> Literal[1] | Literal[False]: ...
def __len__(self) -> Literal[1] | Literal[False]:
return 1
class OneOrFoo:
def __len__(self) -> Literal[1, "foo"]: ...
def __len__(self) -> Literal[1, "foo"]:
return 1
class ZeroOrStr:
def __len__(self) -> Literal[0] | str: ...
def __len__(self) -> Literal[0] | str:
return 0
reveal_type(len(Zero())) # revealed: Literal[0]
reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1]
@ -134,10 +140,12 @@ reveal_type(len(ZeroOrStr())) # revealed: int
from typing import Literal
class LiteralTrue:
def __len__(self) -> Literal[True]: ...
def __len__(self) -> Literal[True]:
return True
class LiteralFalse:
def __len__(self) -> Literal[False]: ...
def __len__(self) -> Literal[False]:
return False
reveal_type(len(LiteralTrue())) # revealed: Literal[1]
reveal_type(len(LiteralFalse())) # revealed: Literal[0]
@ -157,19 +165,24 @@ class SomeEnum(Enum):
INT_2 = 3_2
class Auto:
def __len__(self) -> Literal[SomeEnum.AUTO]: ...
def __len__(self) -> Literal[SomeEnum.AUTO]:
return SomeEnum.AUTO
class Int:
def __len__(self) -> Literal[SomeEnum.INT]: ...
def __len__(self) -> Literal[SomeEnum.INT]:
return SomeEnum.INT
class Str:
def __len__(self) -> Literal[SomeEnum.STR]: ...
def __len__(self) -> Literal[SomeEnum.STR]:
return SomeEnum.STR
class Tuple:
def __len__(self) -> Literal[SomeEnum.TUPLE]: ...
def __len__(self) -> Literal[SomeEnum.TUPLE]:
return SomeEnum.TUPLE
class IntUnion:
def __len__(self) -> Literal[SomeEnum.INT, SomeEnum.INT_2]: ...
def __len__(self) -> Literal[SomeEnum.INT, SomeEnum.INT_2]:
return SomeEnum.INT
reveal_type(len(Auto())) # revealed: int
reveal_type(len(Int())) # revealed: int
@ -184,7 +197,8 @@ reveal_type(len(IntUnion())) # revealed: int
from typing import Literal
class Negative:
def __len__(self) -> Literal[-1]: ...
def __len__(self) -> Literal[-1]:
return -1
# TODO: Emit a diagnostic
reveal_type(len(Negative())) # revealed: int
@ -196,10 +210,12 @@ reveal_type(len(Negative())) # revealed: int
from typing import Literal
class SecondOptionalArgument:
def __len__(self, v: int = 0) -> Literal[0]: ...
def __len__(self, v: int = 0) -> Literal[0]:
return 0
class SecondRequiredArgument:
def __len__(self, v: int) -> Literal[1]: ...
def __len__(self, v: int) -> Literal[1]:
return 1
# TODO: Emit a diagnostic
reveal_type(len(SecondOptionalArgument())) # revealed: Literal[0]

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
```

View file

@ -35,7 +35,7 @@ Each typevar must also appear _somewhere_ in the parameter list:
```py
def absurd[T]() -> T:
# There's no way to construct a T!
...
raise ValueError("absurd")
```
## Inferring generic function parameter types
@ -48,7 +48,8 @@ whether we want to infer a more specific `Literal` type where possible, or use h
the inferred type to e.g. `int`.
```py
def f[T](x: T) -> T: ...
def f[T](x: T) -> T:
return x
# TODO: no error
# TODO: revealed: int or Literal[1]
@ -77,7 +78,8 @@ The matching up of call arguments and discovery of constraints on typevars can b
process for arbitrarily-nested generic types in parameters.
```py
def f[T](x: list[T]) -> T: ...
def f[T](x: list[T]) -> T:
return x[0]
# TODO: revealed: float
reveal_type(f([1.0, 2.0])) # revealed: T
@ -119,7 +121,7 @@ def different_types[T, S](cond: bool, t: T, s: S) -> T:
if cond:
return t
else:
# TODO: error: S is not assignable to T
# error: [invalid-return-type] "Object of type `S` is not assignable to return type `T`"
return s
def same_types[T](cond: bool, t1: T, t2: T) -> T:

View file

@ -37,8 +37,11 @@ from typing import TypeVar
T = TypeVar("T")
def f1(x: T) -> T: ...
def f2(x: T) -> T: ...
def f1(x: T) -> T:
return x
def f2(x: T) -> T:
return x
f1(1)
f2("a")
@ -53,7 +56,8 @@ This also applies to a single generic function being used multiple times, instan
to a different type each time.
```py
def f[T](x: T) -> T: ...
def f[T](x: T) -> T:
return x
# TODO: no error
# TODO: revealed: int or Literal[1]
@ -72,8 +76,11 @@ reveal_type(f("a")) # revealed: T
```py
class C[T]:
def m1(self, x: T) -> T: ...
def m2(self, x: T) -> T: ...
def m1(self, x: T) -> T:
return x
def m2(self, x: T) -> T:
return x
c: C[int] = C()
# TODO: no error
@ -101,7 +108,8 @@ S = TypeVar("S")
# TODO: no error
# error: [invalid-base]
class Legacy(Generic[T]):
def m(self, x: T, y: S) -> S: ...
def m(self, x: T, y: S) -> S:
return y
legacy: Legacy[int] = Legacy()
# TODO: revealed: str
@ -112,7 +120,8 @@ With PEP 695 syntax, it is clearer that the method uses a separate typevar:
```py
class C[T]:
def m[S](self, x: T, y: S) -> S: ...
def m[S](self, x: T, y: S) -> S:
return y
c: C[int] = C()
# TODO: no errors
@ -147,7 +156,8 @@ class C(Generic[T]):
x: list[S] = []
# This is not an error, as shown in the previous test
def m(self, x: S) -> S: ...
def m(self, x: S) -> S:
return x
```
This is true with PEP 695 syntax, as well, though we must use the legacy syntax to define the
@ -169,8 +179,11 @@ class C[T]:
# TODO: error
x: list[S] = []
def m1(self, x: S) -> S: ...
def m2[S](self, x: S) -> S: ...
def m1(self, x: S) -> S:
return x
def m2[S](self, x: S) -> S:
return x
```
## Nested formal typevars must be distinct

View file

@ -431,7 +431,7 @@ def _(flag: bool):
# invalid signature because it only accepts a `str`,
# but the old-style iteration protocol will pass it an `int`
def __getitem__(self, key: str) -> bytes:
return 42
return bytes()
# error: [not-iterable]
for x in Iterable():
@ -484,7 +484,7 @@ def _(flag1: bool, flag2: bool):
return Iterator()
if flag2:
def __getitem__(self, key: int) -> bytes:
return 42
return bytes()
# error: [not-iterable]
for x in Iterable():
@ -682,7 +682,7 @@ def _(flag: bool):
return "foo"
else:
def __getitem__(self, item: str) -> int:
return "foo"
return 42
# error: [not-iterable]
for x in Iterable1():
@ -723,7 +723,7 @@ def _(flag: bool, flag2: bool):
return "foo"
else:
def __getitem__(self, item: str) -> int:
return "foo"
return 42
if flag2:
def __iter__(self) -> Iterator:
return Iterator()

View file

@ -10,7 +10,8 @@ class Iterator:
return 42
class Iterable:
def __iter__(self) -> Iterator: ...
def __iter__(self) -> Iterator:
return Iterator()
def generator_function():
yield from Iterable()

View file

@ -166,7 +166,8 @@ When a class has an explicit `metaclass` that is not a class, but is a callable
`type.__new__` arguments, we should return the meta-type of its return type.
```py
def f(*args, **kwargs) -> int: ...
def f(*args, **kwargs) -> int:
return 1
class A(metaclass=f): ...

View file

@ -170,7 +170,8 @@ if issubclass(t, int):
def issubclass(c, ci):
return True
def flag() -> bool: ...
def flag() -> bool:
return True
t = int if flag() else str
if issubclass(t, int):
@ -182,7 +183,8 @@ if issubclass(t, int):
```py
issubclass_alias = issubclass
def flag() -> bool: ...
def flag() -> bool:
return True
t = int if flag() else str
if issubclass_alias(t, int):
@ -194,7 +196,8 @@ if issubclass_alias(t, int):
```py
from builtins import issubclass as imported_issubclass
def flag() -> bool: ...
def flag() -> bool:
return True
t = int if flag() else str
if imported_issubclass(t, int):
@ -206,7 +209,8 @@ if imported_issubclass(t, int):
```py
from typing import Any
def flag() -> bool: ...
def flag() -> bool:
return True
t = int if flag() else str
@ -229,7 +233,8 @@ if issubclass(t, Any):
### Do not narrow if there are keyword arguments
```py
def flag() -> bool: ...
def flag() -> bool:
return True
t = int if flag() else str

View file

@ -20,7 +20,8 @@ def _(flag: bool):
## Class patterns
```py
def get_object() -> object: ...
def get_object() -> object:
return object()
class A: ...
class B: ...
@ -42,10 +43,12 @@ reveal_type(x) # revealed: object
## Class pattern with guard
```py
def get_object() -> object: ...
def get_object() -> object:
return object()
class A:
def y() -> int: ...
def y() -> int:
return 1
class B: ...

View file

@ -233,16 +233,20 @@ reveal_type(y) # revealed: A
from typing import Literal
class MetaAmbiguous(type):
def __bool__(self) -> bool: ...
def __bool__(self) -> bool:
return True
class MetaFalsy(type):
def __bool__(self) -> Literal[False]: ...
def __bool__(self) -> Literal[False]:
return False
class MetaTruthy(type):
def __bool__(self) -> Literal[True]: ...
def __bool__(self) -> Literal[True]:
return True
class MetaDeferred(type):
def __bool__(self) -> MetaAmbiguous: ...
def __bool__(self) -> MetaAmbiguous:
return MetaAmbiguous()
class AmbiguousClass(metaclass=MetaAmbiguous): ...
class FalsyClass(metaclass=MetaFalsy): ...
@ -300,20 +304,24 @@ def _(x: bool | str):
reveal_type(x and A()) # revealed: Literal[False] | str & ~AlwaysTruthy | A
class Falsy:
def __bool__(self) -> Literal[False]: ...
def __bool__(self) -> Literal[False]:
return False
class Truthy:
def __bool__(self) -> Literal[True]: ...
def __bool__(self) -> Literal[True]:
return True
def _(x: Falsy | Truthy):
reveal_type(x or A()) # revealed: Truthy | A
reveal_type(x and A()) # revealed: Falsy | A
class MetaFalsy(type):
def __bool__(self) -> Literal[False]: ...
def __bool__(self) -> Literal[False]:
return False
class MetaTruthy(type):
def __bool__(self) -> Literal[True]: ...
def __bool__(self) -> Literal[True]:
return True
class FalsyClass(metaclass=MetaFalsy): ...
class TruthyClass(metaclass=MetaTruthy): ...

View file

@ -9,7 +9,8 @@ is retained after the loop.
## Basic `while` loop
```py
def next_item() -> int | None: ...
def next_item() -> int | None:
return 1
x = next_item()
@ -23,7 +24,8 @@ reveal_type(x) # revealed: None
## `while` loop with `else`
```py
def next_item() -> int | None: ...
def next_item() -> int | None:
return 1
x = next_item()
@ -41,7 +43,8 @@ reveal_type(x) # revealed: None
```py
from typing import Literal
def next_item() -> Literal[1, 2, 3]: ...
def next_item() -> Literal[1, 2, 3]:
raise NotImplementedError
x = next_item()

View file

@ -28,7 +28,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
14 | return "foo"
15 | else:
16 | def __getitem__(self, item: str) -> int:
17 | return "foo"
17 | return 42
18 |
19 | # error: [not-iterable]
20 | for x in Iterable1():

View file

@ -26,7 +26,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
12 | # invalid signature because it only accepts a `str`,
13 | # but the old-style iteration protocol will pass it an `int`
14 | def __getitem__(self, key: str) -> bytes:
15 | return 42
15 | return bytes()
16 |
17 | # error: [not-iterable]
18 | for x in Iterable():

View file

@ -36,7 +36,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
22 | return "foo"
23 | else:
24 | def __getitem__(self, item: str) -> int:
25 | return "foo"
25 | return 42
26 | if flag2:
27 | def __iter__(self) -> Iterator:
28 | return Iterator()

View file

@ -25,7 +25,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
11 | return Iterator()
12 | if flag2:
13 | def __getitem__(self, key: int) -> bytes:
14 | return 42
14 | return bytes()
15 |
16 | # error: [not-iterable]
17 | for x in Iterable():

View file

@ -0,0 +1,96 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: return_type.md - Function return type - Invalid conditional return type
mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def f(cond: bool) -> str:
2 | if cond:
3 | return "a"
4 | else:
5 | # error: [invalid-return-type]
6 | return 1
7 |
8 | def f(cond: bool) -> str:
9 | if cond:
10 | # error: [invalid-return-type]
11 | return 1
12 | else:
13 | # error: [invalid-return-type]
14 | return 2
```
# Diagnostics
```
error: lint:invalid-return-type
--> /src/mdtest_snippet.py:6:16
|
4 | else:
5 | # error: [invalid-return-type]
6 | return 1
| ^ Object of type `Literal[1]` is not assignable to return type `str`
7 |
8 | def f(cond: bool) -> str:
|
::: /src/mdtest_snippet.py:1:22
|
1 | def f(cond: bool) -> str:
| --- info: Return type is declared here as `str`
2 | if cond:
3 | return "a"
|
```
```
error: lint:invalid-return-type
--> /src/mdtest_snippet.py:11:16
|
9 | if cond:
10 | # error: [invalid-return-type]
11 | return 1
| ^ Object of type `Literal[1]` is not assignable to return type `str`
12 | else:
13 | # error: [invalid-return-type]
|
::: /src/mdtest_snippet.py:8:22
|
6 | return 1
7 |
8 | def f(cond: bool) -> str:
| --- info: Return type is declared here as `str`
9 | if cond:
10 | # error: [invalid-return-type]
|
```
```
error: lint:invalid-return-type
--> /src/mdtest_snippet.py:14:16
|
12 | else:
13 | # error: [invalid-return-type]
14 | return 2
| ^ Object of type `Literal[2]` is not assignable to return type `str`
|
::: /src/mdtest_snippet.py:8:22
|
6 | return 1
7 |
8 | def f(cond: bool) -> str:
| --- info: Return type is declared here as `str`
9 | if cond:
10 | # error: [invalid-return-type]
|
```

View file

@ -0,0 +1,100 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: return_type.md - Function return type - Invalid implicit return type
mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def f() -> None:
2 | if False:
3 | # error: [invalid-return-type]
4 | return 1
5 |
6 | # error: [invalid-return-type]
7 | def f(cond: bool) -> int:
8 | if cond:
9 | return 1
10 |
11 | # error: [invalid-return-type]
12 | def f(cond: bool) -> int:
13 | if cond:
14 | raise ValueError()
15 |
16 | # error: [invalid-return-type]
17 | def f(cond: bool) -> int:
18 | if cond:
19 | cond = False
20 | else:
21 | return 1
22 | if cond:
23 | return 2
```
# Diagnostics
```
error: lint:invalid-return-type
--> /src/mdtest_snippet.py:4:16
|
2 | if False:
3 | # error: [invalid-return-type]
4 | return 1
| ^ Object of type `Literal[1]` is not assignable to return type `None`
5 |
6 | # error: [invalid-return-type]
|
::: /src/mdtest_snippet.py:1:12
|
1 | def f() -> None:
| ---- info: Return type is declared here as `None`
2 | if False:
3 | # error: [invalid-return-type]
|
```
```
error: lint:invalid-return-type
--> /src/mdtest_snippet.py:7:22
|
6 | # error: [invalid-return-type]
7 | def f(cond: bool) -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
8 | if cond:
9 | return 1
|
```
```
error: lint:invalid-return-type
--> /src/mdtest_snippet.py:12:22
|
11 | # error: [invalid-return-type]
12 | def f(cond: bool) -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
13 | if cond:
14 | raise ValueError()
|
```
```
error: lint:invalid-return-type
--> /src/mdtest_snippet.py:17:22
|
16 | # error: [invalid-return-type]
17 | def f(cond: bool) -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
18 | if cond:
19 | cond = False
|
```

View file

@ -0,0 +1,93 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: return_type.md - Function return type - Invalid return type
mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | # error: [invalid-return-type]
2 | def f() -> int:
3 | 1
4 |
5 | def f() -> str:
6 | # error: [invalid-return-type]
7 | return 1
8 |
9 | def f() -> int:
10 | # error: [invalid-return-type]
11 | return
12 |
13 | from typing import TypeVar
14 |
15 | T = TypeVar("T")
16 |
17 | # TODO: `invalid-return-type` error should be emitted
18 | def m(x: T) -> T: ...
```
# Diagnostics
```
error: lint:invalid-return-type
--> /src/mdtest_snippet.py:2:12
|
1 | # error: [invalid-return-type]
2 | def f() -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
3 | 1
|
```
```
error: lint:invalid-return-type
--> /src/mdtest_snippet.py:7:12
|
5 | def f() -> str:
6 | # error: [invalid-return-type]
7 | return 1
| ^ Object of type `Literal[1]` is not assignable to return type `str`
8 |
9 | def f() -> int:
|
::: /src/mdtest_snippet.py:5:12
|
3 | 1
4 |
5 | def f() -> str:
| --- info: Return type is declared here as `str`
6 | # error: [invalid-return-type]
7 | return 1
|
```
```
error: lint:invalid-return-type
--> /src/mdtest_snippet.py:11:5
|
9 | def f() -> int:
10 | # error: [invalid-return-type]
11 | return
| ^^^^^^ Object of type `None` is not assignable to return type `int`
12 |
13 | from typing import TypeVar
|
::: /src/mdtest_snippet.py:9:12
|
7 | return 1
8 |
9 | def f() -> int:
| --- info: Return type is declared here as `int`
10 | # error: [invalid-return-type]
11 | return
|
```

View file

@ -0,0 +1,77 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: return_type.md - Function return type - Invalid return type in stub file
mdtest path: crates/red_knot_python_semantic/resources/mdtest/function/return_type.md
---
# Python source files
## mdtest_snippet.pyi
```
1 | def f() -> int:
2 | # error: [invalid-return-type]
3 | return ...
4 |
5 | # error: [invalid-return-type]
6 | def foo() -> int:
7 | print("...")
8 | ...
9 |
10 | # error: [invalid-return-type]
11 | def foo() -> int:
12 | f"""{foo} is a function that ..."""
13 | ...
```
# Diagnostics
```
error: lint:invalid-return-type
--> /src/mdtest_snippet.pyi:3:12
|
1 | def f() -> int:
2 | # error: [invalid-return-type]
3 | return ...
| ^^^ Object of type `ellipsis` is not assignable to return type `int`
4 |
5 | # error: [invalid-return-type]
|
::: /src/mdtest_snippet.pyi:1:12
|
1 | def f() -> int:
| --- info: Return type is declared here as `int`
2 | # error: [invalid-return-type]
3 | return ...
|
```
```
error: lint:invalid-return-type
--> /src/mdtest_snippet.pyi:6:14
|
5 | # error: [invalid-return-type]
6 | def foo() -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
7 | print("...")
8 | ...
|
```
```
error: lint:invalid-return-type
--> /src/mdtest_snippet.pyi:11:14
|
10 | # error: [invalid-return-type]
11 | def foo() -> int:
| ^^^ Function can implicitly return `None`, which is not assignable to return type `int`
12 | f"""{foo} is a function that ..."""
13 | ...
|
```

View file

@ -49,5 +49,5 @@ def _(m: int, n: int):
def _(s: bytes) -> bytes:
byte_slice2 = s[0:5]
# TODO: Support overloads... Should be `bytes`
reveal_type(byte_slice2) # revealed: @Todo(return type of decorated function)
return reveal_type(byte_slice2) # revealed: @Todo(return type of decorated function)
```

View file

@ -13,7 +13,7 @@ a = NotSubscriptable[0] # error: "Cannot subscript object of type `Literal[NotS
```py
class Identity:
def __class_getitem__(cls, item: int) -> str:
return item
return str(item)
reveal_type(Identity[0]) # revealed: str
```
@ -25,7 +25,7 @@ def _(flag: bool):
class UnionClassGetItem:
if flag:
def __class_getitem__(cls, item: int) -> str:
return item
return str(item)
else:
def __class_getitem__(cls, item: int) -> int:
return item
@ -39,7 +39,7 @@ def _(flag: bool):
def _(flag: bool):
class A:
def __class_getitem__(cls, item: int) -> str:
return item
return str(item)
class B:
def __class_getitem__(cls, item: int) -> int:

View file

@ -652,12 +652,12 @@ def f(cond: bool) -> str:
x = "before"
if cond:
reveal_type(x) # revealed: Literal["before"]
return
return "a"
x = "after-return"
# TODO: no unresolved-reference error
# error: [unresolved-reference]
reveal_type(x) # revealed: Unknown
else:
x = "else"
reveal_type(x) # revealed: Literal["else"]
return reveal_type(x) # revealed: Literal["else"]
```

View file

@ -91,8 +91,8 @@ with Manager():
from typing_extensions import Self
class Manager:
def __enter__(self) -> Self: ...
def __enter__(self) -> Self:
return self
__exit__: int = 32
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__exit__`"

View file

@ -894,8 +894,12 @@ where
let pre_return_state = matches!(last_stmt, ast::Stmt::Return(_))
.then(|| builder.flow_snapshot());
builder.visit_stmt(last_stmt);
let scope_start_visibility =
builder.current_use_def_map().scope_start_visibility;
if let Some(pre_return_state) = pre_return_state {
builder.flow_restore(pre_return_state);
builder.current_use_def_map_mut().scope_start_visibility =
scope_start_visibility;
}
}

View file

@ -491,7 +491,7 @@ pub enum NodeWithScopeKind {
}
impl NodeWithScopeKind {
pub(super) const fn scope_kind(&self) -> ScopeKind {
pub(crate) const fn scope_kind(&self) -> ScopeKind {
match self {
Self::Module => ScopeKind::Module,
Self::Class(_) => ScopeKind::Class,

View file

@ -320,6 +320,21 @@ pub(crate) struct UseDefMap<'db> {
/// Snapshot of bindings in this scope that can be used to resolve a reference in a nested
/// eager scope.
eager_bindings: EagerBindings,
/// Whether or not the start of the scope is visible.
/// This is used to check if the function can implicitly return `None`.
/// For example:
///
/// ```python
/// def f(cond: bool) -> int:
/// if cond:
/// return 1
/// ```
///
/// In this case, the function may implicitly return `None`.
///
/// This is used by `UseDefMap::can_implicit_return`.
scope_start_visibility: ScopedVisibilityConstraintId,
}
impl<'db> UseDefMap<'db> {
@ -368,6 +383,14 @@ impl<'db> UseDefMap<'db> {
self.declarations_iterator(declarations)
}
/// This function is intended to be called only once inside `TypeInferenceBuilder::infer_function_body`.
pub(crate) fn can_implicit_return(&self, db: &dyn crate::Db) -> bool {
!self
.visibility_constraints
.evaluate(db, &self.predicates, self.scope_start_visibility)
.is_always_false()
}
fn bindings_iterator<'map>(
&'map self,
bindings: &'map SymbolBindings,
@ -530,7 +553,7 @@ pub(super) struct UseDefMapBuilder<'db> {
/// whether or not the start of the scope is visible. This is important for cases like
/// `if True: x = 1; use(x)` where we need to hide the implicit "x = unbound" binding
/// in the "else" branch.
scope_start_visibility: ScopedVisibilityConstraintId,
pub(super) scope_start_visibility: ScopedVisibilityConstraintId,
/// Live bindings at each so-far-recorded use.
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
@ -784,6 +807,7 @@ impl<'db> UseDefMapBuilder<'db> {
declarations_by_binding: self.declarations_by_binding,
bindings_by_declaration: self.bindings_by_declaration,
eager_bindings: self.eager_bindings,
scope_start_visibility: self.scope_start_visibility,
}
}
}

View file

@ -13,7 +13,7 @@ use ruff_db::diagnostic::{
};
use ruff_db::files::File;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::TextRange;
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet;
use std::borrow::Cow;
use std::fmt::Formatter;
@ -33,6 +33,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INCONSISTENT_MRO);
registry.register_lint(&INDEX_OUT_OF_BOUNDS);
registry.register_lint(&INVALID_ARGUMENT_TYPE);
registry.register_lint(&INVALID_RETURN_TYPE);
registry.register_lint(&INVALID_ASSIGNMENT);
registry.register_lint(&INVALID_BASE);
registry.register_lint(&INVALID_CONTEXT_MANAGER);
@ -261,6 +262,25 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Detects returned values that can't be assigned to the function's annotated return type.
///
/// ## Why is this bad?
/// Returning an object of a type incompatible with the annotated return type may cause confusion to the user calling the function.
///
/// ## Examples
/// ```python
/// def func() -> int:
/// return "a" # error: [invalid-return-type]
/// ```
pub(crate) static INVALID_RETURN_TYPE = {
summary: "detects returned values that can't be assigned to the function's annotated return type",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO #14889
pub(crate) static INVALID_ASSIGNMENT = {
@ -1087,6 +1107,47 @@ pub(super) fn report_invalid_attribute_assignment(
);
}
pub(super) fn report_invalid_return_type(
context: &InferContext,
object_range: impl Ranged,
return_type_range: impl Ranged,
expected_ty: Type,
actual_ty: Type,
) {
let return_type_span = Span::from(context.file()).with_range(return_type_range.range());
context.report_lint_with_secondary_messages(
&INVALID_RETURN_TYPE,
object_range,
format_args!(
"Object of type `{}` is not assignable to return type `{}`",
actual_ty.display(context.db()),
expected_ty.display(context.db())
),
vec![OldSecondaryDiagnosticMessage::new(
return_type_span,
format!(
"Return type is declared here as `{}`",
expected_ty.display(context.db())
),
)],
);
}
pub(super) fn report_implicit_return_type(
context: &InferContext,
range: impl Ranged,
expected_ty: Type,
) {
context.report_lint(
&INVALID_RETURN_TYPE,
range,
format_args!(
"Function can implicitly return `None`, which is not assignable to return type `{}`",
expected_ty.display(context.db())
),
);
}
pub(super) fn report_invalid_type_checking_constant(context: &InferContext, node: AnyNodeRef) {
context.report_lint(
&INVALID_TYPE_CHECKING_CONSTANT,

View file

@ -56,8 +56,9 @@ use crate::symbol::{
};
use crate::types::call::{Argument, CallArguments, UnionCallError};
use crate::types::diagnostic::{
report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
report_invalid_assignment, report_invalid_attribute_assignment, report_unresolved_module,
report_implicit_return_type, report_invalid_arguments_to_annotated,
report_invalid_arguments_to_callable, report_invalid_assignment,
report_invalid_attribute_assignment, report_invalid_return_type, report_unresolved_module,
TypeCheckDiagnostics, CALL_NON_CALLABLE, CALL_POSSIBLY_UNBOUND_METHOD,
CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO,
DUPLICATE_BASE, INCONSISTENT_MRO, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION,
@ -269,6 +270,12 @@ impl<'db> InferenceRegion<'db> {
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
struct TypeAndRange<'db> {
ty: Type<'db>,
range: TextRange,
}
/// The inferred types for a single region.
#[derive(Debug, Eq, PartialEq, salsa::Update)]
pub(crate) struct TypeInference<'db> {
@ -452,6 +459,9 @@ pub(super) struct TypeInferenceBuilder<'db> {
/// The type inference results
types: TypeInference<'db>,
/// The returned types and their corresponding ranges of the region, if it is a function body.
return_types_and_ranges: Vec<TypeAndRange<'db>>,
/// The deferred state of inferring types of certain expressions within the region.
///
/// This is different from [`InferenceRegion::Deferred`] which works on the entire definition
@ -483,6 +493,7 @@ impl<'db> TypeInferenceBuilder<'db> {
context: InferContext::new(db, scope),
index,
region,
return_types_and_ranges: vec![],
deferred_state: DeferredExpressionState::None,
types: TypeInference::empty(scope),
}
@ -1051,6 +1062,11 @@ impl<'db> TypeInferenceBuilder<'db> {
);
}
fn record_return_type(&mut self, ty: Type<'db>, range: TextRange) {
self.return_types_and_ranges
.push(TypeAndRange { ty, range });
}
fn infer_module(&mut self, module: &ast::ModModule) {
self.infer_body(&module.body);
}
@ -1107,6 +1123,68 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_definition(parameter);
}
self.infer_body(&function.body);
if let Some(declared_ty) = function
.returns
.as_deref()
.map(|ret| self.file_expression_type(ret))
{
fn is_stub_suite(suite: &[ast::Stmt]) -> bool {
match suite {
[ast::Stmt::Expr(ast::StmtExpr { value: first, .. }), ast::Stmt::Expr(ast::StmtExpr { value: second, .. }), ..] => {
first.is_string_literal_expr() && second.is_ellipsis_literal_expr()
}
[ast::Stmt::Expr(ast::StmtExpr { value, .. }), ast::Stmt::Pass(_), ..] => {
value.is_string_literal_expr()
}
[ast::Stmt::Expr(ast::StmtExpr { value, .. }), ..] => {
value.is_ellipsis_literal_expr() || value.is_string_literal_expr()
}
[ast::Stmt::Pass(_)] => true,
_ => false,
}
}
let is_overload = function.decorator_list.iter().any(|decorator| {
let decorator_type = self.file_expression_type(&decorator.expression);
decorator_type
.into_function_literal()
.is_some_and(|f| f.is_known(self.db(), KnownFunction::Overload))
});
// TODO: Protocol / abstract methods can have empty bodies
if (self.in_stub() || is_overload)
&& self.return_types_and_ranges.is_empty()
&& is_stub_suite(&function.body)
{
return;
}
for invalid in self
.return_types_and_ranges
.iter()
.filter(|ty_range| !ty_range.ty.is_assignable_to(self.db(), declared_ty))
{
report_invalid_return_type(
&self.context,
invalid.range,
function.returns.as_ref().unwrap().range(),
declared_ty,
invalid.ty,
);
}
let scope_id = self.index.node_scope(NodeWithScopeRef::Function(function));
let use_def = self.index.use_def_map(scope_id);
if use_def.can_implicit_return(self.db())
&& !KnownClass::NoneType
.to_instance(self.db())
.is_assignable_to(self.db(), declared_ty)
{
report_implicit_return_type(
&self.context,
function.returns.as_ref().unwrap().range(),
declared_ty,
);
}
}
}
fn infer_body(&mut self, suite: &[ast::Stmt]) {
@ -2743,7 +2821,15 @@ impl<'db> TypeInferenceBuilder<'db> {
}
fn infer_return_statement(&mut self, ret: &ast::StmtReturn) {
self.infer_optional_expression(ret.value.as_deref());
if let Some(ty) = self.infer_optional_expression(ret.value.as_deref()) {
let range = ret
.value
.as_ref()
.map_or(ret.range(), |value| value.range());
self.record_return_type(ty, range);
} else {
self.record_return_type(KnownClass::NoneType.to_instance(self.db()), ret.range());
}
}
fn infer_delete_statement(&mut self, delete: &ast::StmtDelete) {

View file

@ -441,6 +441,16 @@
}
]
},
"invalid-return-type": {
"title": "detects returned values that can't be assigned to the function's annotated return type",
"description": "## What it does\nDetects returned values that can't be assigned to the function's annotated return type.\n\n## Why is this bad?\nReturning an object of a type incompatible with the annotated return type may cause confusion to the user calling the function.\n\n## Examples\n```python\ndef func() -> int:\n return \"a\" # error: [invalid-return-type]\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"invalid-syntax-in-forward-annotation": {
"title": "detects invalid syntax in forward annotations",
"description": "TODO #14889",