diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md b/crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md index 27113ed827..980dd4b510 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md @@ -3,9 +3,12 @@ ## Unbound ```py -x = foo +x = foo # error: [unresolved-reference] "Name `foo` used when not defined" foo = 1 -reveal_type(x) # revealed: Unbound + +# error: [unresolved-reference] +# revealed: Unbound +reveal_type(x) ``` ## Unbound class variable @@ -13,6 +16,10 @@ reveal_type(x) # revealed: Unbound Name lookups within a class scope fall back to globals, but lookups of class attributes don't. ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() x = 1 class C: diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index 5a9add9174..3278a7e48a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -3,6 +3,11 @@ ## Union of attributes ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + if flag: class C: x = 1 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 7f98fb204e..911f21947e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/union.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/union.md @@ -3,6 +3,11 @@ ## Union of return types ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + if flag: def f() -> int: @@ -21,6 +26,11 @@ reveal_type(f()) # revealed: int | str ```py from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`" +def bool_instance() -> bool: + return True + +flag = bool_instance() + if flag: def f() -> int: @@ -34,6 +44,11 @@ reveal_type(f()) # revealed: Unknown | int Calling a union with a non-callable element should emit a diagnostic. ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + if flag: f = 1 else: @@ -50,6 +65,11 @@ reveal_type(x) # revealed: Unknown | int Calling a union with multiple non-callable elements should mention all of them in the diagnostic. ```py +def bool_instance() -> bool: + return True + +flag, flag2 = bool_instance(), bool_instance() + if flag: f = 1 elif flag2: @@ -69,6 +89,11 @@ reveal_type(f()) Calling a union with no callable elements can emit a simpler diagnostic. ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + if flag: f = 1 else: diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/unions.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/unions.md index 9686196bc0..2a07827738 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/unions.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/unions.md @@ -5,6 +5,10 @@ Comparisons on union types need to consider all possible cases: ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() one_or_two = 1 if flag else 2 reveal_type(one_or_two <= 2) # revealed: Literal[True] @@ -52,6 +56,10 @@ With unions on both sides, we need to consider the full cross product of options when building the resulting (union) type: ```py +def bool_instance() -> bool: + return True + +flag_s, flag_l = bool_instance(), bool_instance() small = 1 if flag_s else 2 large = 2 if flag_l else 3 @@ -69,6 +77,10 @@ unsupported. For now, we fall back to `bool` for the result type instead of trying to infer something more precise from the other (supported) variants: ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() x = [1, 2] if flag else 1 result = 1 in x # error: "Operator `in` is not supported" diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md index cb5fab1840..f073a19dc5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md @@ -1,6 +1,9 @@ # Comparison: Unsupported operators ```py +def bool_instance() -> bool: + return True + a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`" reveal_type(a) # revealed: bool @@ -15,6 +18,7 @@ d = 5 < object() # TODO: should be `Unknown` reveal_type(d) # revealed: bool +flag = bool_instance() int_literal_or_str_literal = 1 if flag else "foo" # error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1] | Literal["foo"]`" e = 42 in int_literal_or_str_literal diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md index c9028c7859..f162a3a39d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md @@ -3,6 +3,10 @@ ## Simple if-expression ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() x = 1 if flag else 2 reveal_type(x) # revealed: Literal[1, 2] ``` @@ -10,6 +14,10 @@ reveal_type(x) # revealed: Literal[1, 2] ## If-expression with walrus operator ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() y = 0 z = 0 x = (y := 1) if flag else (z := 2) @@ -21,6 +29,10 @@ reveal_type(z) # revealed: Literal[0, 2] ## Nested if-expression ```py +def bool_instance() -> bool: + return True + +flag, flag2 = bool_instance(), bool_instance() x = 1 if flag else 2 if flag2 else 3 reveal_type(x) # revealed: Literal[1, 2, 3] ``` @@ -28,6 +40,10 @@ reveal_type(x) # revealed: Literal[1, 2, 3] ## None ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() x = 1 if flag else None reveal_type(x) # revealed: Literal[1] | None ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md index 73e73f4506..a44a58b4ca 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md @@ -3,6 +3,10 @@ ## Simple if ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() y = 1 y = 2 @@ -15,6 +19,10 @@ reveal_type(y) # revealed: Literal[2, 3] ## Simple if-elif-else ```py +def bool_instance() -> bool: + return True + +flag, flag2 = bool_instance(), bool_instance() y = 1 y = 2 if flag: @@ -28,13 +36,24 @@ else: x = y reveal_type(x) # revealed: Literal[3, 4, 5] -reveal_type(r) # revealed: Unbound | Literal[2] -reveal_type(s) # revealed: Unbound | Literal[5] + +# revealed: Unbound | Literal[2] +# error: [possibly-unresolved-reference] +reveal_type(r) + +# revealed: Unbound | Literal[5] +# error: [possibly-unresolved-reference] +reveal_type(s) ``` ## Single symbol across if-elif-else ```py +def bool_instance() -> bool: + return True + +flag, flag2 = bool_instance(), bool_instance() + if flag: y = 1 elif flag2: @@ -47,6 +66,10 @@ reveal_type(y) # revealed: Literal[1, 2, 3] ## if-elif-else without else assignment ```py +def bool_instance() -> bool: + return True + +flag, flag2 = bool_instance(), bool_instance() y = 0 if flag: y = 1 @@ -60,6 +83,10 @@ reveal_type(y) # revealed: Literal[0, 1, 2] ## if-elif-else with intervening assignment ```py +def bool_instance() -> bool: + return True + +flag, flag2 = bool_instance(), bool_instance() y = 0 if flag: y = 1 @@ -74,6 +101,10 @@ reveal_type(y) # revealed: Literal[0, 1, 2] ## Nested if statement ```py +def bool_instance() -> bool: + return True + +flag, flag2 = bool_instance(), bool_instance() y = 0 if flag: if flag2: @@ -84,6 +115,10 @@ reveal_type(y) # revealed: Literal[0, 1] ## if-elif without else ```py +def bool_instance() -> bool: + return True + +flag, flag2 = bool_instance(), bool_instance() y = 1 y = 2 if flag: diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md b/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md index 2ce7c9462d..5269bc8b89 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md @@ -21,7 +21,9 @@ match 0: case 2: y = 3 -reveal_type(y) # revealed: Unbound | Literal[2, 3] +# revealed: Unbound | Literal[2, 3] +# error: [possibly-unresolved-reference] +reveal_type(y) ``` ## Basic match diff --git a/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md b/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md index b1eded0113..19fbfa20a5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md +++ b/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md @@ -10,6 +10,10 @@ x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred t ## Incompatible declarations ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() if flag: x: str else: @@ -20,6 +24,10 @@ x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: ## Partial declarations ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() if flag: x: int x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int" @@ -28,6 +36,10 @@ x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: ## Incompatible declarations with bad assignment ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() if flag: x: str else: diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md b/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md index 85bc87b7fd..6b67e6a221 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md +++ b/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md @@ -6,7 +6,7 @@ import re try: - x + help() except NameError as e: reveal_type(e) # revealed: NameError except re.error as f: @@ -19,7 +19,7 @@ except re.error as f: from nonexistent_module import foo # error: [unresolved-import] try: - x + help() except foo as e: reveal_type(foo) # revealed: Unknown reveal_type(e) # revealed: Unknown @@ -31,7 +31,7 @@ except foo as e: EXCEPTIONS = (AttributeError, TypeError) try: - x + help() except (RuntimeError, OSError) as e: reveal_type(e) # revealed: RuntimeError | OSError except EXCEPTIONS as f: @@ -43,7 +43,7 @@ except EXCEPTIONS as f: ```py def foo(x: type[AttributeError], y: tuple[type[OSError], type[RuntimeError]], z: tuple[type[BaseException], ...]): try: - w + help() except x as e: # TODO: should be `AttributeError` reveal_type(e) # revealed: @Todo diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md b/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md index 44d701d1f8..b543544e56 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md +++ b/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md @@ -4,7 +4,7 @@ ```py try: - x + help() except* BaseException as e: reveal_type(e) # revealed: BaseExceptionGroup ``` @@ -13,7 +13,7 @@ except* BaseException as e: ```py try: - x + help() except* OSError as e: # TODO(Alex): more precise would be `ExceptionGroup[OSError]` reveal_type(e) # revealed: BaseExceptionGroup @@ -23,7 +23,7 @@ except* OSError as e: ```py try: - x + help() except* (TypeError, AttributeError) as e: # TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`. reveal_type(e) # revealed: BaseExceptionGroup diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics.md b/crates/red_knot_python_semantic/resources/mdtest/generics.md index 2bab6f4e9e..03660b58b6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics.md @@ -6,9 +6,13 @@ Basic PEP 695 generics ```py class MyBox[T]: + # TODO: `T` is defined here + # error: [unresolved-reference] "Name `T` used when not defined" data: T box_model_number = 695 + # TODO: `T` is defined here + # error: [unresolved-reference] "Name `T` used when not defined" def __init__(self, data: T): self.data = data @@ -26,13 +30,19 @@ reveal_type(MyBox.box_model_number) # revealed: Literal[695] ```py class MyBox[T]: + # TODO: `T` is defined here + # error: [unresolved-reference] "Name `T` used when not defined" data: T + # TODO: `T` is defined here + # error: [unresolved-reference] "Name `T` used when not defined" def __init__(self, data: T): self.data = data -# TODO not error on the subscripting -class MySecureBox[T](MyBox[T]): ... # error: [non-subscriptable] +# TODO not error on the subscripting or the use of type param +# error: [unresolved-reference] "Name `T` used when not defined" +# error: [non-subscriptable] +class MySecureBox[T](MyBox[T]): ... secure_box: MySecureBox[int] = MySecureBox(5) reveal_type(secure_box) # revealed: MySecureBox diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/conditional.md b/crates/red_knot_python_semantic/resources/mdtest/import/conditional.md index 9fd0e09bef..76756cb527 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/conditional.md +++ b/crates/red_knot_python_semantic/resources/mdtest/import/conditional.md @@ -3,11 +3,22 @@ ## Maybe unbound ```py path=maybe_unbound.py +def bool_instance() -> bool: + return True + +flag = bool_instance() if flag: y = 3 -x = y -reveal_type(x) # revealed: Unbound | Literal[3] -reveal_type(y) # revealed: Unbound | Literal[3] + +x = y # error: [possibly-unresolved-reference] + +# revealed: Unbound | Literal[3] +# error: [possibly-unresolved-reference] +reveal_type(x) + +# revealed: Unbound | Literal[3] +# error: [possibly-unresolved-reference] +reveal_type(y) ``` ```py @@ -20,11 +31,22 @@ reveal_type(y) # revealed: Literal[3] ## Maybe unbound annotated ```py path=maybe_unbound_annotated.py +def bool_instance() -> bool: + return True + +flag = bool_instance() + if flag: y: int = 3 -x = y -reveal_type(x) # revealed: Unbound | Literal[3] -reveal_type(y) # revealed: Unbound | Literal[3] +x = y # error: [possibly-unresolved-reference] + +# revealed: Unbound | Literal[3] +# error: [possibly-unresolved-reference] +reveal_type(x) + +# revealed: Unbound | Literal[3] +# error: [possibly-unresolved-reference] +reveal_type(y) ``` Importing an annotated name prefers the declared type over the inferred type: @@ -43,6 +65,10 @@ def f(): ... ``` ```py path=b.py +def bool_instance() -> bool: + return True + +flag = bool_instance() if flag: from c import f else: @@ -67,6 +93,10 @@ x: int ``` ```py path=b.py +def bool_instance() -> bool: + return True + +flag = bool_instance() if flag: from c import x else: diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/relative.md b/crates/red_knot_python_semantic/resources/mdtest/import/relative.md index be9a3168b1..4695374e94 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/import/relative.md +++ b/crates/red_knot_python_semantic/resources/mdtest/import/relative.md @@ -102,7 +102,7 @@ reveal_type(X) # revealed: Literal[42] ``` ```py path=package/foo.py -x +x # error: [unresolved-reference] ``` ```py path=package/bar.py diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/async_for_loops.md b/crates/red_knot_python_semantic/resources/mdtest/loops/async_for_loops.md index ba6c471312..a1a55f81d3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/async_for_loops.md +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/async_for_loops.md @@ -17,8 +17,10 @@ async def foo(): async for x in Iterator(): pass - # TODO - reveal_type(x) # revealed: Unbound | @Todo + # TODO: should reveal `Unbound | Unknown` because `__aiter__` is not defined + # revealed: Unbound | @Todo + # error: [possibly-unresolved-reference] + reveal_type(x) ``` ## Basic async for loop @@ -37,5 +39,7 @@ async def foo(): async for x in IntAsyncIterable(): pass - reveal_type(x) # revealed: Unbound | @Todo + # error: [possibly-unresolved-reference] + # revealed: Unbound | @Todo + reveal_type(x) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/for_loop.md b/crates/red_knot_python_semantic/resources/mdtest/loops/for_loop.md index c431dff6f5..117108928f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/for_loop.md +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/for_loop.md @@ -14,7 +14,9 @@ class IntIterable: for x in IntIterable(): pass -reveal_type(x) # revealed: Unbound | int +# revealed: Unbound | int +# error: [possibly-unresolved-reference] +reveal_type(x) ``` ## With previous definition @@ -85,7 +87,9 @@ class OldStyleIterable: for x in OldStyleIterable(): pass -reveal_type(x) # revealed: Unbound | int +# revealed: Unbound | int +# error: [possibly-unresolved-reference] +reveal_type(x) ``` ## With heterogeneous tuple @@ -94,12 +98,19 @@ reveal_type(x) # revealed: Unbound | int for x in (1, "a", b"foo"): pass -reveal_type(x) # revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"] +# revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"] +# error: [possibly-unresolved-reference] +reveal_type(x) ``` ## With non-callable iterator ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + class NotIterable: if flag: __iter__ = 1 @@ -109,7 +120,9 @@ class NotIterable: for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable" pass -reveal_type(x) # revealed: Unbound | Unknown +# revealed: Unbound | Unknown +# error: [possibly-unresolved-reference] +reveal_type(x) ``` ## Invalid iterable diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md b/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md index daba14ac98..51a123edc2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md @@ -3,6 +3,10 @@ ## Basic While Loop ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() x = 1 while flag: x = 2 @@ -13,6 +17,10 @@ reveal_type(x) # revealed: Literal[1, 2] ## While with else (no break) ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() x = 1 while flag: x = 2 @@ -26,6 +34,10 @@ reveal_type(x) # revealed: Literal[3] ## While with Else (may break) ```py +def bool_instance() -> bool: + return True + +flag, flag2 = bool_instance(), bool_instance() x = 1 y = 0 while flag: diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is.md index 550941ba70..8c042c75d0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is.md @@ -3,6 +3,10 @@ ## `is None` ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() x = None if flag else 1 if x is None: @@ -14,6 +18,11 @@ reveal_type(x) # revealed: None | Literal[1] ## `is` for other types ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + class A: ... x = A() @@ -28,6 +37,10 @@ reveal_type(y) # revealed: A | None ## `is` in chained comparisons ```py +def bool_instance() -> bool: + return True + +x_flag, y_flag = bool_instance(), bool_instance() x = True if x_flag else False y = True if y_flag else False diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is_not.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is_not.md index 9495830883..f7032d02af 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is_not.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_is_not.md @@ -5,6 +5,10 @@ The type guard removes `None` from the union type: ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() x = None if flag else 1 if x is not None: @@ -16,6 +20,10 @@ reveal_type(x) # revealed: None | Literal[1] ## `is not` for other singleton types ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() x = True if flag else False reveal_type(x) # revealed: bool @@ -42,6 +50,10 @@ if x is not y: The type guard removes `False` from the union type of the tested value only. ```py +def bool_instance() -> bool: + return True + +x_flag, y_flag = bool_instance(), bool_instance() x = True if x_flag else False y = True if y_flag else False diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_nested.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_nested.md index bbbb2eb881..46492f8cca 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_nested.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_nested.md @@ -16,6 +16,10 @@ if x != 1: ## Multiple negative contributions with simplification ```py +def bool_instance() -> bool: + return True + +flag1, flag2 = bool_instance(), bool_instance() x = 1 if flag1 else 2 if flag2 else 3 if x != 1: diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_not_eq.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_not_eq.md index 621b863283..d0af94e9ea 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_not_eq.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals_not_eq.md @@ -3,6 +3,10 @@ ## `x != None` ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() x = None if flag else 1 if x != None: @@ -12,6 +16,10 @@ if x != None: ## `!=` for other singleton types ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() x = True if flag else False if x != False: @@ -21,6 +29,10 @@ if x != False: ## `x != y` where `y` is of literal type ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() x = 1 if flag else 2 if x != 1: @@ -30,6 +42,11 @@ if x != 1: ## `x != y` where `y` is a single-valued type ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + class A: ... class B: ... @@ -44,8 +61,13 @@ if C != A: Only single-valued types should narrow the type: ```py -def int_instance() -> int: ... +def bool_instance() -> bool: + return True +def int_instance() -> int: + return 42 + +flag = bool_instance() x = int_instance() if flag else None y = int_instance() diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md index bcc3e452b4..5b5d984107 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/isinstance.md @@ -5,6 +5,11 @@ Narrowing for `isinstance(object, classinfo)` expressions. ## `classinfo` is a single type ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + x = 1 if flag else "a" if isinstance(x, int): @@ -26,6 +31,11 @@ Note: `isinstance(x, (int, str))` should not be confused with `isinstance(x, int | str)`: ```py +def bool_instance() -> bool: + return True + +flag, flag1, flag2 = bool_instance(), bool_instance(), bool_instance() + x = 1 if flag else "a" if isinstance(x, (int, str)): @@ -56,6 +66,11 @@ if isinstance(y, (str, bytes)): ## `classinfo` is a nested tuple of types ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + x = 1 if flag else "a" if isinstance(x, (bool, (bytes, int))): @@ -81,6 +96,11 @@ if isinstance(x, A): ## No narrowing for instances of `builtins.type` ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + t = type("t", (), {}) # This isn't testing what we want it to test if we infer anything more precise here: @@ -94,6 +114,11 @@ if isinstance(x, t): ## Do not use custom `isinstance` for narrowing ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + def isinstance(x, t): return True @@ -105,6 +130,11 @@ if isinstance(x, int): ## Do support narrowing if `isinstance` is aliased ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + isinstance_alias = isinstance x = 1 if flag else "a" @@ -117,6 +147,10 @@ if isinstance_alias(x, int): ```py from builtins import isinstance as imported_isinstance +def bool_instance() -> bool: + return True + +flag = bool_instance() x = 1 if flag else "a" if imported_isinstance(x, int): reveal_type(x) # revealed: Literal[1] @@ -125,6 +159,10 @@ if imported_isinstance(x, int): ## Do not narrow if second argument is not a type ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() x = 1 if flag else "a" # TODO: this should cause us to emit a diagnostic during @@ -141,6 +179,10 @@ if isinstance(x, "int"): ## Do not narrow if there are keyword arguments ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() x = 1 if flag else "a" # TODO: this should cause us to emit a diagnostic 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 0c8ea0e363..2144f91f9e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/match.md @@ -3,6 +3,11 @@ ## Single `match` pattern ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + x = None if flag else 1 reveal_type(x) # revealed: None | Literal[1] diff --git a/crates/red_knot_python_semantic/resources/mdtest/shadowing/variable_declaration.md b/crates/red_knot_python_semantic/resources/mdtest/shadowing/variable_declaration.md index ebce13cced..44a93668c9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/shadowing/variable_declaration.md +++ b/crates/red_knot_python_semantic/resources/mdtest/shadowing/variable_declaration.md @@ -3,6 +3,11 @@ ## Shadow after incompatible declarations is OK ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + if flag: x: str else: diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/not.md b/crates/red_knot_python_semantic/resources/mdtest/unary/not.md index f02dbcd2a3..bb9f6ccac7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/unary/not.md +++ b/crates/red_knot_python_semantic/resources/mdtest/unary/not.md @@ -37,6 +37,11 @@ y = 1 ## Union ```py +def bool_instance() -> bool: + return True + +flag = bool_instance() + if flag: p = 1 q = 3.3 diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index d2d307b7b4..7f932827f1 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -320,9 +320,7 @@ impl<'db> TypeInferenceBuilder<'db> { db, index, region, - file, - types: TypeInference::empty(scope), } } @@ -2418,7 +2416,23 @@ impl<'db> TypeInferenceBuilder<'db> { None }; - bindings_ty(self.db, definitions, unbound_ty) + let ty = bindings_ty(self.db, definitions, unbound_ty); + + if ty.is_unbound() { + self.add_diagnostic( + name.into(), + "unresolved-reference", + format_args!("Name `{id}` used when not defined"), + ); + } else if ty.may_be_unbound(self.db) { + self.add_diagnostic( + name.into(), + "possibly-unresolved-reference", + format_args!("Name `{id}` used when possibly not defined"), + ); + } + + ty } ExprContext::Store | ExprContext::Del => Type::None, ExprContext::Invalid => Type::Unknown, @@ -3808,6 +3822,7 @@ mod tests { Ok(db) } + #[track_caller] fn assert_public_ty(db: &TestDb, file_name: &str, symbol_name: &str, expected: &str) { let file = system_path_to_file(db, file_name).expect("file to exist"); @@ -3819,6 +3834,7 @@ mod tests { ); } + #[track_caller] fn assert_scope_ty( db: &TestDb, file_name: &str, @@ -3844,6 +3860,7 @@ mod tests { assert_eq!(ty.display(db).to_string(), expected); } + #[track_caller] fn assert_diagnostic_messages(diagnostics: &TypeCheckDiagnostics, expected: &[&str]) { let messages: Vec<&str> = diagnostics .iter() @@ -3852,6 +3869,7 @@ mod tests { assert_eq!(&messages, expected); } + #[track_caller] fn assert_file_diagnostics(db: &TestDb, filename: &str, expected: &[&str]) { let file = system_path_to_file(db, filename).unwrap(); let diagnostics = check_types(db, file); @@ -4439,7 +4457,7 @@ mod tests { from typing_extensions import reveal_type try: - x + print except as e: reveal_type(e) ", @@ -4576,7 +4594,10 @@ mod tests { assert_file_diagnostics( &db, "src/a.py", - &["Object of type `Unbound` is not iterable"], + &[ + "Name `x` used when not defined", + "Object of type `Unbound` is not iterable", + ], ); Ok(()) @@ -4711,7 +4732,7 @@ mod tests { assert_scope_ty(&db, "src/a.py", &["foo", ""], "z", "Unbound"); // (There is a diagnostic for invalid syntax that's emitted, but it's not listed by `assert_file_diagnostics`) - assert_file_diagnostics(&db, "src/a.py", &[]); + assert_file_diagnostics(&db, "src/a.py", &["Name `z` used when not defined"]); Ok(()) } diff --git a/crates/red_knot_workspace/src/lib.rs b/crates/red_knot_workspace/src/lib.rs index f0b3f62a98..495a130115 100644 --- a/crates/red_knot_workspace/src/lib.rs +++ b/crates/red_knot_workspace/src/lib.rs @@ -1,4 +1,3 @@ pub mod db; -pub mod lint; pub mod watch; pub mod workspace; diff --git a/crates/red_knot_workspace/src/lint.rs b/crates/red_knot_workspace/src/lint.rs deleted file mode 100644 index dbc7ea7289..0000000000 --- a/crates/red_knot_workspace/src/lint.rs +++ /dev/null @@ -1,318 +0,0 @@ -use std::cell::RefCell; -use std::time::Duration; - -use tracing::debug_span; - -use red_knot_python_semantic::types::Type; -use red_knot_python_semantic::{HasTy, ModuleName, SemanticModel}; -use ruff_db::files::File; -use ruff_db::parsed::{parsed_module, ParsedModule}; -use ruff_db::source::{source_text, SourceText}; -use ruff_python_ast as ast; -use ruff_python_ast::visitor::{walk_expr, walk_stmt, Visitor}; -use ruff_text_size::{Ranged, TextSize}; - -use crate::db::Db; - -/// Workaround query to test for if the computation should be cancelled. -/// Ideally, push for Salsa to expose an API for testing if cancellation was requested. -#[salsa::tracked] -#[allow(unused_variables)] -pub(crate) fn unwind_if_cancelled(db: &dyn Db) {} - -#[salsa::tracked(return_ref)] -pub(crate) fn lint_syntax(db: &dyn Db, file_id: File) -> Vec { - #[allow(clippy::print_stdout)] - if std::env::var("RED_KNOT_SLOW_LINT").is_ok() { - for i in 0..10 { - unwind_if_cancelled(db); - - println!("RED_KNOT_SLOW_LINT is set, sleeping for {i}/10 seconds"); - std::thread::sleep(Duration::from_secs(1)); - } - } - - let mut diagnostics = Vec::new(); - - let source = source_text(db.upcast(), file_id); - lint_lines(&source, &mut diagnostics); - - let parsed = parsed_module(db.upcast(), file_id); - - if parsed.errors().is_empty() { - let ast = parsed.syntax(); - - let mut visitor = SyntaxLintVisitor { - diagnostics, - source: &source, - }; - visitor.visit_body(&ast.body); - diagnostics = visitor.diagnostics; - } - - diagnostics -} - -fn lint_lines(source: &str, diagnostics: &mut Vec) { - for (line_number, line) in source.lines().enumerate() { - if line.len() < 88 { - continue; - } - - let char_count = line.chars().count(); - if char_count > 88 { - diagnostics.push(format!( - "Line {} is too long ({} characters)", - line_number + 1, - char_count - )); - } - } -} - -#[allow(unreachable_pub)] -#[salsa::tracked(return_ref)] -pub fn lint_semantic(db: &dyn Db, file_id: File) -> Vec { - let _span = debug_span!("lint_semantic", file=%file_id.path(db)).entered(); - - let source = source_text(db.upcast(), file_id); - let parsed = parsed_module(db.upcast(), file_id); - let semantic = SemanticModel::new(db.upcast(), file_id); - - if !parsed.is_valid() { - return vec![]; - } - - let context = SemanticLintContext { - source, - parsed, - semantic, - diagnostics: RefCell::new(Vec::new()), - }; - - SemanticVisitor { context: &context }.visit_body(parsed.suite()); - - context.diagnostics.take() -} - -fn format_diagnostic(context: &SemanticLintContext, message: &str, start: TextSize) -> String { - let source_location = context - .semantic - .line_index() - .source_location(start, context.source_text()); - format!( - "{}:{}:{}: {}", - context.semantic.file_path(), - source_location.row, - source_location.column, - message, - ) -} - -fn lint_maybe_undefined(context: &SemanticLintContext, name: &ast::ExprName) { - if !matches!(name.ctx, ast::ExprContext::Load) { - return; - } - let semantic = &context.semantic; - let ty = name.ty(semantic); - if ty.is_unbound() { - context.push_diagnostic(format_diagnostic( - context, - &format!("Name `{}` used when not defined", &name.id), - name.start(), - )); - } else if ty.may_be_unbound(semantic.db()) { - context.push_diagnostic(format_diagnostic( - context, - &format!("Name `{}` used when possibly not defined", &name.id), - name.start(), - )); - } -} - -fn lint_bad_override(context: &SemanticLintContext, class: &ast::StmtClassDef) { - let semantic = &context.semantic; - - // TODO we should have a special marker on the real typing module (from typeshed) so if you - // have your own "typing" module in your project, we don't consider it THE typing module (and - // same for other stdlib modules that our lint rules care about) - let Some(typing) = semantic.resolve_module(&ModuleName::new("typing").unwrap()) else { - return; - }; - - let override_ty = semantic.global_symbol_ty(&typing, "override"); - - let Type::ClassLiteral(class_ty) = class.ty(semantic) else { - return; - }; - - for function in class - .body - .iter() - .filter_map(|stmt| stmt.as_function_def_stmt()) - { - let Type::FunctionLiteral(ty) = function.ty(semantic) else { - return; - }; - - // TODO this shouldn't make direct use of the Db; see comment on SemanticModel::db - let db = semantic.db(); - - if ty.has_decorator(db, override_ty) { - let method_name = ty.name(db); - if class_ty - .inherited_class_member(db, method_name) - .is_unbound() - { - // TODO should have a qualname() method to support nested classes - context.push_diagnostic( - format!( - "Method {}.{} is decorated with `typing.override` but does not override any base class method", - class_ty.name(db), - method_name, - )); - } - } - } -} - -pub(crate) struct SemanticLintContext<'a> { - source: SourceText, - parsed: &'a ParsedModule, - semantic: SemanticModel<'a>, - diagnostics: RefCell>, -} - -impl<'db> SemanticLintContext<'db> { - #[allow(unused)] - pub(crate) fn source_text(&self) -> &str { - self.source.as_str() - } - - #[allow(unused)] - pub(crate) fn ast(&self) -> &'db ast::ModModule { - self.parsed.syntax() - } - - pub(crate) fn push_diagnostic(&self, diagnostic: String) { - self.diagnostics.borrow_mut().push(diagnostic); - } - - #[allow(unused)] - pub(crate) fn extend_diagnostics(&mut self, diagnostics: impl IntoIterator) { - self.diagnostics.get_mut().extend(diagnostics); - } -} - -#[derive(Debug)] -struct SyntaxLintVisitor<'a> { - diagnostics: Vec, - source: &'a str, -} - -impl Visitor<'_> for SyntaxLintVisitor<'_> { - fn visit_string_literal(&mut self, string_literal: &'_ ast::StringLiteral) { - // A very naive implementation of use double quotes - let text = &self.source[string_literal.range]; - - if text.starts_with('\'') { - self.diagnostics - .push("Use double quotes for strings".to_string()); - } - } -} - -struct SemanticVisitor<'a> { - context: &'a SemanticLintContext<'a>, -} - -impl Visitor<'_> for SemanticVisitor<'_> { - fn visit_stmt(&mut self, stmt: &ast::Stmt) { - if let ast::Stmt::ClassDef(class) = stmt { - lint_bad_override(self.context, class); - } - - walk_stmt(self, stmt); - } - - fn visit_expr(&mut self, expr: &ast::Expr) { - match expr { - ast::Expr::Name(name) if matches!(name.ctx, ast::ExprContext::Load) => { - lint_maybe_undefined(self.context, name); - } - _ => {} - } - - walk_expr(self, expr); - } -} - -#[cfg(test)] -mod tests { - use red_knot_python_semantic::{Program, ProgramSettings, PythonVersion, SearchPathSettings}; - use ruff_db::files::system_path_to_file; - use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; - - use crate::db::tests::TestDb; - - use super::lint_semantic; - - fn setup_db() -> TestDb { - setup_db_with_root(SystemPathBuf::from("/src")) - } - - fn setup_db_with_root(src_root: SystemPathBuf) -> TestDb { - let db = TestDb::new(); - - db.memory_file_system() - .create_directory_all(&src_root) - .unwrap(); - - Program::from_settings( - &db, - &ProgramSettings { - target_version: PythonVersion::default(), - search_paths: SearchPathSettings::new(src_root), - }, - ) - .expect("Valid program settings"); - - db - } - - #[test] - fn undefined_variable() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x = int - if flag: - y = x - y - ", - ) - .unwrap(); - - let file = system_path_to_file(&db, "/src/a.py").expect("file to exist"); - let messages = lint_semantic(&db, file); - - assert_ne!(messages, &[] as &[String], "expected some diagnostics"); - - assert_eq!( - *messages, - if cfg!(windows) { - vec![ - "\\src\\a.py:3:4: Name `flag` used when not defined", - "\\src\\a.py:5:1: Name `y` used when possibly not defined", - ] - } else { - vec![ - "/src/a.py:3:4: Name `flag` used when not defined", - "/src/a.py:5:1: Name `y` used when possibly not defined", - ] - } - ); - } -} diff --git a/crates/red_knot_workspace/src/workspace.rs b/crates/red_knot_workspace/src/workspace.rs index a35aabc3ad..52e5f57a7a 100644 --- a/crates/red_knot_workspace/src/workspace.rs +++ b/crates/red_knot_workspace/src/workspace.rs @@ -15,12 +15,9 @@ use ruff_db::{ use ruff_python_ast::{name::Name, PySourceType}; use ruff_text_size::Ranged; +use crate::db::Db; use crate::db::RootDatabase; use crate::workspace::files::{Index, Indexed, IndexedIter, PackageFiles}; -use crate::{ - db::Db, - lint::{lint_semantic, lint_syntax}, -}; mod files; mod metadata; @@ -423,8 +420,6 @@ pub(super) fn check_file(db: &dyn Db, file: File) -> Vec { )); } - diagnostics.extend_from_slice(lint_syntax(db, file)); - diagnostics.extend_from_slice(lint_semantic(db, file)); diagnostics } @@ -540,17 +535,17 @@ impl Iterator for WorkspaceFilesIter<'_> { #[cfg(test)] mod tests { + use red_knot_python_semantic::types::check_types; use ruff_db::files::system_path_to_file; use ruff_db::source::source_text; use ruff_db::system::{DbWithTestSystem, SystemPath}; use ruff_db::testing::assert_function_query_was_not_run; use crate::db::tests::TestDb; - use crate::lint::lint_syntax; use crate::workspace::check_file; #[test] - fn check_file_skips_linting_when_file_cant_be_read() -> ruff_db::system::Result<()> { + fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> { let mut db = TestDb::new(); let path = SystemPath::new("test.py"); @@ -568,7 +563,7 @@ mod tests { ); let events = db.take_salsa_events(); - assert_function_query_was_not_run(&db, lint_syntax, file, &events); + assert_function_query_was_not_run(&db, check_types, file, &events); // The user now creates a new file with an empty text. The source text // content returned by `source_text` remains unchanged, but the diagnostics should get updated. diff --git a/crates/ruff_benchmark/benches/red_knot.rs b/crates/ruff_benchmark/benches/red_knot.rs index 1a002f190b..657b278079 100644 --- a/crates/ruff_benchmark/benches/red_knot.rs +++ b/crates/ruff_benchmark/benches/red_knot.rs @@ -28,30 +28,18 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[ // We don't support `*` imports yet: "/src/tomllib/_parser.py:7:29: Module `collections.abc` has no member `Iterable`", // We don't support terminal statements in control flow yet: - "/src/tomllib/_parser.py:353:5: Method `__getitem__` of type `Unbound | @Todo` is not callable on object of type `Unbound | @Todo`", - "/src/tomllib/_parser.py:455:9: Method `__getitem__` of type `Unbound | @Todo` is not callable on object of type `Unbound | @Todo`", - // True positives! - "Line 69 is too long (89 characters)", - "Use double quotes for strings", - "Use double quotes for strings", - "Use double quotes for strings", - "Use double quotes for strings", - "Use double quotes for strings", - "Use double quotes for strings", - "Use double quotes for strings", - // We don't support terminal statements in control flow yet: "/src/tomllib/_parser.py:66:18: Name `s` used when possibly not defined", "/src/tomllib/_parser.py:98:12: Name `char` used when possibly not defined", "/src/tomllib/_parser.py:101:12: Name `char` used when possibly not defined", "/src/tomllib/_parser.py:104:14: Name `char` used when possibly not defined", - "/src/tomllib/_parser.py:104:14: Name `char` used when possibly not defined", - "/src/tomllib/_parser.py:115:14: Name `char` used when possibly not defined", "/src/tomllib/_parser.py:115:14: Name `char` used when possibly not defined", "/src/tomllib/_parser.py:126:12: Name `char` used when possibly not defined", "/src/tomllib/_parser.py:348:20: Name `nest` used when possibly not defined", "/src/tomllib/_parser.py:353:5: Name `nest` used when possibly not defined", + "/src/tomllib/_parser.py:353:5: Method `__getitem__` of type `Unbound | @Todo` is not callable on object of type `Unbound | @Todo`", "/src/tomllib/_parser.py:453:24: Name `nest` used when possibly not defined", "/src/tomllib/_parser.py:455:9: Name `nest` used when possibly not defined", + "/src/tomllib/_parser.py:455:9: Method `__getitem__` of type `Unbound | @Todo` is not callable on object of type `Unbound | @Todo`", "/src/tomllib/_parser.py:482:16: Name `char` used when possibly not defined", "/src/tomllib/_parser.py:566:12: Name `char` used when possibly not defined", "/src/tomllib/_parser.py:573:12: Name `char` used when possibly not defined",