diff --git a/crates/red_knot/tests/cli.rs b/crates/red_knot/tests/cli.rs index 09ce6ab6bc..7369654de9 100644 --- a/crates/red_knot/tests/cli.rs +++ b/crates/red_knot/tests/cli.rs @@ -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 "#, ), ( diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md index c12edcec1a..230a67f426 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md @@ -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: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index 3755f93d21..bb263d3a27 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -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: diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index 596f35114b..5c701b22a6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -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: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/boundness_declaredness/public.md b/crates/red_knot_python_semantic/resources/mdtest/boundness_declaredness/public.md index d9d42f7d0a..3c41c72e44 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/boundness_declaredness/public.md +++ b/crates/red_knot_python_semantic/resources/mdtest/boundness_declaredness/public.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md b/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md index 4e5f96fb1b..5b6bb36879 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md @@ -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" diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/dunder.md b/crates/red_knot_python_semantic/resources/mdtest/call/dunder.md index b4288752cc..7f8cb0d564 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/dunder.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/dunder.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/function.md b/crates/red_knot_python_semantic/resources/mdtest/call/function.md index e0cd44e5d2..d31a7f58aa 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/function.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/function.md @@ -37,6 +37,8 @@ def foo() -> int: return 42 def decorator(func) -> Callable[[], int]: + # TODO: no error + # error: [invalid-return-type] return foo @decorator diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/union.md b/crates/red_knot_python_semantic/resources/mdtest/call/union.md index d2ae8875a8..31240b0c1d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/union.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/union.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md index 8cac539fac..a0c6680c61 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md @@ -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() diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md index 20c8c914ac..6bc04f7b6f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md @@ -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: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md index 0702c2de54..6cf77e5f1e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md index ff3f684a5d..557791790d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md @@ -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] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comprehensions/basic.md b/crates/red_knot_python_semantic/resources/mdtest/comprehensions/basic.md index 5236b27cce..8047fc078d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comprehensions/basic.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comprehensions/basic.md @@ -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()] diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/len.md b/crates/red_knot_python_semantic/resources/mdtest/expression/len.md index 19d965c99b..f3c1938efc 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/len.md +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/len.md @@ -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] diff --git a/crates/red_knot_python_semantic/resources/mdtest/function/return_type.md b/crates/red_knot_python_semantic/resources/mdtest/function/return_type.md new file mode 100644 index 0000000000..c8d6f7dffc --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/function/return_type.md @@ -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 + + + +```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 + + + +```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 + + + +```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 + + + +```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 +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md index e2499cff07..712152ba3e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md @@ -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: diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md index d3f29077d8..14342a838f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/scoping.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/for.md b/crates/red_knot_python_semantic/resources/mdtest/loops/for.md index 60b7e47074..bc18230147 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/for.md +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/for.md @@ -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() diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/iterators.md b/crates/red_knot_python_semantic/resources/mdtest/loops/iterators.md index 8ec99c9e40..5b8c009fe3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/iterators.md +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/iterators.md @@ -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() diff --git a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md index ed65589093..33cd463848 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md +++ b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md @@ -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): ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md index e874d606d2..3ad543361d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md index c13b7dd4f5..1ff85d4bb8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md @@ -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: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md index 5ad241beaa..057c4d706b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md @@ -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): ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/while.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/while.md index ac91a576c9..af8141b646 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/while.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/while.md @@ -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() diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap index 98baf3ea35..6298cdd024 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap @@ -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(): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap index 0a26d07d6f..65063fd7fe 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap @@ -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(): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap index 1341e46b3f..95953b915a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap @@ -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() diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap index 0bcd214cf4..11cf64a294 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap @@ -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(): diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap new file mode 100644 index 0000000000..6835731476 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_conditional_return_type.snap @@ -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] + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap new file mode 100644 index 0000000000..a2f20ea75d --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_implicit_return_type.snap @@ -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 + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap new file mode 100644 index 0000000000..e8b03d5c76 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type.snap @@ -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 + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap new file mode 100644 index 0000000000..3c7340e526 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/return_type.md_-_Function_return_type_-_Invalid_return_type_in_stub_file.snap @@ -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 | ... + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md index 73cc378a05..7371c10562 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md @@ -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) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md index 2903153b07..cbe37b302b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md @@ -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: diff --git a/crates/red_knot_python_semantic/resources/mdtest/terminal_statements.md b/crates/red_knot_python_semantic/resources/mdtest/terminal_statements.md index 9918d8082e..b757b0d820 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/terminal_statements.md +++ b/crates/red_knot_python_semantic/resources/mdtest/terminal_statements.md @@ -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"] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/with/sync.md b/crates/red_knot_python_semantic/resources/mdtest/with/sync.md index 2a98cbe19c..b180d6f55c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/with/sync.md +++ b/crates/red_knot_python_semantic/resources/mdtest/with/sync.md @@ -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__`" diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index 2174cb207c..f113b39505 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -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; } } diff --git a/crates/red_knot_python_semantic/src/semantic_index/symbol.rs b/crates/red_knot_python_semantic/src/semantic_index/symbol.rs index 21c069b198..9604de384d 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/symbol.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/symbol.rs @@ -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, diff --git a/crates/red_knot_python_semantic/src/semantic_index/use_def.rs b/crates/red_knot_python_semantic/src/semantic_index/use_def.rs index e882e5d4b5..dc6558e65e 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/use_def.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/use_def.rs @@ -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, @@ -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, } } } diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index 8a88541d2a..2aa3f5777c 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -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, diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 1bdbc43806..bb28029c21 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -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>, + /// 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) { diff --git a/knot.schema.json b/knot.schema.json index c0d82b5c68..43591dce0b 100644 --- a/knot.schema.json +++ b/knot.schema.json @@ -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",