mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 14:52:01 +00:00
[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:
parent
e17cd350b6
commit
78b5f0b165
43 changed files with 983 additions and 103 deletions
|
@ -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
|
||||
"#,
|
||||
),
|
||||
(
|
||||
|
|
|
@ -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: ...
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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: ...
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -37,6 +37,8 @@ def foo() -> int:
|
|||
return 42
|
||||
|
||||
def decorator(func) -> Callable[[], int]:
|
||||
# TODO: no error
|
||||
# error: [invalid-return-type]
|
||||
return foo
|
||||
|
||||
@decorator
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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: ...
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
```
|
||||
|
|
|
@ -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()]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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): ...
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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: ...
|
||||
|
||||
|
|
|
@ -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): ...
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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]
|
||||
|
|
||||
|
||||
```
|
|
@ -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
|
||||
|
|
||||
|
||||
```
|
|
@ -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
|
||||
|
|
||||
|
||||
```
|
|
@ -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 | ...
|
||||
|
|
||||
|
||||
```
|
|
@ -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)
|
||||
```
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"]
|
||||
```
|
||||
|
|
|
@ -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__`"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue