From d77480768d210bb71f16eeb9d871ed5d0f412815 Mon Sep 17 00:00:00 2001 From: Alex <83035922+Lexxxzy@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:23:46 +0300 Subject: [PATCH] [red-knot] Port type inference tests to new test framework (#13719) ## Summary Porting infer tests to new markdown tests framework. Link to the corresponding issue: #13696 --------- Co-authored-by: Carl Meyer --- .../mdtest/assignment/annotations.md | 25 + .../mdtest/assignment/multi_target.md | 9 + .../resources/mdtest/assignment/unbound.md | 32 + .../resources/mdtest/assignment/walrus.md | 17 + .../resources/mdtest/attributes.md | 15 + .../resources/mdtest/binary/integers.md | 36 + .../mdtest/call/callable_instance.md | 21 + .../resources/mdtest/call/constructor.md | 8 + .../resources/mdtest/call/function.md | 51 + .../resources/mdtest/call/union.md | 74 + .../resources/mdtest/comparison/integers.md | 41 + .../mdtest/comparison/non_boolean_returns.md | 37 + .../resources/mdtest/comparison/strings.md | 29 + .../mdtest/comparison/unsupported.md | 15 + .../mdtest/conditional/if_expression.md | 35 + .../mdtest/conditional/if_statement.md | 97 + .../resources/mdtest/conditional/match.md | 39 + .../resources/mdtest/declaration/error.md | 39 + .../resources/mdtest/exception/basic.md | 55 + .../resources/mdtest/exception/except_star.md | 32 + .../resources/mdtest/expression/boolean.md | 151 + .../resources/mdtest/import/basic.md | 23 + .../resources/mdtest/import/builtins.md | 6 + .../resources/mdtest/import/conditional.md | 42 + .../resources/mdtest/import/errors.md | 47 + .../resources/mdtest/import/relative.md | 133 + .../resources/mdtest/import/stubs.md | 25 + .../resources/mdtest/literal/boolean.md | 8 + .../mdtest/literal/collections/dictionary.md | 8 + .../mdtest/literal/collections/list.md | 8 + .../mdtest/literal/collections/set.md | 8 + .../mdtest/literal/collections/tuple.md | 20 + .../resources/mdtest/literal/complex.md | 7 + .../resources/mdtest/literal/f_string.md | 44 + .../resources/mdtest/literal/float.md | 7 + .../resources/mdtest/literal/integer.md | 56 + .../resources/mdtest/literal/string.md | 26 + .../resources/mdtest/loops/async_for_loops.md | 41 + .../resources/mdtest/loops/for_loop.md | 134 + .../resources/mdtest/loops/iterators.md | 18 + .../resources/mdtest/loops/while_loop.md | 43 + .../resources/mdtest/narrow/not_none.md | 9 + .../resources/mdtest/numbers.md | 35 - .../resources/mdtest/shadowing/class.md | 17 + .../resources/mdtest/shadowing/function.md | 24 + .../mdtest/shadowing/variable_declaration.md | 11 + .../resources/mdtest/stubs/class.md | 10 + .../resources/mdtest/subscript/bytes.md | 15 + .../resources/mdtest/subscript/class.md | 92 + .../resources/mdtest/subscript/instance.md | 45 + .../resources/mdtest/subscript/string.md | 32 + .../resources/mdtest/subscript/tuple.md | 21 + .../resources/mdtest/unary/integers.md | 37 + .../resources/mdtest/unary/not.md | 149 + .../src/types/infer.rs | 4633 +++-------------- crates/red_knot_test/src/parser.rs | 2 +- 56 files changed, 2682 insertions(+), 4012 deletions(-) create mode 100644 crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/assignment/multi_target.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/assignment/walrus.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/attributes.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/binary/integers.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/call/constructor.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/call/function.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/call/union.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/comparison/non_boolean_returns.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/comparison/strings.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/conditional/match.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/declaration/error.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/exception/basic.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/import/basic.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/import/builtins.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/import/conditional.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/import/errors.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/import/relative.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/import/stubs.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/literal/boolean.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/literal/collections/list.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/literal/collections/set.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/literal/collections/tuple.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/literal/complex.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/literal/f_string.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/literal/float.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/literal/integer.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/literal/string.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/loops/async_for_loops.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/loops/for_loop.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/loops/iterators.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/narrow/not_none.md delete mode 100644 crates/red_knot_python_semantic/resources/mdtest/numbers.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/shadowing/class.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/shadowing/function.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/shadowing/variable_declaration.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/stubs/class.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/subscript/class.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/subscript/instance.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/subscript/string.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/unary/integers.md create mode 100644 crates/red_knot_python_semantic/resources/mdtest/unary/not.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md b/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md new file mode 100644 index 0000000000000..c56674aebb74a --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md @@ -0,0 +1,25 @@ +# Assignment with annotations + +## Annotation only transparent to local inference + +```py +x = 1 +x: int +y = x + +reveal_type(y) # revealed: Literal[1] +``` + +## Violates own annotation + +```py +x: int = 'foo' # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`" + +``` + +## Violates previous annotation + +```py +x: int +x = 'foo' # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`" +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/multi_target.md b/crates/red_knot_python_semantic/resources/mdtest/assignment/multi_target.md new file mode 100644 index 0000000000000..285f31f6854a3 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/multi_target.md @@ -0,0 +1,9 @@ +# Multi-target assignment + +## Basic + +```py +x = y = 1 +reveal_type(x) # revealed: Literal[1] +reveal_type(y) # revealed: Literal[1] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md b/crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md new file mode 100644 index 0000000000000..b875f677ff751 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/unbound.md @@ -0,0 +1,32 @@ +# Unbound + +## Maybe unbound + +```py +if flag: + y = 3 +x = y +reveal_type(x) # revealed: Unbound | Literal[3] +``` + +## Unbound + +```py +x = foo; foo = 1 +reveal_type(x) # revealed: Unbound +``` + +## Unbound class variable + +Class variables can reference global variables unless overridden within the class scope. + +```py +x = 1 +class C: + y = x + if flag: + x = 2 + +reveal_type(C.x) # revealed: Unbound | Literal[2] +reveal_type(C.y) # revealed: Literal[1] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/walrus.md b/crates/red_knot_python_semantic/resources/mdtest/assignment/walrus.md new file mode 100644 index 0000000000000..d493d5b629fef --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/walrus.md @@ -0,0 +1,17 @@ +# Walrus operator + +## Basic + +```py +x = (y := 1) + 1 +reveal_type(x) # revealed: Literal[2] +reveal_type(y) # revealed: Literal[1] +``` + +## Walrus self-addition + +```py +x = 0 +(x := x + 1) +reveal_type(x) # revealed: Literal[1] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md new file mode 100644 index 0000000000000..b077f515c0ae5 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -0,0 +1,15 @@ +# Class attributes + +## Union of attributes + +```py +if flag: + class C: + x = 1 +else: + class C: + x = 2 + +y = C.x +reveal_type(y) # revealed: Literal[1, 2] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md new file mode 100644 index 0000000000000..9251e3e8d2b2a --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md @@ -0,0 +1,36 @@ +## Binary operations on integers + +## Basic Arithmetic + +```py +a = 2 + 1 +b = a - 4 +c = a * b +d = c // 3 +e = c / 3 +f = 5 % 3 + +reveal_type(a) # revealed: Literal[3] +reveal_type(b) # revealed: Literal[-1] +reveal_type(c) # revealed: Literal[-3] +reveal_type(d) # revealed: Literal[-1] +reveal_type(e) # revealed: float +reveal_type(f) # revealed: Literal[2] +``` + +## Division by Zero + +```py +# TODO: `a` should be `int` and `e` should be `float` once we support inference. +a = 1 / 0 # error: "Cannot divide object of type `Literal[1]` by zero" +b = 2 // 0 # error: "Cannot floor divide object of type `Literal[2]` by zero" +c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero" +d = int() / 0 # error: "Cannot divide object of type `int` by zero" +e = 1.0 / 0 # error: "Cannot divide object of type `float` by zero" + +reveal_type(a) # revealed: float +reveal_type(b) # revealed: int +reveal_type(c) # revealed: int +reveal_type(d) # revealed: @Todo +reveal_type(e) # revealed: @Todo +``` 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 new file mode 100644 index 0000000000000..1b1c46bcc121e --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md @@ -0,0 +1,21 @@ +# Callable instance + +## Dunder call + +```py +class Multiplier: + def __init__(self, factor: float): + self.factor = factor + + def __call__(self, number: float) -> float: + return number * self.factor + +a = Multiplier(2.0)(3.0) + +class Unit: ... + +b = Unit()(3.0) # error: "Object of type `Unit` is not callable" + +reveal_type(a) # revealed: float +reveal_type(b) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md b/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md new file mode 100644 index 0000000000000..100568a8dd5a5 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/call/constructor.md @@ -0,0 +1,8 @@ +# Constructor + +```py +class Foo: ... + +x = Foo() +reveal_type(x) # revealed: Foo +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/function.md b/crates/red_knot_python_semantic/resources/mdtest/call/function.md new file mode 100644 index 0000000000000..055345eb28383 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/call/function.md @@ -0,0 +1,51 @@ +# Call expression + +## Simple + +```py +def get_int() -> int: + return 42 + +x = get_int() +reveal_type(x) # revealed: int +``` + +## Async + +```py +async def get_int_async() -> int: + return 42 + +x = get_int_async() + +# TODO: we don't yet support `types.CoroutineType`, should be generic `Coroutine[Any, Any, int]` +reveal_type(x) # revealed: @Todo +``` + +## Decorated + +```py +from typing import Callable + +def foo() -> int: + return 42 + +def decorator(func) -> Callable[[], int]: + return foo + +@decorator +def bar() -> str: + return 'bar' + +x = bar() + +# TODO: should reveal `int`, as the decorator replaces `bar` with `foo` +reveal_type(x) # revealed: @Todo +``` + +## Invalid callable + +```py +nonsense = 123 +x = nonsense() # error: "Object of type `Literal[123]` is not callable" +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/union.md b/crates/red_knot_python_semantic/resources/mdtest/call/union.md new file mode 100644 index 0000000000000..25beeedcd4216 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/call/union.md @@ -0,0 +1,74 @@ +# Unions in calls + +## Union of return types + +```py +if flag: + def f() -> int: + return 1 +else: + def f() -> str: + return 'foo' + +x = f() +reveal_type(x) # revealed: int | str +``` + +## Calling with an unknown union + +```py +from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`" + +if flag: + def f() -> int: + return 1 + +x = f() +reveal_type(x) # revealed: Unknown | int +``` + +## Non-callable elements in a union + +Calling a union with a non-callable element should emit a diagnostic. + +```py +if flag: + f = 1 +else: + def f() -> int: + return 1 + +x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)" +reveal_type(x) # revealed: Unknown | int +``` + +## Multiple non-callable elements in a union + +Calling a union with multiple non-callable elements should mention all of them in the diagnostic. + +```py +if flag: + f = 1 +elif flag2: + f = 'foo' +else: + def f() -> int: + return 1 + +x = f() # error: "Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])" +reveal_type(x) # revealed: Unknown | int +``` + +## All non-callable union elements + +Calling a union with no callable elements can emit a simpler diagnostic. + +```py +if flag: + f = 1 +else: + f = 'foo' + +x = f() # error: "Object of type `Literal[1] | Literal["foo"]` is not callable" +reveal_type(x) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md new file mode 100644 index 0000000000000..a72f02129bb58 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md @@ -0,0 +1,41 @@ +# Comparing integers + +## Integer literals + +```py +a = 1 == 1 == True +b = 1 == 1 == 2 == 4 +c = False < True <= 2 < 3 != 6 +d = 1 < 1 +e = 1 > 1 +f = 1 is 1 +g = 1 is not 1 +h = 1 is 2 +i = 1 is not 7 +j = 1 <= "" and 0 < 1 + +reveal_type(a) # revealed: Literal[True] +reveal_type(b) # revealed: Literal[False] +reveal_type(c) # revealed: Literal[True] +reveal_type(d) # revealed: Literal[False] +reveal_type(e) # revealed: Literal[False] +reveal_type(f) # revealed: bool +reveal_type(g) # revealed: bool +reveal_type(h) # revealed: Literal[False] +reveal_type(i) # revealed: Literal[True] +reveal_type(j) # revealed: @Todo | Literal[True] +``` + +## Integer instance + +```py +# TODO: implement lookup of `__eq__` on typeshed `int` stub. +def int_instance() -> int: ... +a = 1 == int_instance() +b = 9 < int_instance() +c = int_instance() < int_instance() + +reveal_type(a) # revealed: @Todo +reveal_type(b) # revealed: bool +reveal_type(c) # revealed: bool +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/non_boolean_returns.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/non_boolean_returns.md new file mode 100644 index 0000000000000..8917afba7f16e --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/non_boolean_returns.md @@ -0,0 +1,37 @@ +# Non boolean returns + +Walking through examples: + +- `a = A() < B() < C()` + + 1. `A() < B() and B() < C()` - split in N comparison + 1. `A()` and `B()` - evaluate outcome types + 1. `bool` and `bool` - evaluate truthiness + 1. `A | B` - union of "first true" types + +- `b = 0 < 1 < A() < 3` + + 1. `0 < 1 and 1 < A() and A() < 3` - split in N comparison + 1. `True` and `bool` and `A` - evaluate outcome types + 1. `True` and `bool` and `bool` - evaluate truthiness + 1. `bool | A` - union of "true" types + +- `c = 10 < 0 < A() < B() < C()` short-circuit to False + +```py +from __future__ import annotations +class A: + def __lt__(self, other) -> A: ... +class B: + def __lt__(self, other) -> B: ... +class C: + def __lt__(self, other) -> C: ... + +a = A() < B() < C() +b = 0 < 1 < A() < 3 +c = 10 < 0 < A() < B() < C() + +reveal_type(a) # revealed: A | B +reveal_type(b) # revealed: bool | A +reveal_type(c) # revealed: Literal[False] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/strings.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/strings.md new file mode 100644 index 0000000000000..04b5c41f01ea3 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/strings.md @@ -0,0 +1,29 @@ +# Comparing strings + +## String literals + +```py +def str_instance() -> str: ... +a = "abc" == "abc" +b = "ab_cd" <= "ab_ce" +c = "abc" in "ab cd" +d = "" not in "hello" +e = "--" is "--" +f = "A" is "B" +g = "--" is not "--" +h = "A" is not "B" +i = str_instance() < "..." +# ensure we're not comparing the interned salsa symbols, which compare by order of declaration. +j = "ab" < "ab_cd" + +reveal_type(a) # revealed: Literal[True] +reveal_type(b) # revealed: Literal[True] +reveal_type(c) # revealed: Literal[False] +reveal_type(d) # revealed: Literal[False] +reveal_type(e) # revealed: bool +reveal_type(f) # revealed: Literal[False] +reveal_type(g) # revealed: bool +reveal_type(h) # revealed: Literal[True] +reveal_type(i) # revealed: bool +reveal_type(j) # revealed: Literal[True] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md new file mode 100644 index 0000000000000..156d03342eb5a --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md @@ -0,0 +1,15 @@ +# Unsupported operators + +```py +a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`" +b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`" +c = object() < 5 # error: "Operator `<` is not supported for types `object` and `Literal[5]`" +# TODO should error, need to check if __lt__ signature is valid for right operand +d = 5 < object() + +reveal_type(a) # revealed: bool +reveal_type(b) # revealed: bool +reveal_type(c) # revealed: Unknown +# TODO: should be `Unknown` +reveal_type(d) # revealed: bool +``` 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 new file mode 100644 index 0000000000000..cdd2e400dac10 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_expression.md @@ -0,0 +1,35 @@ +# If expressions + +## Simple if-expression + +```py +x = 1 if flag else 2 +reveal_type(x) # revealed: Literal[1, 2] +``` + +## If-expression with walrus operator + +```py +y = 0 +z = 0 +x = (y := 1) if flag else (z := 2) +a = y +b = z +reveal_type(x) # revealed: Literal[1, 2] +reveal_type(a) # revealed: Literal[0, 1] +reveal_type(b) # revealed: Literal[0, 2] +``` + +## Nested if-expression + +```py +x = 1 if flag else 2 if flag2 else 3 +reveal_type(x) # revealed: Literal[1, 2, 3] +``` + +## None + +```py +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 new file mode 100644 index 0000000000000..6edf0fb855ae4 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/if_statement.md @@ -0,0 +1,97 @@ +# If statements + +## Simple if + +```py +y = 1 +y = 2 + +if flag: + y = 3 + +x = y + +reveal_type(x) # revealed: Literal[2, 3] +``` + +## Simple if-elif-else + +```py +y = 1 +y = 2 +if flag: + y = 3 +elif flag2: + y = 4 +else: + r = y + y = 5 + s = y +x = y +reveal_type(x) # revealed: Literal[3, 4, 5] +reveal_type(r) # revealed: Unbound | Literal[2] +reveal_type(s) # revealed: Unbound | Literal[5] +``` + +## Single symbol across if-elif-else + +```py +if flag: + y = 1 +elif flag2: + y = 2 +else: + y = 3 +reveal_type(y) # revealed: Literal[1, 2, 3] +``` + +## if-elif-else without else assignment + +```py +y = 0 +if flag: + y = 1 +elif flag2: + y = 2 +else: + pass +reveal_type(y) # revealed: Literal[0, 1, 2] +``` + +## if-elif-else with intervening assignment + +```py +y = 0 +if flag: + y = 1 + z = 3 +elif flag2: + y = 2 +else: + pass +reveal_type(y) # revealed: Literal[0, 1, 2] +``` + +## Nested if statement + +```py +y = 0 +if flag: + if flag2: + y = 1 +reveal_type(y) # revealed: Literal[0, 1] +``` + +## if-elif without else + +```py +y = 1 +y = 2 +if flag: + y = 3 +elif flag2: + y = 4 +x = y + +reveal_type(x) # revealed: Literal[2, 3, 4] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md b/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md new file mode 100644 index 0000000000000..2ce7c9462d90d --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/conditional/match.md @@ -0,0 +1,39 @@ +# Pattern matching + +## With wildcard + +```py +match 0: + case 1: + y = 2 + case _: + y = 3 + +reveal_type(y) # revealed: Literal[2, 3] +``` + +## Without wildcard + +```py +match 0: + case 1: + y = 2 + case 2: + y = 3 + +reveal_type(y) # revealed: Unbound | Literal[2, 3] +``` + +## Basic match + +```py +y = 1 +y = 2 +match 0: + case 1: + y = 3 + case 2: + y = 4 + +reveal_type(y) # revealed: Literal[2, 3, 4] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md b/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md new file mode 100644 index 0000000000000..2062f0fa76bcb --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/declaration/error.md @@ -0,0 +1,39 @@ +# Errors while declaring + +## Violates previous assignment + +```py +x = 1 +x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred type `Literal[1]`" +``` + +## Incompatible declarations + +```py +if flag: + x: str +else: + x: int +x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int" +``` + +## Partial declarations + +```py +if flag: + x: int +x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int" +``` + +## Incompatible declarations with bad assignment + +```py +if flag: + x: str +else: + x: int + +# error: [conflicting-declarations] +# error: [invalid-assignment] +x = b'foo' +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md b/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md new file mode 100644 index 0000000000000..f0af58b3b77b0 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md @@ -0,0 +1,55 @@ +# Exception Handling + +## Single Exception + +```py +import re +try: + x +except NameError as e: + reveal_type(e) # revealed: NameError +except re.error as f: + reveal_type(f) # revealed: error +``` + +## Unknown type in except handler does not cause spurious diagnostic + +```py +from nonexistent_module import foo # error: [unresolved-import] + +try: + x +except foo as e: + reveal_type(foo) # revealed: Unknown + reveal_type(e) # revealed: Unknown +``` + +## Multiple Exceptions in a Tuple + +```py +EXCEPTIONS = (AttributeError, TypeError) + +try: + x +except (RuntimeError, OSError) as e: + reveal_type(e) # revealed: RuntimeError | OSError +except EXCEPTIONS as f: + reveal_type(f) # revealed: AttributeError | TypeError +``` + +## Dynamic exception types + +```py +def foo(x: type[AttributeError], y: tuple[type[OSError], type[RuntimeError]], z: tuple[type[BaseException], ...]): + try: + w + except x as e: + # TODO: should be `AttributeError` + reveal_type(e) # revealed: @Todo + except y as f: + # TODO: should be `OSError | RuntimeError` + reveal_type(f) # revealed: @Todo + except z as g: + # TODO: should be `BaseException` + reveal_type(g) # 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 new file mode 100644 index 0000000000000..e9b3dfbca4e29 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md @@ -0,0 +1,32 @@ +# Except star + +TODO(Alex): Once we support `sys.version_info` branches, we can set `--target-version=py311` in these tests and the inferred type will just be `BaseExceptionGroup` + +## Except\* with BaseException + +```py +try: + x +except* BaseException as e: + reveal_type(e) # revealed: Unknown | BaseExceptionGroup +``` + +## Except\* with specific exception + +```py +try: + x +except* OSError as e: + # TODO(Alex): more precise would be `ExceptionGroup[OSError]` + reveal_type(e) # revealed: Unknown | BaseExceptionGroup +``` + +## Except\* with multiple exceptions + +```py +try: + x +except* (TypeError, AttributeError) as e: + #TODO(Alex): more precise would be `ExceptionGroup[TypeError | AttributeError]`. + reveal_type(e) # revealed: Unknown | BaseExceptionGroup +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md b/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md new file mode 100644 index 0000000000000..f27d6cfc11d5b --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md @@ -0,0 +1,151 @@ +# Expressions + +## OR + +```py +def foo() -> str: + pass + +a = True or False +b = 'x' or 'y' or 'z' +c = '' or 'y' or 'z' +d = False or 'z' +e = False or True +f = False or False +g = foo() or False +h = foo() or True + +reveal_type(a) # revealed: Literal[True] +reveal_type(b) # revealed: Literal["x"] +reveal_type(c) # revealed: Literal["y"] +reveal_type(d) # revealed: Literal["z"] +reveal_type(e) # revealed: Literal[True] +reveal_type(f) # revealed: Literal[False] +reveal_type(g) # revealed: str | Literal[False] +reveal_type(h) # revealed: str | Literal[True] +``` + +## AND + +```py +def foo() -> str: + pass + +a = True and False +b = False and True +c = foo() and False +d = foo() and True +e = 'x' and 'y' and 'z' +f = 'x' and 'y' and '' +g = '' and 'y' + +reveal_type(a) # revealed: Literal[False] +reveal_type(b) # revealed: Literal[False] +reveal_type(c) # revealed: str | Literal[False] +reveal_type(d) # revealed: str | Literal[True] +reveal_type(e) # revealed: Literal["z"] +reveal_type(f) # revealed: Literal[""] +reveal_type(g) # revealed: Literal[""] +``` + +## Simple function calls to bool + +```py +def returns_bool() -> bool: + return True + +if returns_bool(): + x = True +else: + x = False + +reveal_type(x) # revealed: bool +``` + +## Complex + +```py +def foo() -> str: + pass + +a = "x" and "y" or "z" +b = "x" or "y" and "z" +c = "" and "y" or "z" +d = "" or "y" and "z" +e = "x" and "y" or "" +f = "x" or "y" and "" + +reveal_type(a) # revealed: Literal["y"] +reveal_type(b) # revealed: Literal["x"] +reveal_type(c) # revealed: Literal["z"] +reveal_type(d) # revealed: Literal["z"] +reveal_type(e) # revealed: Literal["y"] +reveal_type(f) # revealed: Literal["x"] +``` + +## `bool()` function + +## Evaluates to builtin + +```py path=a.py +redefined_builtin_bool = bool + +def my_bool(x)-> bool: pass +``` + +```py +from a import redefined_builtin_bool, my_bool +a = redefined_builtin_bool(0) +b = my_bool(0) + +reveal_type(a) # revealed: Literal[False] +reveal_type(b) # revealed: bool +``` + +## Truthy values + +```py +a = bool(1) +b = bool((0,)) +c = bool("NON EMPTY") +d = bool(True) + +def foo(): pass +e = bool(foo) + +reveal_type(a) # revealed: Literal[True] +reveal_type(b) # revealed: Literal[True] +reveal_type(c) # revealed: Literal[True] +reveal_type(d) # revealed: Literal[True] +reveal_type(e) # revealed: Literal[True] +``` + +## Falsy values + +```py +a = bool(0) +b = bool(()) +c = bool(None) +d = bool("") +e = bool(False) +f = bool() + +reveal_type(a) # revealed: Literal[False] +reveal_type(b) # revealed: Literal[False] +reveal_type(c) # revealed: Literal[False] +reveal_type(d) # revealed: Literal[False] +reveal_type(e) # revealed: Literal[False] +reveal_type(f) # revealed: Literal[False] +``` + +## Ambiguous values + +```py +a = bool([]) +b = bool({}) +c = bool(set()) + +reveal_type(a) # revealed: bool +reveal_type(b) # revealed: bool +reveal_type(c) # revealed: bool +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/basic.md b/crates/red_knot_python_semantic/resources/mdtest/import/basic.md new file mode 100644 index 0000000000000..b3702cbcacccb --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/import/basic.md @@ -0,0 +1,23 @@ +# Structures + +## Class import following + +```py +from b import C as D; E = D +reveal_type(E) # revealed: Literal[C] +``` + +```py path=b.py +class C: pass +``` + +## Module member resolution + +```py +import b; D = b.C +reveal_type(D) # revealed: Literal[C] +``` + +```py path=b.py +class C: pass +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md b/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md new file mode 100644 index 0000000000000..4c0c66fdd09cb --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/import/builtins.md @@ -0,0 +1,6 @@ +# Importing builtin module + +```py +import builtins; x = builtins.copyright +reveal_type(x) # revealed: Literal[copyright] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/conditional.md b/crates/red_knot_python_semantic/resources/mdtest/import/conditional.md new file mode 100644 index 0000000000000..3ceb12c32ab7f --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/import/conditional.md @@ -0,0 +1,42 @@ +# Conditional imports + +## Reimport + +```py path=c.py +def f(): ... +``` + +```py path=b.py +if flag: + from c import f +else: + def f(): ... +``` + +```py +# TODO we should not emit this error +from b import f # error: [invalid-assignment] "Object of type `Literal[f, f]` is not assignable to `Literal[f, f]`" +# TODO: We should disambiguate in such cases, showing `Literal[b.f, c.f]`. +reveal_type(f) # revealed: Literal[f, f] +``` + +## Reimport with stub declaration + +When we have a declared type in one path and only an inferred-from-definition type in the other, we +should still be able to unify those: + +```py path=c.pyi +x: int +``` + +```py path=b.py +if flag: + from c import x +else: + x = 1 +``` + +```py +from b import x +reveal_type(x) # revealed: int +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/errors.md b/crates/red_knot_python_semantic/resources/mdtest/import/errors.md new file mode 100644 index 0000000000000..9c3b8482c9d30 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/import/errors.md @@ -0,0 +1,47 @@ +# Unresolved Imports + +## Unresolved import statement + +```py +import bar # error: "Cannot resolve import `bar`" +``` + +## Unresolved import from statement + +```py +from bar import baz # error: "Cannot resolve import `bar`" +``` + +## Unresolved import from resolved module + +```py path=a.py +``` + +```py +from a import thing # error: "Module `a` has no member `thing`" +``` + +## Resolved import of symbol from unresolved import + +```py path=a.py +import foo as foo # error: "Cannot resolve import `foo`" +``` + +Importing the unresolved import into a second file should not trigger an additional "unresolved +import" violation: + +```py +from a import foo +``` + +## No implicit shadowing error + +```py path=b.py +x: int +``` + +```py +from b import x + +x = 'foo' # error: "Object of type `Literal["foo"]" +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/relative.md b/crates/red_knot_python_semantic/resources/mdtest/import/relative.md new file mode 100644 index 0000000000000..c7d618cfcaf6b --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/import/relative.md @@ -0,0 +1,133 @@ +# Relative + +## Non-existent + +```py path=package/__init__.py +``` + +```py path=package/bar.py +from .foo import X # error: [unresolved-import] +reveal_type(X) # revealed: Unknown +``` + +## Simple + +```py path=package/__init__.py +``` + +```py path=package/foo.py +X = 42 +``` + +```py path=package/bar.py +from .foo import X +reveal_type(X) # revealed: Literal[42] +``` + +## Dotted + +```py path=package/__init__.py +``` + +```py path=package/foo/bar/baz.py +X = 42 +``` + +```py path=package/bar.py +from .foo.bar.baz import X +reveal_type(X) # revealed: Literal[42] +``` + +## Bare to package + +```py path=package/__init__.py +X = 42 +``` + +```py path=package/bar.py +from . import X +reveal_type(X) # revealed: Literal[42] +``` + +## Non-existent + bare to package + +```py path=package/bar.py +from . import X # error: [unresolved-import] +reveal_type(X) # revealed: Unknown +``` + +## Dunder init + +```py path=package/__init__.py +from .foo import X +reveal_type(X) # revealed: Literal[42] +``` + +```py path=package/foo.py +X = 42 +``` + +## Non-existent + dunder init + +```py path=package/__init__.py +from .foo import X # error: [unresolved-import] +reveal_type(X) # revealed: Unknown +``` + +## Long relative import + +```py path=package/__init__.py +``` + +```py path=package/foo.py +X = 42 +``` + +```py path=package/subpackage/subsubpackage/bar.py +from ...foo import X +reveal_type(X) # revealed: Literal[42] +``` + +## Unbound symbol + +```py path=package/__init__.py +``` + +```py path=package/foo.py +x +``` + +```py path=package/bar.py +from .foo import x # error: [unresolved-import] +reveal_type(x) # revealed: Unknown +``` + +## Bare to module + +```py path=package/__init__.py +``` + +```py path=package/foo.py +X = 42 +``` + +```py path=package/bar.py +# TODO: support submodule imports +from . import foo # error: [unresolved-import] +y = foo.X + +# TODO: should be `Literal[42]` +reveal_type(y) # revealed: Unknown +``` + +## Non-existent + bare to module + +```py path=package/__init__.py +``` + +```py path=package/bar.py +# TODO: submodule imports possibly not supported right now? +from . import foo # error: [unresolved-import] + +reveal_type(foo) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/import/stubs.md b/crates/red_knot_python_semantic/resources/mdtest/import/stubs.md new file mode 100644 index 0000000000000..9b6fb7f0ade70 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/import/stubs.md @@ -0,0 +1,25 @@ +# Stubs + +## Import from stub declaration + +```py +from b import x +y = x +reveal_type(y) # revealed: int +``` + +```py path=b.pyi +x: int +``` + +## Import from non-stub with declaration and definition + +```py +from b import x +y = x +reveal_type(y) # revealed: int +``` + +```py path=b.py +x: int = 1 +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/boolean.md b/crates/red_knot_python_semantic/resources/mdtest/literal/boolean.md new file mode 100644 index 0000000000000..00cc4073a78ee --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/literal/boolean.md @@ -0,0 +1,8 @@ +# Boolean literals + +```py +x = True +y = False +reveal_type(x) # revealed: Literal[True] +reveal_type(y) # revealed: Literal[False] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md new file mode 100644 index 0000000000000..dc4aa3197fb65 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md @@ -0,0 +1,8 @@ +# Dictionaries + +## Empty dictionary + +```py +x = {} +reveal_type(x) # revealed: dict +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/list.md b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/list.md new file mode 100644 index 0000000000000..67b3e1f907b18 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/list.md @@ -0,0 +1,8 @@ +# Lists + +## Empty list + +```py +x = [] +reveal_type(x) # revealed: list +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/set.md b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/set.md new file mode 100644 index 0000000000000..dcd1da384b57c --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/set.md @@ -0,0 +1,8 @@ +# Sets + +## Basic set + +```py +x = {1, 2} +reveal_type(x) # revealed: set +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/tuple.md b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/tuple.md new file mode 100644 index 0000000000000..159da51d8e852 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/tuple.md @@ -0,0 +1,20 @@ +# Tuples + +## Empty tuple + +```py +x = () +reveal_type(x) # revealed: tuple[()] +``` + +## Heterogeneous tuple + +```py +x = (1, 'a') +y = (1, (2, 3)) +z = (x, 2) + +reveal_type(x) # revealed: tuple[Literal[1], Literal["a"]] +reveal_type(y) # revealed: tuple[Literal[1], tuple[Literal[2], Literal[3]]] +reveal_type(z) # revealed: tuple[tuple[Literal[1], Literal["a"]], Literal[2]] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/complex.md b/crates/red_knot_python_semantic/resources/mdtest/literal/complex.md new file mode 100644 index 0000000000000..4071a041f1b7d --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/literal/complex.md @@ -0,0 +1,7 @@ +# Complex literals + +## Complex numbers + +```py +reveal_type(2j) # revealed: complex +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/f_string.md b/crates/red_knot_python_semantic/resources/mdtest/literal/f_string.md new file mode 100644 index 0000000000000..524b76d9fca5f --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/literal/f_string.md @@ -0,0 +1,44 @@ +# f-strings + +## Expression + +```py +x = 0 +y = str() +z = False + +a = f'hello' +b = f'h {x}' +c = 'one ' f'single ' f'literal' +d = 'first ' f'second({b})' f' third' +e = f'-{y}-' +f = f'-{y}-' f'--' '--' +g = f'{z} == {False} is {True}' + +reveal_type(a) # revealed: Literal["hello"] +reveal_type(b) # revealed: Literal["h 0"] +reveal_type(c) # revealed: Literal["one single literal"] +reveal_type(d) # revealed: Literal["first second(h 0) third"] +reveal_type(e) # revealed: str +reveal_type(f) # revealed: str +reveal_type(g) # revealed: Literal["False == False is True"] +``` + +## Conversion Flags + +```py +string = 'hello' +a = f'{string!r}' + +# TODO: should be `Literal["'hello'"]` +reveal_type(a) # revealed: str +``` + +## Format Specifiers + +```py +a = f'{1:02}' + +# TODO: should be `Literal["01"]` +reveal_type(a) # revealed: str +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/float.md b/crates/red_knot_python_semantic/resources/mdtest/literal/float.md new file mode 100644 index 0000000000000..e4b0597ff23a9 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/literal/float.md @@ -0,0 +1,7 @@ +# Float literals + +## Basic + +```py +reveal_type(1.0) # revealed: float +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/integer.md b/crates/red_knot_python_semantic/resources/mdtest/literal/integer.md new file mode 100644 index 0000000000000..338a3b6107a78 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/literal/integer.md @@ -0,0 +1,56 @@ +# Integer literals + +## Literals + +We can infer an integer literal type: + +```py +reveal_type(1) # revealed: Literal[1] +``` + +## Variable + +```py +x = 1 +reveal_type(x) # revealed: Literal[1] +``` + +## Overflow + +We only track integer literals within the range of an i64: + +```py +reveal_type(9223372036854775808) # revealed: int +``` + +## Big int + +We don't support big integer literals; we just infer `int` type instead: + +```py +x = 10_000_000_000_000_000_000 +reveal_type(x) # revealed: int +``` + +## Negated + +```py +x = -1 +y = -1234567890987654321 +z = --987 +reveal_type(x) # revealed: Literal[-1] +reveal_type(y) # revealed: Literal[-1234567890987654321] +reveal_type(z) # revealed: Literal[987] +``` + +## Floats + +```py +reveal_type(1.0) # revealed: float +``` + +## Complex + +```py +reveal_type(2j) # revealed: complex +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/string.md b/crates/red_knot_python_semantic/resources/mdtest/literal/string.md new file mode 100644 index 0000000000000..80a320eece042 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/literal/string.md @@ -0,0 +1,26 @@ +# String literals + +## Simple + +```py +w = "Hello" +x = 'world' +y = "Guten " + 'tag' +z = 'bon ' + "jour" + +reveal_type(w) # revealed: Literal["Hello"] +reveal_type(x) # revealed: Literal["world"] +reveal_type(y) # revealed: Literal["Guten tag"] +reveal_type(z) # revealed: Literal["bon jour"] +``` + +## Nested Quotes + +```py +x = 'I say "hello" to you' +y = "You say \"hey\" back" +z = 'No "closure here' +reveal_type(x) # revealed: Literal["I say \"hello\" to you"] +reveal_type(y) # revealed: Literal["You say \"hey\" back"] +reveal_type(z) # revealed: Literal["No \"closure here"] +``` 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 new file mode 100644 index 0000000000000..c09a60dd1b86e --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/async_for_loops.md @@ -0,0 +1,41 @@ +# Async + +Async `for` loops do not work according to the synchronous iteration protocol. + +## Invalid async for loop + +```py +async def foo(): + class Iterator: + def __next__(self) -> int: + return 42 + + class Iterable: + def __iter__(self) -> Iterator: + return Iterator() + + async for x in Iterator(): + pass + + # TODO + reveal_type(x) # revealed: Unbound | @Todo +``` + +## Basic async for loop + +```py +async def foo(): + class IntAsyncIterator: + async def __anext__(self) -> int: + return 42 + + class IntAsyncIterable: + def __aiter__(self) -> IntAsyncIterator: + return IntAsyncIterator() + + #TODO(Alex): async iterables/iterators! + async for x in IntAsyncIterable(): + pass + + reveal_type(x) # revealed: Unbound | @Todo +``` 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 new file mode 100644 index 0000000000000..dc12ca30afa7a --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/for_loop.md @@ -0,0 +1,134 @@ +# For loops + +## Basic `for` loop + +```py +class IntIterator: + def __next__(self) -> int: + return 42 + +class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() + +for x in IntIterable(): + pass + +reveal_type(x) # revealed: Unbound | int +``` + +## With previous definition + +```py +class IntIterator: + def __next__(self) -> int: + return 42 + +class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() + +x = 'foo' + +for x in IntIterable(): + pass + +reveal_type(x) # revealed: Literal["foo"] | int +``` + +## With `else` (no break) + +```py +class IntIterator: + def __next__(self) -> int: + return 42 + +class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() + +for x in IntIterable(): + pass +else: + x = 'foo' + +reveal_type(x) # revealed: Literal["foo"] +``` + +## May `break` + +```py +class IntIterator: + def __next__(self) -> int: + return 42 + +class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() + +for x in IntIterable(): + if x > 5: + break +else: + x = 'foo' + +reveal_type(x) # revealed: int | Literal["foo"] +``` + +## With old-style iteration protocol + +```py +class OldStyleIterable: + def __getitem__(self, key: int) -> int: + return 42 + +for x in OldStyleIterable(): + pass + +reveal_type(x) # revealed: Unbound | int +``` + +## With heterogeneous tuple + +```py +for x in (1, 'a', b'foo'): + pass + +reveal_type(x) # revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"] +``` + +## With non-callable iterator + +```py +class NotIterable: + if flag: + __iter__ = 1 + else: + __iter__ = None + +for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable" + pass + +reveal_type(x) # revealed: Unbound | Unknown +``` + +## Invalid iterable + +```py +nonsense = 123 +for x in nonsense: # error: "Object of type `Literal[123]` is not iterable" + pass +``` + +## New over old style iteration protocol + +```py +class NotIterable: + def __getitem__(self, key: int) -> int: + return 42 + + __iter__ = None + +for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable" + pass +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/iterators.md b/crates/red_knot_python_semantic/resources/mdtest/loops/iterators.md new file mode 100644 index 0000000000000..95ad5c448d247 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/iterators.md @@ -0,0 +1,18 @@ +# Iterators + +## Yield must be iterable + +```py +class NotIterable: pass + +class Iterator: + def __next__(self) -> int: + return 42 + +class Iterable: + def __iter__(self) -> Iterator: ... + +def generator_function(): + yield from Iterable() + yield from NotIterable() # error: "Object of type `NotIterable` is not 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 new file mode 100644 index 0000000000000..e75786f693fda --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/while_loop.md @@ -0,0 +1,43 @@ +# While loops + +## Basic While Loop + +```py +x = 1 +while flag: + x = 2 + +reveal_type(x) # revealed: Literal[1, 2] +``` + +## While with else (no break) + +```py +x = 1 +while flag: + x = 2 +else: + y = x + x = 3 + +reveal_type(x) # revealed: Literal[3] +reveal_type(y) # revealed: Literal[1, 2] +``` + +## While with Else (may break) + +```py +x = 1 +y = 0 +while flag: + x = 2 + if flag2: + y = 4 + break +else: + y = x + x = 3 + +reveal_type(x) # revealed: Literal[2, 3] +reveal_type(y) # revealed: Literal[1, 2, 4] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/not_none.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/not_none.md new file mode 100644 index 0000000000000..5faed9a52b3d3 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/not_none.md @@ -0,0 +1,9 @@ +# `is not None` narrowing + +```py +x = None if flag else 1 +if x is not None: + reveal_type(x) # revealed: Literal[1] + +reveal_type(x) # revealed: None | Literal[1] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/numbers.md b/crates/red_knot_python_semantic/resources/mdtest/numbers.md deleted file mode 100644 index 982cadc184ac5..0000000000000 --- a/crates/red_knot_python_semantic/resources/mdtest/numbers.md +++ /dev/null @@ -1,35 +0,0 @@ -# Numbers - -## Integers - -### Literals - -We can infer an integer literal type: - -```py -reveal_type(1) # revealed: Literal[1] -``` - -### Overflow - -We only track integer literals within the range of an i64: - -```py -reveal_type(9223372036854775808) # revealed: int -``` - -## Floats - -There aren't literal float types, but we infer the general float type: - -```py -reveal_type(1.0) # revealed: float -``` - -## Complex - -Same for complex: - -```py -reveal_type(2j) # revealed: complex -``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/shadowing/class.md b/crates/red_knot_python_semantic/resources/mdtest/shadowing/class.md new file mode 100644 index 0000000000000..feae514a2146e --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/shadowing/class.md @@ -0,0 +1,17 @@ +# Classes shadowing + +## Implicit error + +```py +class C: pass +C = 1 # error: "Implicit shadowing of class `C`; annotate to make it explicit if this is intentional" +``` + +## Explicit + +No diagnostic is raised in the case of explicit shadowing: + +```py +class C: pass +C: int = 1 +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/shadowing/function.md b/crates/red_knot_python_semantic/resources/mdtest/shadowing/function.md new file mode 100644 index 0000000000000..90e2e3ec523bd --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/shadowing/function.md @@ -0,0 +1,24 @@ +# Function shadowing + +## Parameter + +Parameter `x` of type `str` is shadowed and reassigned with a new `int` value inside the function. No diagnostics should be generated. + +```py path=a.py +def f(x: str): + x: int = int(x) +``` + +## Implicit error + +```py path=a.py +def f(): pass +f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional" +``` + +## Explicit shadowing + +```py path=a.py +def f(): pass +f: int = 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 new file mode 100644 index 0000000000000..a896987baa750 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/shadowing/variable_declaration.md @@ -0,0 +1,11 @@ +# Shadwing declaration + +## Shadow after incompatible declarations is OK + +```py +if flag: + x: str +else: + x: int +x: bytes = b'foo' +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md b/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md new file mode 100644 index 0000000000000..94cb3b158c6ed --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/stubs/class.md @@ -0,0 +1,10 @@ +# Class defenitions in stubs + +## Cyclical class definition + +In type stubs, classes can reference themselves in their base class definitions. For example, in `typeshed`, we have `class str(Sequence[str]): ...`. + +```py path=a.pyi +class C(C): ... +reveal_type(C) # revealed: Literal[C] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md new file mode 100644 index 0000000000000..af8fb7a424f3b --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md @@ -0,0 +1,15 @@ +# Bytes subscript + +## Simple + +```py +w = b'red' b'knot' +x = b'hello' +y = b'world' + b'!' +z = b'\xff\x00' + +reveal_type(w) # revealed: Literal[b"redknot"] +reveal_type(x) # revealed: Literal[b"hello"] +reveal_type(y) # revealed: Literal[b"world!"] +reveal_type(z) # revealed: Literal[b"\xff\x00"] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md new file mode 100644 index 0000000000000..e9c28dff235dd --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/class.md @@ -0,0 +1,92 @@ +# Class subscript + +## Class getitem unbound + +```py +class NotSubscriptable: pass +a = NotSubscriptable[0] # error: "Cannot subscript object of type `Literal[NotSubscriptable]` with no `__class_getitem__` method" +``` + +## Class getitem + +```py +class Identity: + def __class_getitem__(cls, item: int) -> str: + return item + +a = Identity[0] +reveal_type(a) # revealed: str +``` + +## Class getitem union + +```py +flag = True + +class Identity: + if flag: + def __class_getitem__(cls, item: int) -> str: + return item + else: + def __class_getitem__(cls, item: int) -> int: + return item + +a = Identity[0] +reveal_type(a) # revealed: str | int +``` + +## Class getitem with class union + +```py +flag = True + +class Identity1: + def __class_getitem__(cls, item: int) -> str: + return item + +class Identity2: + def __class_getitem__(cls, item: int) -> int: + return item + +if flag: + a = Identity1 +else: + a = Identity2 + +b = a[0] +reveal_type(a) # revealed: Literal[Identity1, Identity2] +reveal_type(b) # revealed: str | int +``` + +## Class getitem with unbound method union + +```py +flag = True + +if flag: + class Identity: + def __class_getitem__(self, x: int) -> str: + pass +else: + class Identity: pass + +a = Identity[42] # error: [call-non-callable] "Method `__class_getitem__` of type `Literal[__class_getitem__] | Unbound` is not callable on object of type `Literal[Identity, Identity]`" +reveal_type(a) # revealed: str | Unknown +``` + +## TODO: Class getitem non-class union + +```py +flag = True + +if flag: + class Identity: + def __class_getitem__(self, x: int) -> str: + pass +else: + Identity = 1 + +a = Identity[42] # error: "Cannot subscript object of type `Literal[Identity] | Literal[1]` with no `__getitem__` method" +# TODO: should _probably_ emit `str | Unknown` +reveal_type(a) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/instance.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/instance.md new file mode 100644 index 0000000000000..a363f0fb3cc4c --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/instance.md @@ -0,0 +1,45 @@ +# Instance subscript + +## Getitem unbound + +```py +class NotSubscriptable: pass +a = NotSubscriptable()[0] # error: "Cannot subscript object of type `NotSubscriptable` with no `__getitem__` method" +``` + +## Getitem not callable + +```py +class NotSubscriptable: + __getitem__ = None + +a = NotSubscriptable()[0] # error: "Method `__getitem__` of type `None` is not callable on object of type `NotSubscriptable`" +``` + +## Valid getitem + +```py +class Identity: + def __getitem__(self, index: int) -> int: + return index + +a = Identity()[0] +reveal_type(a) # revealed: int +``` + +## Getitem union + +```py +flag = True + +class Identity: + if flag: + def __getitem__(self, index: int) -> int: + return index + else: + def __getitem__(self, index: int) -> str: + return str(index) + +a = Identity()[0] +reveal_type(a) # revealed: int | str +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md new file mode 100644 index 0000000000000..f4bee2b32e300 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md @@ -0,0 +1,32 @@ +# Subscript on strings + +## Simple + +```py +s = 'abcde' + +a = s[0] +b = s[1] +c = s[-1] +d = s[-2] +e = s[8] # error: [index-out-of-bounds] "Index 8 is out of bounds for string `Literal["abcde"]` with length 5" +f = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5" + +reveal_type(a) # revealed: Literal["a"] +reveal_type(b) # revealed: Literal["b"] +reveal_type(c) # revealed: Literal["e"] +reveal_type(d) # revealed: Literal["d"] +reveal_type(e) # revealed: Unknown +reveal_type(f) # revealed: Unknown +``` + +## Function return + +```py +def add(x: int, y: int) -> int: + return x + y + +a = 'abcde'[add(0, 1)] +# TODO: Support overloads... Should be `str` +reveal_type(a) # revealed: @Todo +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md new file mode 100644 index 0000000000000..f837d989d2be6 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md @@ -0,0 +1,21 @@ +# Tuple subscripts + +## Basic + +```py +t = (1, 'a', 'b') + +a = t[0] +b = t[1] +c = t[-1] +d = t[-2] +e = t[4] # error: [index-out-of-bounds] +f = t[-4] # error: [index-out-of-bounds] + +reveal_type(a) # revealed: Literal[1] +reveal_type(b) # revealed: Literal["a"] +reveal_type(c) # revealed: Literal["b"] +reveal_type(d) # revealed: Literal["a"] +reveal_type(e) # revealed: Unknown +reveal_type(f) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/integers.md b/crates/red_knot_python_semantic/resources/mdtest/unary/integers.md new file mode 100644 index 0000000000000..04878aa68bdde --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/unary/integers.md @@ -0,0 +1,37 @@ +# Unary Operations + +## Unary Addition + +```py +a = +0 +b = +1 +c = +True + +reveal_type(a) # revealed: Literal[0] +reveal_type(b) # revealed: Literal[1] +reveal_type(c) # revealed: Literal[1] +``` + +## Unary Subtraction + +```py +a = -0 +b = -1 +c = -True + +reveal_type(a) # revealed: Literal[0] +reveal_type(b) # revealed: Literal[-1] +reveal_type(c) # revealed: Literal[-1] +``` + +## Unary Bitwise Inversion + +```py +a = ~0 +b = ~1 +c = ~True + +reveal_type(a) # revealed: Literal[-1] +reveal_type(b) # revealed: Literal[-2] +reveal_type(c) # revealed: Literal[-2] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/not.md b/crates/red_knot_python_semantic/resources/mdtest/unary/not.md new file mode 100644 index 0000000000000..db959a686a219 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/unary/not.md @@ -0,0 +1,149 @@ +# Unary not + +## None + +```py +a = not None +b = not not None +reveal_type(a) # revealed: Literal[True] +reveal_type(b) # revealed: Literal[False] +``` + +## Function + +```py +from typing import reveal_type + +def f(): + return 1 + +a = not f +b = not reveal_type + +reveal_type(a) # revealed: Literal[False] +# TODO Unknown should not be part of the type of typing.reveal_type +# reveal_type(b) revealed: Literal[False] +``` + +## Module + +```py +import b; import warnings + +x = not b +z = not warnings + +reveal_type(x) # revealed: Literal[False] +reveal_type(z) # revealed: Literal[False] +``` + +```py path=b.py +y = 1 +``` + +## Union + +```py +if flag: + p = 1 + q = 3.3 + r = "hello" + s = "world" + t = 0 +else: + p = "hello" + q = 4 + r = "" + s = 0 + t = "" + +a = not p +b = not q +c = not r +d = not s +e = not t + +reveal_type(a) # revealed: Literal[False] +reveal_type(b) # revealed: bool +reveal_type(c) # revealed: bool +reveal_type(d) # revealed: bool +reveal_type(e) # revealed: Literal[True] +``` + +## Integer literal + +```py +a = not 1 +b = not 1234567890987654321 +e = not 0 +x = not -1 +y = not -1234567890987654321 +z = not --987 + +reveal_type(a) # revealed: Literal[False] +reveal_type(b) # revealed: Literal[False] +reveal_type(e) # revealed: Literal[True] +reveal_type(x) # revealed: Literal[False] +reveal_type(y) # revealed: Literal[False] +reveal_type(z) # revealed: Literal[False] +``` + +## Boolean literal + +```py +w = True +x = False +y = not w +z = not x + +reveal_type(w) # revealed: Literal[True] +reveal_type(x) # revealed: Literal[False] +reveal_type(y) # revealed: Literal[False] +reveal_type(z) # revealed: Literal[True] +``` + +## String literal + +```py +a = not "hello" +b = not "" +c = not "0" +d = not "hello" + "world" + +reveal_type(a) # revealed: Literal[False] +reveal_type(b) # revealed: Literal[True] +reveal_type(c) # revealed: Literal[False] +reveal_type(d) # revealed: Literal[False] +``` + +## Bytes literal + +```py +a = not b"hello" +b = not b"" +c = not b"0" +d = not b"hello" + b"world" + +reveal_type(a) # revealed: Literal[False] +reveal_type(b) # revealed: Literal[True] +reveal_type(c) # revealed: Literal[False] +reveal_type(d) # revealed: Literal[False] +``` + +## Tuple + +```py +a = not (1,) +b = not (1, 2) +c = not (1, 2, 3) +d = not () +e = not ("hello",) +f = not (1, "hello") + +reveal_type(a) # revealed: Literal[False] +reveal_type(b) # revealed: Literal[False] +reveal_type(c) # revealed: Literal[False] +reveal_type(d) # revealed: Literal[True] +reveal_type(e) # revealed: Literal[False] +reveal_type(f) # revealed: Literal[False] +``` diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 3af5d9775c003..24d292af63067 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -3282,7 +3282,6 @@ mod tests { use crate::semantic_index::definition::Definition; use crate::semantic_index::symbol::FileScopeId; use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map}; - use crate::stdlib::builtins_module_scope; use crate::types::{ check_types, global_symbol_ty, infer_definition_types, symbol_ty, TypeCheckDiagnostics, }; @@ -3392,4413 +3391,1095 @@ mod tests { } #[test] - fn reveal_type() -> anyhow::Result<()> { + fn from_import_with_no_module_name() -> anyhow::Result<()> { + // This test checks that invalid syntax in a `StmtImportFrom` node + // leads to the type being inferred as `Unknown` + let mut db = setup_db(); + db.write_file("src/foo.py", "from import bar")?; + assert_public_ty(&db, "src/foo.py", "bar", "Unknown"); + Ok(()) + } + + #[test] + fn resolve_base_class_by_name() -> anyhow::Result<()> { let mut db = setup_db(); db.write_dedented( - "/src/a.py", + "src/mod.py", " - from typing import reveal_type + class Base: + pass - x = 1 - reveal_type(x) + class Sub(Base): + pass ", )?; - assert_file_diagnostics(&db, "/src/a.py", &["Revealed type is `Literal[1]`"]); + let mod_file = system_path_to_file(&db, "src/mod.py").expect("file to exist"); + let ty = global_symbol_ty(&db, mod_file, "Sub"); + + let class = ty.expect_class(); + + let base_names: Vec<_> = class + .bases(&db) + .map(|base_ty| format!("{}", base_ty.display(&db))) + .collect(); + + assert_eq!(base_names, vec!["Literal[Base]"]); Ok(()) } #[test] - fn reveal_type_aliased() -> anyhow::Result<()> { + fn resolve_method() -> anyhow::Result<()> { let mut db = setup_db(); db.write_dedented( - "/src/a.py", + "src/mod.py", " - from typing import reveal_type as rt - - x = 1 - rt(x) + class C: + def f(self): pass ", )?; - assert_file_diagnostics(&db, "/src/a.py", &["Revealed type is `Literal[1]`"]); + let mod_file = system_path_to_file(&db, "src/mod.py").unwrap(); + let ty = global_symbol_ty(&db, mod_file, "C"); + let class_id = ty.expect_class(); + let member_ty = class_id.class_member(&db, &Name::new_static("f")); + let func = member_ty.expect_function(); + assert_eq!(func.name(&db), "f"); Ok(()) } #[test] - fn reveal_type_typing_extensions() -> anyhow::Result<()> { + fn not_literal_string() -> anyhow::Result<()> { let mut db = setup_db(); + let content = format!( + r#" + v = not "{y}" + w = not 10*"{y}" + x = not "{y}"*10 + z = not 0*"{y}" + u = not (-100)*"{y}" + "#, + y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), + ); + db.write_dedented("src/a.py", &content)?; - db.write_dedented( - "/src/a.py", - " - import typing_extensions - - x = 1 - typing_extensions.reveal_type(x) - ", - )?; - - assert_file_diagnostics(&db, "/src/a.py", &["Revealed type is `Literal[1]`"]); + assert_public_ty(&db, "src/a.py", "v", "bool"); + assert_public_ty(&db, "src/a.py", "w", "bool"); + assert_public_ty(&db, "src/a.py", "x", "bool"); + assert_public_ty(&db, "src/a.py", "z", "Literal[True]"); + assert_public_ty(&db, "src/a.py", "u", "Literal[True]"); Ok(()) } #[test] - fn reveal_type_builtin() -> anyhow::Result<()> { + fn multiplied_string() -> anyhow::Result<()> { let mut db = setup_db(); db.write_dedented( - "/src/a.py", - " - x = 1 - reveal_type(x) - ", + "src/a.py", + &format!( + r#" + w = 2 * "hello" + x = "goodbye" * 3 + y = "a" * {y} + z = {z} * "b" + a = 0 * "hello" + b = -3 * "hello" + "#, + y = TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE, + z = TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1 + ), )?; - assert_file_diagnostics( + assert_public_ty(&db, "src/a.py", "w", r#"Literal["hellohello"]"#); + assert_public_ty(&db, "src/a.py", "x", r#"Literal["goodbyegoodbyegoodbye"]"#); + assert_public_ty( &db, - "/src/a.py", - &[ - "`reveal_type` used without importing it; this is allowed for debugging convenience but will fail at runtime", - "Revealed type is `Literal[1]`", - ], + "src/a.py", + "y", + &format!( + r#"Literal["{}"]"#, + "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE) + ), ); + assert_public_ty(&db, "src/a.py", "z", "LiteralString"); + assert_public_ty(&db, "src/a.py", "a", r#"Literal[""]"#); + assert_public_ty(&db, "src/a.py", "b", r#"Literal[""]"#); Ok(()) } #[test] - fn follow_import_to_class() -> anyhow::Result<()> { + fn multiplied_literal_string() -> anyhow::Result<()> { let mut db = setup_db(); + let content = format!( + r#" + v = "{y}" + w = 10*"{y}" + x = "{y}"*10 + z = 0*"{y}" + u = (-100)*"{y}" + "#, + y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), + ); + db.write_dedented("src/a.py", &content)?; - db.write_files([ - ("src/a.py", "from b import C as D; E = D"), - ("src/b.py", "class C: pass"), - ])?; - - assert_public_ty(&db, "src/a.py", "E", "Literal[C]"); - + assert_public_ty(&db, "src/a.py", "v", "LiteralString"); + assert_public_ty(&db, "src/a.py", "w", "LiteralString"); + assert_public_ty(&db, "src/a.py", "x", "LiteralString"); + assert_public_ty(&db, "src/a.py", "z", r#"Literal[""]"#); + assert_public_ty(&db, "src/a.py", "u", r#"Literal[""]"#); Ok(()) } #[test] - fn follow_relative_import_simple() -> anyhow::Result<()> { + fn truncated_string_literals_become_literal_string() -> anyhow::Result<()> { let mut db = setup_db(); + let content = format!( + r#" + w = "{y}" + x = "a" + "{z}" + "#, + y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), + z = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE), + ); + db.write_dedented("src/a.py", &content)?; - db.write_files([ - ("src/package/__init__.py", ""), - ("src/package/foo.py", "X = 42"), - ("src/package/bar.py", "from .foo import X"), - ])?; - - assert_public_ty(&db, "src/package/bar.py", "X", "Literal[42]"); + assert_public_ty(&db, "src/a.py", "w", "LiteralString"); + assert_public_ty(&db, "src/a.py", "x", "LiteralString"); Ok(()) } #[test] - fn follow_nonexistent_relative_import_simple() -> anyhow::Result<()> { + fn adding_string_literals_and_literal_string() -> anyhow::Result<()> { let mut db = setup_db(); + let content = format!( + r#" + v = "{y}" + w = "{y}" + "a" + x = "a" + "{y}" + z = "{y}" + "{y}" + "#, + y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), + ); + db.write_dedented("src/a.py", &content)?; - db.write_files([ - ("src/package/__init__.py", ""), - ("src/package/bar.py", "from .foo import X"), - ])?; - - assert_public_ty(&db, "src/package/bar.py", "X", "Unknown"); + assert_public_ty(&db, "src/a.py", "v", "LiteralString"); + assert_public_ty(&db, "src/a.py", "w", "LiteralString"); + assert_public_ty(&db, "src/a.py", "x", "LiteralString"); + assert_public_ty(&db, "src/a.py", "z", "LiteralString"); Ok(()) } #[test] - fn follow_relative_import_dotted() -> anyhow::Result<()> { + fn bytes_type() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_files([ - ("src/package/__init__.py", ""), - ("src/package/foo/bar/baz.py", "X = 42"), - ("src/package/bar.py", "from .foo.bar.baz import X"), - ])?; + db.write_dedented( + "src/a.py", + " + w = b'red' b'knot' + x = b'hello' + y = b'world' + b'!' + z = b'\\xff\\x00' + ", + )?; - assert_public_ty(&db, "src/package/bar.py", "X", "Literal[42]"); + assert_public_ty(&db, "src/a.py", "w", "Literal[b\"redknot\"]"); + assert_public_ty(&db, "src/a.py", "x", "Literal[b\"hello\"]"); + assert_public_ty(&db, "src/a.py", "y", "Literal[b\"world!\"]"); + assert_public_ty(&db, "src/a.py", "z", "Literal[b\"\\xff\\x00\"]"); Ok(()) } #[test] - fn follow_relative_import_bare_to_package() -> anyhow::Result<()> { + fn ellipsis_type() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_files([ - ("src/package/__init__.py", "X = 42"), - ("src/package/bar.py", "from . import X"), - ])?; - - assert_public_ty(&db, "src/package/bar.py", "X", "Literal[42]"); + db.write_dedented( + "src/a.py", + " + x = ... + ", + )?; - Ok(()) - } + // TODO: sys.version_info, and need to understand @final and @type_check_only + assert_public_ty(&db, "src/a.py", "x", "Unknown | EllipsisType"); - #[test] - fn follow_nonexistent_relative_import_bare_to_package() -> anyhow::Result<()> { - let mut db = setup_db(); - db.write_files([("src/package/bar.py", "from . import X")])?; - assert_public_ty(&db, "src/package/bar.py", "X", "Unknown"); Ok(()) } - #[ignore = "TODO: Submodule imports possibly not supported right now?"] #[test] - fn follow_relative_import_bare_to_module() -> anyhow::Result<()> { + fn function_return_type() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_files([ - ("src/package/__init__.py", ""), - ("src/package/foo.py", "X = 42"), - ("src/package/bar.py", "from . import foo; y = foo.X"), - ])?; + db.write_file("src/a.py", "def example() -> int: return 42")?; - assert_public_ty(&db, "src/package/bar.py", "y", "Literal[42]"); + let mod_file = system_path_to_file(&db, "src/a.py").unwrap(); + let function = global_symbol_ty(&db, mod_file, "example").expect_function(); + let returns = function.return_type(&db); + assert_eq!(returns.display(&db).to_string(), "int"); Ok(()) } - #[ignore = "TODO: Submodule imports possibly not supported right now?"] #[test] - fn follow_nonexistent_import_bare_to_module() -> anyhow::Result<()> { + fn import_cycle() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_files([ - ("src/package/__init__.py", ""), - ("src/package/bar.py", "from . import foo"), - ])?; + db.write_dedented( + "src/a.py", + " + class A: pass + import b + class C(b.B): pass + ", + )?; + db.write_dedented( + "src/b.py", + " + from a import A + class B(A): pass + ", + )?; - assert_public_ty(&db, "src/package/bar.py", "foo", "Unknown"); + let a = system_path_to_file(&db, "src/a.py").expect("file to exist"); + let c_ty = global_symbol_ty(&db, a, "C"); + let c_class = c_ty.expect_class(); + let mut c_bases = c_class.bases(&db); + let b_ty = c_bases.next().unwrap(); + let b_class = b_ty.expect_class(); + assert_eq!(b_class.name(&db), "B"); + let mut b_bases = b_class.bases(&db); + let a_ty = b_bases.next().unwrap(); + let a_class = a_ty.expect_class(); + assert_eq!(a_class.name(&db), "A"); Ok(()) } + /// An unbound function local that has definitions in the scope does not fall back to globals. #[test] - fn follow_relative_import_from_dunder_init() -> anyhow::Result<()> { + fn unbound_function_local() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_files([ - ("src/package/__init__.py", "from .foo import X"), - ("src/package/foo.py", "X = 42"), - ])?; + db.write_dedented( + "src/a.py", + " + x = 1 + def f(): + y = x + x = 2 + ", + )?; - assert_public_ty(&db, "src/package/__init__.py", "X", "Literal[42]"); + let file = system_path_to_file(&db, "src/a.py").expect("file to exist"); + let index = semantic_index(&db, file); + let function_scope = index + .child_scopes(FileScopeId::global()) + .next() + .unwrap() + .0 + .to_scope_id(&db, file); + let y_ty = symbol_ty(&db, function_scope, "y"); + let x_ty = symbol_ty(&db, function_scope, "x"); - Ok(()) - } + assert_eq!(y_ty.display(&db).to_string(), "Unbound"); + assert_eq!(x_ty.display(&db).to_string(), "Literal[2]"); - #[test] - fn follow_nonexistent_relative_import_from_dunder_init() -> anyhow::Result<()> { - let mut db = setup_db(); - db.write_files([("src/package/__init__.py", "from .foo import X")])?; - assert_public_ty(&db, "src/package/__init__.py", "X", "Unknown"); Ok(()) } + /// A name reference to a never-defined symbol in a function is implicitly a global lookup. #[test] - fn follow_very_relative_import() -> anyhow::Result<()> { + fn implicit_global_in_function() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_files([ - ("src/package/__init__.py", ""), - ("src/package/foo.py", "X = 42"), - ( - "src/package/subpackage/subsubpackage/bar.py", - "from ...foo import X", - ), - ])?; + db.write_dedented( + "src/a.py", + " + x = 1 + def f(): + y = x + ", + )?; - assert_public_ty( - &db, - "src/package/subpackage/subsubpackage/bar.py", - "X", - "Literal[42]", - ); + let file = system_path_to_file(&db, "src/a.py").expect("file to exist"); + let index = semantic_index(&db, file); + let function_scope = index + .child_scopes(FileScopeId::global()) + .next() + .unwrap() + .0 + .to_scope_id(&db, file); + let y_ty = symbol_ty(&db, function_scope, "y"); + let x_ty = symbol_ty(&db, function_scope, "x"); + + assert_eq!(x_ty.display(&db).to_string(), "Unbound"); + assert_eq!(y_ty.display(&db).to_string(), "Literal[1]"); Ok(()) } #[test] - fn imported_unbound_symbol_is_unknown() -> anyhow::Result<()> { + fn conditionally_global_or_builtin() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_files([ - ("src/package/__init__.py", ""), - ("src/package/foo.py", "x"), - ("src/package/bar.py", "from package.foo import x"), - ])?; + db.write_dedented( + "/src/a.py", + " + if flag: + copyright = 1 + def f(): + y = copyright + ", + )?; + + let file = system_path_to_file(&db, "src/a.py").expect("file to exist"); + let index = semantic_index(&db, file); + let function_scope = index + .child_scopes(FileScopeId::global()) + .next() + .unwrap() + .0 + .to_scope_id(&db, file); + let y_ty = symbol_ty(&db, function_scope, "y"); - // the type as seen from external modules (`Unknown`) - // is different from the type inside the module itself (`Unbound`): - assert_public_ty(&db, "src/package/foo.py", "x", "Unbound"); - assert_public_ty(&db, "src/package/bar.py", "x", "Unknown"); + assert_eq!( + y_ty.display(&db).to_string(), + "Literal[copyright] | Literal[1]" + ); Ok(()) } #[test] - fn from_import_with_no_module_name() -> anyhow::Result<()> { - // This test checks that invalid syntax in a `StmtImportFrom` node - // leads to the type being inferred as `Unknown` + fn local_inference() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_file("src/foo.py", "from import bar")?; - assert_public_ty(&db, "src/foo.py", "bar", "Unknown"); + + db.write_file("/src/a.py", "x = 10")?; + let a = system_path_to_file(&db, "/src/a.py").unwrap(); + + let parsed = parsed_module(&db, a); + + let statement = parsed.suite().first().unwrap().as_assign_stmt().unwrap(); + let model = SemanticModel::new(&db, a); + + let literal_ty = statement.value.ty(&model); + + assert_eq!(format!("{}", literal_ty.display(&db)), "Literal[10]"); + Ok(()) } #[test] - fn resolve_base_class_by_name() -> anyhow::Result<()> { + fn builtin_symbol_vendored_stdlib() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_dedented( - "src/mod.py", - " - class Base: - pass - - class Sub(Base): - pass - ", - )?; + db.write_file("/src/a.py", "c = copyright")?; - let mod_file = system_path_to_file(&db, "src/mod.py").expect("file to exist"); - let ty = global_symbol_ty(&db, mod_file, "Sub"); + assert_public_ty(&db, "/src/a.py", "c", "Literal[copyright]"); - let class = ty.expect_class(); + Ok(()) + } - let base_names: Vec<_> = class - .bases(&db) - .map(|base_ty| format!("{}", base_ty.display(&db))) - .collect(); + #[test] + fn builtin_symbol_custom_stdlib() -> anyhow::Result<()> { + let db = setup_db_with_custom_typeshed( + "/typeshed", + [ + ("/src/a.py", "c = copyright"), + ( + "/typeshed/stdlib/builtins.pyi", + "def copyright() -> None: ...", + ), + ("/typeshed/stdlib/VERSIONS", "builtins: 3.8-"), + ], + )?; - assert_eq!(base_names, vec!["Literal[Base]"]); + assert_public_ty(&db, "/src/a.py", "c", "Literal[copyright]"); Ok(()) } #[test] - fn resolve_method() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/mod.py", - " - class C: - def f(self): pass - ", + fn unknown_builtin_later_defined() -> anyhow::Result<()> { + let db = setup_db_with_custom_typeshed( + "/typeshed", + [ + ("/src/a.py", "x = foo"), + ("/typeshed/stdlib/builtins.pyi", "foo = bar; bar = 1"), + ("/typeshed/stdlib/VERSIONS", "builtins: 3.8-"), + ], )?; - let mod_file = system_path_to_file(&db, "src/mod.py").unwrap(); - let ty = global_symbol_ty(&db, mod_file, "C"); - let class_id = ty.expect_class(); - let member_ty = class_id.class_member(&db, &Name::new_static("f")); - let func = member_ty.expect_function(); + assert_public_ty(&db, "/src/a.py", "x", "Unbound"); - assert_eq!(func.name(&db), "f"); Ok(()) } #[test] - fn resolve_module_member() -> anyhow::Result<()> { + fn str_builtin() -> anyhow::Result<()> { let mut db = setup_db(); + db.write_file("/src/a.py", "x = str")?; + assert_public_ty(&db, "/src/a.py", "x", "Literal[str]"); + Ok(()) + } - db.write_files([ - ("src/a.py", "import b; D = b.C"), - ("src/b.py", "class C: pass"), - ])?; + #[test] + fn deferred_annotation_builtin() -> anyhow::Result<()> { + let mut db = setup_db(); + db.write_file("/src/a.pyi", "class C(object): pass")?; + let file = system_path_to_file(&db, "/src/a.pyi").unwrap(); + let ty = global_symbol_ty(&db, file, "C"); + + let base = ty + .expect_class() + .bases(&db) + .next() + .expect("there should be at least one base"); - assert_public_ty(&db, "src/a.py", "D", "Literal[C]"); + assert_eq!(base.display(&db).to_string(), "Literal[object]"); Ok(()) } #[test] - fn negated_int_literal() -> anyhow::Result<()> { + fn deferred_annotation_in_stubs_always_resolve() -> anyhow::Result<()> { let mut db = setup_db(); + // Stub files should always resolve deferred annotations db.write_dedented( - "src/a.py", + "/src/stub.pyi", " - x = -1 - y = -1234567890987654321 - z = --987 + def get_foo() -> Foo: ... + class Foo: ... + foo = get_foo() ", )?; - - assert_public_ty(&db, "src/a.py", "x", "Literal[-1]"); - assert_public_ty(&db, "src/a.py", "y", "Literal[-1234567890987654321]"); - assert_public_ty(&db, "src/a.py", "z", "Literal[987]"); + assert_public_ty(&db, "/src/stub.pyi", "foo", "Foo"); Ok(()) } #[test] - fn boolean_literal() -> anyhow::Result<()> { + fn deferred_annotations_regular_source_fails() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_file("src/a.py", "x = True\ny = False")?; - - assert_public_ty(&db, "src/a.py", "x", "Literal[True]"); - assert_public_ty(&db, "src/a.py", "y", "Literal[False]"); + // In (regular) source files, deferred annotations are *not* resolved + // Also tests imports from `__future__` that are not annotations + db.write_dedented( + "/src/source.py", + " + from __future__ import with_statement as annotations + def get_foo() -> Foo: ... + class Foo: ... + foo = get_foo() + ", + )?; + assert_public_ty(&db, "/src/source.py", "foo", "Unknown"); Ok(()) } #[test] - fn not_none_literal() -> anyhow::Result<()> { + fn deferred_annotation_in_sources_with_future_resolves() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_file( - "src/a.py", - r#" - a = not None - b = not not None - "#, + // In source files with `__future__.annotations`, deferred annotations are resolved + db.write_dedented( + "/src/source_with_future.py", + " + from __future__ import annotations + def get_foo() -> Foo: ... + class Foo: ... + foo = get_foo() + ", )?; - assert_public_ty(&db, "src/a.py", "a", "Literal[True]"); - assert_public_ty(&db, "src/a.py", "b", "Literal[False]"); + assert_public_ty(&db, "/src/source_with_future.py", "foo", "Foo"); Ok(()) } #[test] - fn not_function() -> anyhow::Result<()> { + fn nonlocal_name_reference() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_file( - "src/a.py", - r#" - from typing import reveal_type + db.write_dedented( + "/src/a.py", + " def f(): - return 1 - - a = not f - b = not reveal_type - "#, + x = 1 + def g(): + y = x + ", )?; - assert_public_ty(&db, "src/a.py", "a", "Literal[False]"); - // TODO Unknown should not be part of the type of typing.reveal_type - // assert_public_ty(&db, "src/a.py", "b", "Literal[False]"); + assert_scope_ty(&db, "/src/a.py", &["f", "g"], "y", "Literal[1]"); + Ok(()) } #[test] - fn not_module() -> anyhow::Result<()> { + fn nonlocal_name_reference_multi_level() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_files([ - ( - "src/a.py", - "import b; import warnings; - x = not b; - z = not warnings", - ), - ("src/b.py", "y = 1"), - ])?; + db.write_dedented( + "/src/a.py", + " + def f(): + x = 1 + def g(): + def h(): + y = x + ", + )?; - assert_public_ty(&db, "src/a.py", "x", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "z", "Literal[False]"); + assert_scope_ty(&db, "/src/a.py", &["f", "g", "h"], "y", "Literal[1]"); Ok(()) } #[test] - fn not_union() -> anyhow::Result<()> { + fn nonlocal_name_reference_skips_class_scope() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_file( - "src/a.py", - r#" - if flag: - p = 1 - q = 3.3 - r = "hello" - s = "world" - t = 0 - else: - p = "hello" - q = 4 - r = "" - s = 0 - t = "" - - a = not p - b = not q - c = not r - d = not s - e = not t - "#, + db.write_dedented( + "/src/a.py", + " + def f(): + x = 1 + class C: + x = 2 + def g(): + y = x + ", )?; - assert_public_ty(&db, "src/a.py", "a", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "b", "bool"); - assert_public_ty(&db, "src/a.py", "c", "bool"); - assert_public_ty(&db, "src/a.py", "d", "bool"); - assert_public_ty(&db, "src/a.py", "e", "Literal[True]"); + assert_scope_ty(&db, "/src/a.py", &["f", "C", "g"], "y", "Literal[1]"); Ok(()) } #[test] - fn not_integer_literal() -> anyhow::Result<()> { + fn nonlocal_name_reference_skips_annotation_only_assignment() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_file( - "src/a.py", - r#" - a = not 1 - b = not 1234567890987654321 - e = not 0 - x = not -1 - y = not -1234567890987654321 - z = not --987 - "#, + db.write_dedented( + "/src/a.py", + " + def f(): + x = 1 + def g(): + // it's pretty weird to have an annotated assignment in a function where the + // name is otherwise not defined; maybe should be an error? + x: int + def h(): + y = x + ", )?; - assert_public_ty(&db, "src/a.py", "a", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "b", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "e", "Literal[True]"); - assert_public_ty(&db, "src/a.py", "x", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "y", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "z", "Literal[False]"); + + assert_scope_ty(&db, "/src/a.py", &["f", "g", "h"], "y", "Literal[1]"); Ok(()) } #[test] - fn not_boolean_literal() -> anyhow::Result<()> { + fn exception_handler_with_invalid_syntax() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_file( + db.write_dedented( "src/a.py", - r#" - w = True - x = False - y = not w - z = not x - - "#, - )?; - assert_public_ty(&db, "src/a.py", "w", "Literal[True]"); - assert_public_ty(&db, "src/a.py", "x", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "y", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "z", "Literal[True]"); - - Ok(()) - } - - #[test] - fn not_string_literal() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_file( - "src/a.py", - r#" - a = not "hello" - b = not "" - c = not "0" - d = not "hello" + "world" - "#, - )?; - assert_public_ty(&db, "src/a.py", "a", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "b", "Literal[True]"); - assert_public_ty(&db, "src/a.py", "c", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "d", "Literal[False]"); - - Ok(()) - } - - #[test] - fn not_literal_string() -> anyhow::Result<()> { - let mut db = setup_db(); - let content = format!( - r#" - v = not "{y}" - w = not 10*"{y}" - x = not "{y}"*10 - z = not 0*"{y}" - u = not (-100)*"{y}" - "#, - y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), - ); - db.write_dedented("src/a.py", &content)?; - - assert_public_ty(&db, "src/a.py", "v", "bool"); - assert_public_ty(&db, "src/a.py", "w", "bool"); - assert_public_ty(&db, "src/a.py", "x", "bool"); - assert_public_ty(&db, "src/a.py", "z", "Literal[True]"); - assert_public_ty(&db, "src/a.py", "u", "Literal[True]"); - - Ok(()) - } - - #[test] - fn not_bytes_literal() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_file( - "src/a.py", - r#" - a = not b"hello" - b = not b"" - c = not b"0" - d = not b"hello" + b"world" - "#, - )?; - assert_public_ty(&db, "src/a.py", "a", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "b", "Literal[True]"); - assert_public_ty(&db, "src/a.py", "c", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "d", "Literal[False]"); - - Ok(()) - } - - #[test] - fn not_tuple() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_file( - "src/a.py", - r#" - a = not (1,) - b = not (1, 2) - c = not (1, 2, 3) - d = not () - e = not ("hello",) - f = not (1, "hello") - "#, - )?; - - assert_public_ty(&db, "src/a.py", "a", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "b", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "c", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "d", "Literal[True]"); - assert_public_ty(&db, "src/a.py", "e", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "f", "Literal[False]"); - - Ok(()) - } - - #[test] - fn string_type() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - r#" - w = "Hello" - x = 'world' - y = "Guten " + 'tag' - z = 'bon ' + "jour" - "#, - )?; - - assert_public_ty(&db, "src/a.py", "w", r#"Literal["Hello"]"#); - assert_public_ty(&db, "src/a.py", "x", r#"Literal["world"]"#); - assert_public_ty(&db, "src/a.py", "y", r#"Literal["Guten tag"]"#); - assert_public_ty(&db, "src/a.py", "z", r#"Literal["bon jour"]"#); - - Ok(()) - } - - #[test] - fn string_type_with_nested_quotes() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - r#" - x = 'I say "hello" to you' - y = "You say \"hey\" back" - z = 'No "closure here' - "#, - )?; - - assert_public_ty(&db, "src/a.py", "x", r#"Literal["I say \"hello\" to you"]"#); - assert_public_ty(&db, "src/a.py", "y", r#"Literal["You say \"hey\" back"]"#); - assert_public_ty(&db, "src/a.py", "z", r#"Literal["No \"closure here"]"#); - - Ok(()) - } - - #[test] - fn multiplied_string() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - &format!( - r#" - w = 2 * "hello" - x = "goodbye" * 3 - y = "a" * {y} - z = {z} * "b" - a = 0 * "hello" - b = -3 * "hello" - "#, - y = TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE, - z = TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1 - ), - )?; - - assert_public_ty(&db, "src/a.py", "w", r#"Literal["hellohello"]"#); - assert_public_ty(&db, "src/a.py", "x", r#"Literal["goodbyegoodbyegoodbye"]"#); - assert_public_ty( - &db, - "src/a.py", - "y", - &format!( - r#"Literal["{}"]"#, - "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE) - ), - ); - assert_public_ty(&db, "src/a.py", "z", "LiteralString"); - assert_public_ty(&db, "src/a.py", "a", r#"Literal[""]"#); - assert_public_ty(&db, "src/a.py", "b", r#"Literal[""]"#); - - Ok(()) - } - - #[test] - fn multiplied_literal_string() -> anyhow::Result<()> { - let mut db = setup_db(); - let content = format!( - r#" - v = "{y}" - w = 10*"{y}" - x = "{y}"*10 - z = 0*"{y}" - u = (-100)*"{y}" - "#, - y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), - ); - db.write_dedented("src/a.py", &content)?; - - assert_public_ty(&db, "src/a.py", "v", "LiteralString"); - assert_public_ty(&db, "src/a.py", "w", "LiteralString"); - assert_public_ty(&db, "src/a.py", "x", "LiteralString"); - assert_public_ty(&db, "src/a.py", "z", r#"Literal[""]"#); - assert_public_ty(&db, "src/a.py", "u", r#"Literal[""]"#); - Ok(()) - } - - #[test] - fn truncated_string_literals_become_literal_string() -> anyhow::Result<()> { - let mut db = setup_db(); - let content = format!( - r#" - w = "{y}" - x = "a" + "{z}" - "#, - y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), - z = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE), - ); - db.write_dedented("src/a.py", &content)?; - - assert_public_ty(&db, "src/a.py", "w", "LiteralString"); - assert_public_ty(&db, "src/a.py", "x", "LiteralString"); - - Ok(()) - } - - #[test] - fn adding_string_literals_and_literal_string() -> anyhow::Result<()> { - let mut db = setup_db(); - let content = format!( - r#" - v = "{y}" - w = "{y}" + "a" - x = "a" + "{y}" - z = "{y}" + "{y}" - "#, - y = "a".repeat(TypeInferenceBuilder::MAX_STRING_LITERAL_SIZE + 1), - ); - db.write_dedented("src/a.py", &content)?; - - assert_public_ty(&db, "src/a.py", "v", "LiteralString"); - assert_public_ty(&db, "src/a.py", "w", "LiteralString"); - assert_public_ty(&db, "src/a.py", "x", "LiteralString"); - assert_public_ty(&db, "src/a.py", "z", "LiteralString"); - - Ok(()) - } - - #[test] - fn comparison_integer_literals() -> anyhow::Result<()> { - let mut db = setup_db(); - db.write_dedented( - "src/a.py", - r#" - a = 1 == 1 == True - b = 1 == 1 == 2 == 4 - c = False < True <= 2 < 3 != 6 - d = 1 < 1 - e = 1 > 1 - f = 1 is 1 - g = 1 is not 1 - h = 1 is 2 - i = 1 is not 7 - j = 1 <= "" and 0 < 1 - "#, - )?; - - assert_public_ty(&db, "src/a.py", "a", "Literal[True]"); - assert_public_ty(&db, "src/a.py", "b", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "c", "Literal[True]"); - assert_public_ty(&db, "src/a.py", "d", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "e", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "f", "bool"); - assert_public_ty(&db, "src/a.py", "g", "bool"); - assert_public_ty(&db, "src/a.py", "h", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "i", "Literal[True]"); - assert_public_ty(&db, "src/a.py", "j", "@Todo | Literal[True]"); - - Ok(()) - } - - #[test] - fn comparison_integer_instance() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - r#" - def int_instance() -> int: ... - a = 1 == int_instance() - b = 9 < int_instance() - c = int_instance() < int_instance() - "#, - )?; - - // TODO: implement lookup of `__eq__` on typeshed `int` stub - assert_public_ty(&db, "src/a.py", "a", "@Todo"); - assert_public_ty(&db, "src/a.py", "b", "bool"); - assert_public_ty(&db, "src/a.py", "c", "bool"); - - Ok(()) - } - - #[test] - fn comparison_string_literals() -> anyhow::Result<()> { - let mut db = setup_db(); - db.write_dedented( - "src/a.py", - r#" - def str_instance() -> str: ... - a = "abc" == "abc" - b = "ab_cd" <= "ab_ce" - c = "abc" in "ab cd" - d = "" not in "hello" - e = "--" is "--" - f = "A" is "B" - g = "--" is not "--" - h = "A" is not "B" - i = str_instance() < "..." - j = "ab" < "ab_cd" - "#, - )?; - - assert_public_ty(&db, "src/a.py", "a", "Literal[True]"); - assert_public_ty(&db, "src/a.py", "b", "Literal[True]"); - assert_public_ty(&db, "src/a.py", "c", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "d", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "e", "bool"); - assert_public_ty(&db, "src/a.py", "f", "Literal[False]"); - assert_public_ty(&db, "src/a.py", "g", "bool"); - assert_public_ty(&db, "src/a.py", "h", "Literal[True]"); - assert_public_ty(&db, "src/a.py", "i", "bool"); - // Very cornercase test ensuring we're not comparing the interned salsa symbols, which - // compare by order of declaration - assert_public_ty(&db, "src/a.py", "j", "Literal[True]"); - - Ok(()) - } - - #[test] - fn comparison_unsupported_operators() -> anyhow::Result<()> { - let mut db = setup_db(); - db.write_dedented( - "src/a.py", - r#" - a = 1 in 7 - b = 0 not in 10 - c = object() < 5 - d = 5 < object() - "#, - )?; - - assert_file_diagnostics( - &db, - "src/a.py", - &[ - "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`", - "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`", - "Operator `<` is not supported for types `object` and `Literal[5]`", - ], - ); - assert_public_ty(&db, "src/a.py", "a", "bool"); - assert_public_ty(&db, "src/a.py", "b", "bool"); - assert_public_ty(&db, "src/a.py", "c", "Unknown"); - // TODO: this should be `Unknown` but we don't check if __lt__ signature is valid for right - // operand type - assert_public_ty(&db, "src/a.py", "d", "bool"); - - Ok(()) - } - - #[test] - fn comparison_non_bool_returns() -> anyhow::Result<()> { - let mut db = setup_db(); - db.write_dedented( - "src/a.py", - r#" - from __future__ import annotations - class A: - def __lt__(self, other) -> A: ... - class B: - def __lt__(self, other) -> B: ... - class C: - def __lt__(self, other) -> C: ... - - a = A() < B() < C() - b = 0 < 1 < A() < 3 - c = 10 < 0 < A() < B() < C() - "#, - )?; - - // Walking through the example - // 1. A() < B() < C() - // 2. A() < B() and B() < C() - split in N comparison - // 3. A() and B() - evaluate outcome types - // 4. bool and bool - evaluate truthiness - // 5. A | B - union of "first true" types - assert_public_ty(&db, "src/a.py", "a", "A | B"); - // Walking through the example - // 1. 0 < 1 < A() < 3 - // 2. 0 < 1 and 1 < A() and A() < 3 - split in N comparison - // 3. True and bool and A - evaluate outcome types - // 4. True and bool and bool - evaluate truthiness - // 5. bool | A - union of "true" types - assert_public_ty(&db, "src/a.py", "b", "bool | A"); - // Short-cicuit to False - assert_public_ty(&db, "src/a.py", "c", "Literal[False]"); - - Ok(()) - } - - #[test] - fn bytes_type() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - w = b'red' b'knot' - x = b'hello' - y = b'world' + b'!' - z = b'\\xff\\x00' - ", - )?; - - assert_public_ty(&db, "src/a.py", "w", "Literal[b\"redknot\"]"); - assert_public_ty(&db, "src/a.py", "x", "Literal[b\"hello\"]"); - assert_public_ty(&db, "src/a.py", "y", "Literal[b\"world!\"]"); - assert_public_ty(&db, "src/a.py", "z", "Literal[b\"\\xff\\x00\"]"); - - Ok(()) - } - - #[test] - fn ellipsis_type() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - x = ... - ", - )?; - - // TODO: sys.version_info, and need to understand @final and @type_check_only - assert_public_ty(&db, "src/a.py", "x", "Unknown | EllipsisType"); - - Ok(()) - } - - #[test] - fn function_return_type() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_file("src/a.py", "def example() -> int: return 42")?; - - let mod_file = system_path_to_file(&db, "src/a.py").unwrap(); - let function = global_symbol_ty(&db, mod_file, "example").expect_function(); - let returns = function.return_type(&db); - assert_eq!(returns.display(&db).to_string(), "int"); - - Ok(()) - } - - #[test] - fn fstring_expression() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - x = 0 - y = str() - z = False - - a = f'hello' - b = f'h {x}' - c = 'one ' f'single ' f'literal' - d = 'first ' f'second({b})' f' third' - e = f'-{y}-' - f = f'-{y}-' f'--' '--' - g = f'{z} == {False} is {True}' - ", - )?; - - assert_public_ty(&db, "src/a.py", "a", "Literal[\"hello\"]"); - assert_public_ty(&db, "src/a.py", "b", "Literal[\"h 0\"]"); - assert_public_ty(&db, "src/a.py", "c", "Literal[\"one single literal\"]"); - assert_public_ty(&db, "src/a.py", "d", "Literal[\"first second(h 0) third\"]"); - assert_public_ty(&db, "src/a.py", "e", "str"); - assert_public_ty(&db, "src/a.py", "f", "str"); - assert_public_ty(&db, "src/a.py", "g", "Literal[\"False == False is True\"]"); - - Ok(()) - } - - #[test] - fn fstring_expression_with_conversion_flags() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - string = 'hello' - a = f'{string!r}' - ", - )?; - - assert_public_ty(&db, "src/a.py", "a", "str"); // Should be `Literal["'hello'"]` - - Ok(()) - } - - #[test] - fn fstring_expression_with_format_specifier() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - a = f'{1:02}' - ", - )?; - - assert_public_ty(&db, "src/a.py", "a", "str"); // Should be `Literal["01"]` - - Ok(()) - } - - #[test] - fn basic_call_expression() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - def get_int() -> int: - return 42 - - x = get_int() - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", "int"); - - Ok(()) - } - - #[test] - fn basic_async_call_expression() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - async def get_int_async() -> int: - return 42 - - x = get_int_async() - ", - )?; - - // TODO: Generic `types.CoroutineType`! - assert_public_ty(&db, "src/a.py", "x", "@Todo"); - - Ok(()) - } - - #[test] - fn basic_decorated_call_expression() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - from typing import Callable - - def foo() -> int: - return 42 - - def decorator(func) -> Callable[[], int]: - return foo - - @decorator - def bar() -> str: - return 'bar' - - x = bar() - ", - )?; - - // TODO: should be `int`! - assert_public_ty(&db, "src/a.py", "x", "@Todo"); - - Ok(()) - } - - #[test] - fn class_constructor_call_expression() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - class Foo: ... - - x = Foo() - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", "Foo"); - - Ok(()) - } - - #[test] - fn call_union() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - if flag: - def f() -> int: - return 1 - else: - def f() -> str: - return 'foo' - x = f() - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", "int | str"); - - Ok(()) - } - - #[test] - fn call_union_with_unknown() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - from nonexistent import f - if flag: - def f() -> int: - return 1 - x = f() - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", "Unknown | int"); - - Ok(()) - } - - #[test] - fn call_union_with_not_callable() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - if flag: - f = 1 - else: - def f() -> int: - return 1 - x = f() - ", - )?; - - assert_file_diagnostics( - &db, - "src/a.py", - &["Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"], - ); - assert_public_ty(&db, "src/a.py", "x", "Unknown | int"); - - Ok(()) - } - - #[test] - fn call_union_with_multiple_not_callable() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - if flag: - f = 1 - elif flag2: - f = 'foo' - else: - def f() -> int: - return 1 - x = f() - ", - )?; - - assert_file_diagnostics( - &db, - "src/a.py", - &[ - r#"Object of type `Literal[1] | Literal["foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"#, - ], - ); - assert_public_ty(&db, "src/a.py", "x", "Unknown | int"); - - Ok(()) - } - - #[test] - fn call_union_with_all_not_callable() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - if flag: - f = 1 - else: - f = 'foo' - x = f() - ", - )?; - - assert_file_diagnostics( - &db, - "src/a.py", - &[r#"Object of type `Literal[1] | Literal["foo"]` is not callable"#], - ); - assert_public_ty(&db, "src/a.py", "x", "Unknown"); - - Ok(()) - } - - #[test] - fn invalid_callable() { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - nonsense = 123 - x = nonsense() - ", - ) - .unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &["Object of type `Literal[123]` is not callable"], - ); - } - - #[test] - fn resolve_union() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - if flag: - x = 1 - else: - x = 2 - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", "Literal[1, 2]"); - - Ok(()) - } - - #[test] - fn simplify_true_and_false_to_bool() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - from typing_extensions import reveal_type - - def returns_bool() -> bool: - return True - - if returns_bool(): - x = True - else: - x = False - - reveal_type(x) - ", - )?; - - assert_file_diagnostics(&db, "src/a.py", &["Revealed type is `bool`"]); - - Ok(()) - } - - #[test] - fn literal_int_arithmetic() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - a = 2 + 1 - b = a - 4 - c = a * b - d = c // 3 - e = c / 3 - f = 5 % 3 - ", - )?; - - assert_public_ty(&db, "src/a.py", "a", "Literal[3]"); - assert_public_ty(&db, "src/a.py", "b", "Literal[-1]"); - assert_public_ty(&db, "src/a.py", "c", "Literal[-3]"); - assert_public_ty(&db, "src/a.py", "d", "Literal[-1]"); - assert_public_ty(&db, "src/a.py", "e", "float"); - assert_public_ty(&db, "src/a.py", "f", "Literal[2]"); - - Ok(()) - } - - #[test] - fn division_by_zero() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - a = 1 / 0 - b = 2 // 0 - c = 3 % 0 - d = int() / 0 - e = 1.0 / 0 - ", - )?; - - assert_public_ty(&db, "/src/a.py", "a", "float"); - assert_public_ty(&db, "/src/a.py", "b", "int"); - assert_public_ty(&db, "/src/a.py", "c", "int"); - // TODO: These should be `int` and `float` respectively once we support inference - assert_public_ty(&db, "/src/a.py", "d", "@Todo"); - assert_public_ty(&db, "/src/a.py", "e", "@Todo"); - - assert_file_diagnostics( - &db, - "src/a.py", - &[ - "Cannot divide object of type `Literal[1]` by zero", - "Cannot floor divide object of type `Literal[2]` by zero", - "Cannot reduce object of type `Literal[3]` modulo zero", - "Cannot divide object of type `int` by zero", - "Cannot divide object of type `float` by zero", - ], - ); - - Ok(()) - } - - #[test] - fn walrus() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_file("src/a.py", "x = (y := 1) + 1")?; - - assert_public_ty(&db, "src/a.py", "x", "Literal[2]"); - assert_public_ty(&db, "src/a.py", "y", "Literal[1]"); - - Ok(()) - } - - #[test] - fn walrus_self_plus_one() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - x = 0 - (x := x + 1) - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", "Literal[1]"); - - Ok(()) - } - - #[test] - fn ifexpr() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_file("src/a.py", "x = 1 if flag else 2")?; - - assert_public_ty(&db, "src/a.py", "x", "Literal[1, 2]"); - - Ok(()) - } - - #[test] - fn ifexpr_walrus() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - y = 0 - z = 0 - x = (y := 1) if flag else (z := 2) - a = y - b = z - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", "Literal[1, 2]"); - assert_public_ty(&db, "src/a.py", "a", "Literal[0, 1]"); - assert_public_ty(&db, "src/a.py", "b", "Literal[0, 2]"); - - Ok(()) - } - - #[test] - fn ifexpr_nested() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_file("src/a.py", "x = 1 if flag else 2 if flag2 else 3")?; - - assert_public_ty(&db, "src/a.py", "x", "Literal[1, 2, 3]"); - - Ok(()) - } - - #[test] - fn multi_target_assign() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_file("src/a.py", "x = y = 1")?; - - assert_public_ty(&db, "src/a.py", "x", "Literal[1]"); - assert_public_ty(&db, "src/a.py", "y", "Literal[1]"); - - Ok(()) - } - - #[test] - fn none() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_file("src/a.py", "x = 1 if flag else None")?; - - assert_public_ty(&db, "src/a.py", "x", "Literal[1] | None"); - Ok(()) - } - - #[test] - fn simple_if() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - y = 1 - y = 2 - if flag: - y = 3 - x = y - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", "Literal[2, 3]"); - Ok(()) - } - - #[test] - fn maybe_unbound() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - if flag: - y = 3 - x = y - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", "Unbound | Literal[3]"); - Ok(()) - } - - #[test] - fn if_elif_else() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - y = 1 - y = 2 - if flag: - y = 3 - elif flag2: - y = 4 - else: - r = y - y = 5 - s = y - x = y - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", "Literal[3, 4, 5]"); - assert_public_ty(&db, "src/a.py", "r", "Unbound | Literal[2]"); - assert_public_ty(&db, "src/a.py", "s", "Unbound | Literal[5]"); - Ok(()) - } - - #[test] - fn if_elif_else_single_symbol() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - if flag: - y = 1 - elif flag2: - y = 2 - else: - y = 3 - ", - )?; - - assert_public_ty(&db, "src/a.py", "y", "Literal[1, 2, 3]"); - Ok(()) - } - - #[test] - fn if_elif_else_no_definition_in_else() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - y = 0 - if flag: - y = 1 - elif flag2: - y = 2 - else: - pass - ", - )?; - - assert_public_ty(&db, "src/a.py", "y", "Literal[0, 1, 2]"); - Ok(()) - } - - #[test] - fn if_elif_else_no_definition_in_else_one_intervening_definition() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - y = 0 - if flag: - y = 1 - z = 3 - elif flag2: - y = 2 - else: - pass - ", - )?; - - assert_public_ty(&db, "src/a.py", "y", "Literal[0, 1, 2]"); - Ok(()) - } - - #[test] - fn nested_if() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - y = 0 - if flag: - if flag2: - y = 1 - ", - )?; - - assert_public_ty(&db, "src/a.py", "y", "Literal[0, 1]"); - Ok(()) - } - - #[test] - fn if_elif() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - y = 1 - y = 2 - if flag: - y = 3 - elif flag2: - y = 4 - x = y - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", "Literal[2, 3, 4]"); - Ok(()) - } - - #[test] - fn match_with_wildcard() { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - match 0: - case 1: - y = 2 - case _: - y = 3 -", - ) - .unwrap(); - - assert_public_ty(&db, "src/a.py", "y", "Literal[2, 3]"); - } - - #[test] - fn match_without_wildcard() { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - match 0: - case 1: - y = 2 - case 2: - y = 3 -", - ) - .unwrap(); - - assert_public_ty(&db, "src/a.py", "y", "Unbound | Literal[2, 3]"); - } - - #[test] - fn match_stmt() { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - y = 1 - y = 2 - match 0: - case 1: - y = 3 - case 2: - y = 4 -", - ) - .unwrap(); - - assert_public_ty(&db, "src/a.py", "y", "Literal[2, 3, 4]"); - } - - #[test] - fn import_cycle() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - class A: pass - import b - class C(b.B): pass - ", - )?; - db.write_dedented( - "src/b.py", - " - from a import A - class B(A): pass - ", - )?; - - let a = system_path_to_file(&db, "src/a.py").expect("file to exist"); - let c_ty = global_symbol_ty(&db, a, "C"); - let c_class = c_ty.expect_class(); - let mut c_bases = c_class.bases(&db); - let b_ty = c_bases.next().unwrap(); - let b_class = b_ty.expect_class(); - assert_eq!(b_class.name(&db), "B"); - let mut b_bases = b_class.bases(&db); - let a_ty = b_bases.next().unwrap(); - let a_class = a_ty.expect_class(); - assert_eq!(a_class.name(&db), "A"); - - Ok(()) - } - - /// An unbound function local that has definitions in the scope does not fall back to globals. - #[test] - fn unbound_function_local() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - x = 1 - def f(): - y = x - x = 2 - ", - )?; - - let file = system_path_to_file(&db, "src/a.py").expect("file to exist"); - let index = semantic_index(&db, file); - let function_scope = index - .child_scopes(FileScopeId::global()) - .next() - .unwrap() - .0 - .to_scope_id(&db, file); - let y_ty = symbol_ty(&db, function_scope, "y"); - let x_ty = symbol_ty(&db, function_scope, "x"); - - assert_eq!(y_ty.display(&db).to_string(), "Unbound"); - assert_eq!(x_ty.display(&db).to_string(), "Literal[2]"); - - Ok(()) - } - - /// A name reference to a never-defined symbol in a function is implicitly a global lookup. - #[test] - fn implicit_global_in_function() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - x = 1 - def f(): - y = x - ", - )?; - - let file = system_path_to_file(&db, "src/a.py").expect("file to exist"); - let index = semantic_index(&db, file); - let function_scope = index - .child_scopes(FileScopeId::global()) - .next() - .unwrap() - .0 - .to_scope_id(&db, file); - let y_ty = symbol_ty(&db, function_scope, "y"); - let x_ty = symbol_ty(&db, function_scope, "x"); - - assert_eq!(x_ty.display(&db).to_string(), "Unbound"); - assert_eq!(y_ty.display(&db).to_string(), "Literal[1]"); - - Ok(()) - } - - #[test] - fn conditionally_global_or_builtin() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - if flag: - copyright = 1 - def f(): - y = copyright - ", - )?; - - let file = system_path_to_file(&db, "src/a.py").expect("file to exist"); - let index = semantic_index(&db, file); - let function_scope = index - .child_scopes(FileScopeId::global()) - .next() - .unwrap() - .0 - .to_scope_id(&db, file); - let y_ty = symbol_ty(&db, function_scope, "y"); - - assert_eq!( - y_ty.display(&db).to_string(), - "Literal[copyright] | Literal[1]" - ); - - Ok(()) - } - - /// Class name lookups do fall back to globals, but the public type never does. - #[test] - fn unbound_class_local() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - x = 1 - class C: - y = x - if flag: - x = 2 - ", - )?; - - let file = system_path_to_file(&db, "src/a.py").expect("file to exist"); - let index = semantic_index(&db, file); - let class_scope = index - .child_scopes(FileScopeId::global()) - .next() - .unwrap() - .0 - .to_scope_id(&db, file); - let y_ty = symbol_ty(&db, class_scope, "y"); - let x_ty = symbol_ty(&db, class_scope, "x"); - - assert_eq!(x_ty.display(&db).to_string(), "Unbound | Literal[2]"); - assert_eq!(y_ty.display(&db).to_string(), "Literal[1]"); - - Ok(()) - } - - #[test] - fn local_inference() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_file("/src/a.py", "x = 10")?; - let a = system_path_to_file(&db, "/src/a.py").unwrap(); - - let parsed = parsed_module(&db, a); - - let statement = parsed.suite().first().unwrap().as_assign_stmt().unwrap(); - let model = SemanticModel::new(&db, a); - - let literal_ty = statement.value.ty(&model); - - assert_eq!(format!("{}", literal_ty.display(&db)), "Literal[10]"); - - Ok(()) - } - - #[test] - fn builtin_symbol_vendored_stdlib() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_file("/src/a.py", "c = copyright")?; - - assert_public_ty(&db, "/src/a.py", "c", "Literal[copyright]"); - - Ok(()) - } - - #[test] - fn builtin_symbol_custom_stdlib() -> anyhow::Result<()> { - let db = setup_db_with_custom_typeshed( - "/typeshed", - [ - ("/src/a.py", "c = copyright"), - ( - "/typeshed/stdlib/builtins.pyi", - "def copyright() -> None: ...", - ), - ("/typeshed/stdlib/VERSIONS", "builtins: 3.8-"), - ], - )?; - - assert_public_ty(&db, "/src/a.py", "c", "Literal[copyright]"); - - Ok(()) - } - - #[test] - fn unknown_global_later_defined() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_file("/src/a.py", "x = foo; foo = 1")?; - - assert_public_ty(&db, "/src/a.py", "x", "Unbound"); - - Ok(()) - } - - #[test] - fn unknown_builtin_later_defined() -> anyhow::Result<()> { - let db = setup_db_with_custom_typeshed( - "/typeshed", - [ - ("/src/a.py", "x = foo"), - ("/typeshed/stdlib/builtins.pyi", "foo = bar; bar = 1"), - ("/typeshed/stdlib/VERSIONS", "builtins: 3.8-"), - ], - )?; - - assert_public_ty(&db, "/src/a.py", "x", "Unbound"); - - Ok(()) - } - - #[test] - fn import_builtins() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_file("/src/a.py", "import builtins; x = builtins.copyright")?; - - assert_public_ty(&db, "/src/a.py", "x", "Literal[copyright]"); - // imported builtins module is the same file as the implicit builtins - let file = system_path_to_file(&db, "/src/a.py").expect("file to exist"); - let builtins_ty = global_symbol_ty(&db, file, "builtins"); - let builtins_file = builtins_ty.expect_module(); - let implicit_builtins_file = builtins_module_scope(&db) - .expect("builtins module should exist") - .file(&db); - assert_eq!(builtins_file, implicit_builtins_file); - - Ok(()) - } - - /// A class's bases can be self-referential; this looks silly but a slightly more complex - /// version of it actually occurs in typeshed: `class str(Sequence[str]): ...` - #[test] - fn cyclical_class_pyi_definition() -> anyhow::Result<()> { - let mut db = setup_db(); - db.write_file("/src/a.pyi", "class C(C): ...")?; - assert_public_ty(&db, "/src/a.pyi", "C", "Literal[C]"); - Ok(()) - } - - #[test] - fn str_builtin() -> anyhow::Result<()> { - let mut db = setup_db(); - db.write_file("/src/a.py", "x = str")?; - assert_public_ty(&db, "/src/a.py", "x", "Literal[str]"); - Ok(()) - } - - #[test] - fn deferred_annotation_builtin() -> anyhow::Result<()> { - let mut db = setup_db(); - db.write_file("/src/a.pyi", "class C(object): pass")?; - let file = system_path_to_file(&db, "/src/a.pyi").unwrap(); - let ty = global_symbol_ty(&db, file, "C"); - - let base = ty - .expect_class() - .bases(&db) - .next() - .expect("there should be at least one base"); - - assert_eq!(base.display(&db).to_string(), "Literal[object]"); - - Ok(()) - } - - #[test] - fn deferred_annotation_in_stubs_always_resolve() -> anyhow::Result<()> { - let mut db = setup_db(); - - // Stub files should always resolve deferred annotations - db.write_dedented( - "/src/stub.pyi", - " - def get_foo() -> Foo: ... - class Foo: ... - foo = get_foo() - ", - )?; - assert_public_ty(&db, "/src/stub.pyi", "foo", "Foo"); - - Ok(()) - } - - #[test] - fn deferred_annotations_regular_source_fails() -> anyhow::Result<()> { - let mut db = setup_db(); - - // In (regular) source files, deferred annotations are *not* resolved - // Also tests imports from `__future__` that are not annotations - db.write_dedented( - "/src/source.py", - " - from __future__ import with_statement as annotations - def get_foo() -> Foo: ... - class Foo: ... - foo = get_foo() - ", - )?; - assert_public_ty(&db, "/src/source.py", "foo", "Unknown"); - - Ok(()) - } - - #[test] - fn deferred_annotation_in_sources_with_future_resolves() -> anyhow::Result<()> { - let mut db = setup_db(); - - // In source files with `__future__.annotations`, deferred annotations are resolved - db.write_dedented( - "/src/source_with_future.py", - " - from __future__ import annotations - def get_foo() -> Foo: ... - class Foo: ... - foo = get_foo() - ", - )?; - assert_public_ty(&db, "/src/source_with_future.py", "foo", "Foo"); - - Ok(()) - } - - #[test] - fn while_loop() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x = 1 - while flag: - x = 2 - ", - )?; - - // body of while loop may or may not run - assert_public_ty(&db, "/src/a.py", "x", "Literal[1, 2]"); - - Ok(()) - } - - #[test] - fn while_else_no_break() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x = 1 - while flag: - x = 2 - else: - y = x - x = 3 - ", - )?; - - // body of the loop can't break, so we can get else, or body+else - // x must be 3, because else will always run - assert_public_ty(&db, "/src/a.py", "x", "Literal[3]"); - // y can be 1 or 2 because else always runs, and body may or may not run first - assert_public_ty(&db, "/src/a.py", "y", "Literal[1, 2]"); - - Ok(()) - } - - #[test] - fn while_else_may_break() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x = 1 - y = 0 - while flag: - x = 2 - if flag2: - y = 4 - break - else: - y = x - x = 3 - ", - )?; - - // body may break: we can get just-body (only if we break), just-else, or body+else - assert_public_ty(&db, "/src/a.py", "x", "Literal[2, 3]"); - // if just-body were possible without the break, then 0 would be possible for y - // 1 and 2 both being possible for y shows that we can hit else with or without body - assert_public_ty(&db, "/src/a.py", "y", "Literal[1, 2, 4]"); - - Ok(()) - } - - #[test] - fn attribute_of_union() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - if flag: - class C: - x = 1 - else: - class C: - x = 2 - y = C.x - ", - )?; - - assert_public_ty(&db, "/src/a.py", "y", "Literal[1, 2]"); - - Ok(()) - } - - #[test] - fn big_int() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x = 10_000_000_000_000_000_000 - ", - )?; - - assert_public_ty(&db, "/src/a.py", "x", "int"); - - Ok(()) - } - - #[test] - fn empty_tuple_literal() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x = () - ", - )?; - - assert_public_ty(&db, "/src/a.py", "x", "tuple[()]"); - - Ok(()) - } - - #[test] - fn tuple_heterogeneous_literal() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x = (1, 'a') - y = (1, (2, 3)) - z = (x, 2) - ", - )?; - - assert_public_ty(&db, "/src/a.py", "x", r#"tuple[Literal[1], Literal["a"]]"#); - assert_public_ty( - &db, - "/src/a.py", - "y", - "tuple[Literal[1], tuple[Literal[2], Literal[3]]]", - ); - assert_public_ty( - &db, - "/src/a.py", - "z", - r#"tuple[tuple[Literal[1], Literal["a"]], Literal[2]]"#, - ); - - Ok(()) - } - - #[test] - fn list_literal() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x = [] - ", - )?; - - // TODO should be a generic type - assert_public_ty(&db, "/src/a.py", "x", "list"); - - Ok(()) - } - - #[test] - fn set_literal() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x = {1, 2} - ", - )?; - - // TODO should be a generic type - assert_public_ty(&db, "/src/a.py", "x", "set"); - - Ok(()) - } - - #[test] - fn dict_literal() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x = {} - ", - )?; - - // TODO should be a generic type - assert_public_ty(&db, "/src/a.py", "x", "dict"); - - Ok(()) - } - - #[test] - fn nonlocal_name_reference() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - def f(): - x = 1 - def g(): - y = x - ", - )?; - - assert_scope_ty(&db, "/src/a.py", &["f", "g"], "y", "Literal[1]"); - - Ok(()) - } - - #[test] - fn nonlocal_name_reference_multi_level() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - def f(): - x = 1 - def g(): - def h(): - y = x - ", - )?; - - assert_scope_ty(&db, "/src/a.py", &["f", "g", "h"], "y", "Literal[1]"); - - Ok(()) - } - - #[test] - fn nonlocal_name_reference_skips_class_scope() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - def f(): - x = 1 - class C: - x = 2 - def g(): - y = x - ", - )?; - - assert_scope_ty(&db, "/src/a.py", &["f", "C", "g"], "y", "Literal[1]"); - - Ok(()) - } - - #[test] - fn nonlocal_name_reference_skips_annotation_only_assignment() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - def f(): - x = 1 - def g(): - // it's pretty weird to have an annotated assignment in a function where the - // name is otherwise not defined; maybe should be an error? - x: int - def h(): - y = x - ", - )?; - - assert_scope_ty(&db, "/src/a.py", &["f", "g", "h"], "y", "Literal[1]"); - - Ok(()) - } - - #[test] - fn annotation_only_assignment_transparent_to_local_inference() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x = 1 - x: int - y = x - ", - )?; - - assert_public_ty(&db, "/src/a.py", "y", "Literal[1]"); - - Ok(()) - } - - /// A declared-but-not-bound name can be imported from a stub file. - #[test] - fn import_from_stub_declaration_only() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - from b import x - y = x - ", - )?; - db.write_dedented( - "/src/b.pyi", - " - x: int - ", - )?; - - assert_public_ty(&db, "/src/a.py", "y", "int"); - - Ok(()) - } - - /// Declarations take priority over definitions when importing from a non-stub file. - #[test] - fn import_from_non_stub_declared_and_bound() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - from b import x - y = x - ", - )?; - db.write_dedented( - "/src/b.py", - " - x: int = 1 - ", - )?; - - assert_public_ty(&db, "/src/a.py", "y", "int"); - - Ok(()) - } - - #[test] - fn unresolved_import_statement() { - let mut db = setup_db(); - - db.write_file("src/foo.py", "import bar\n").unwrap(); - - assert_file_diagnostics(&db, "src/foo.py", &["Cannot resolve import `bar`"]); - } - - #[test] - fn unresolved_import_from_statement() { - let mut db = setup_db(); - - db.write_file("src/foo.py", "from bar import baz\n") - .unwrap(); - assert_file_diagnostics(&db, "/src/foo.py", &["Cannot resolve import `bar`"]); - } - - #[test] - fn unresolved_import_from_resolved_module() { - let mut db = setup_db(); - - db.write_files([("/src/a.py", ""), ("/src/b.py", "from a import thing")]) - .unwrap(); - - assert_file_diagnostics(&db, "/src/b.py", &["Module `a` has no member `thing`"]); - } - - #[test] - fn resolved_import_of_symbol_from_unresolved_import() { - let mut db = setup_db(); - - db.write_files([ - ("/src/a.py", "import foo as foo"), - ("/src/b.py", "from a import foo"), - ]) - .unwrap(); - - assert_file_diagnostics(&db, "/src/a.py", &["Cannot resolve import `foo`"]); - - // Importing the unresolved import into a second first-party file should not trigger - // an additional "unresolved import" violation - assert_file_diagnostics(&db, "/src/b.py", &[]); - } - - #[test] - fn basic_for_loop() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - class IntIterator: - def __next__(self) -> int: - return 42 - - class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - - for x in IntIterable(): - pass - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", "Unbound | int"); - - Ok(()) - } - - #[test] - fn for_loop_with_previous_definition() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - class IntIterator: - def __next__(self) -> int: - return 42 - - class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - - x = 'foo' - - for x in IntIterable(): - pass - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", r#"Literal["foo"] | int"#); - - Ok(()) - } - - #[test] - fn for_loop_no_break() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - class IntIterator: - def __next__(self) -> int: - return 42 - - class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - - for x in IntIterable(): - pass - else: - x = 'foo' - ", - )?; - - // The `for` loop can never break, so the `else` clause will always be executed, - // meaning that the visible definition by the end of the scope is solely determined - // by the `else` clause - assert_public_ty(&db, "src/a.py", "x", r#"Literal["foo"]"#); - - Ok(()) - } - - #[test] - fn for_loop_may_break() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - class IntIterator: - def __next__(self) -> int: - return 42 - - class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - - for x in IntIterable(): - if x > 5: - break - else: - x = 'foo' - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", r#"int | Literal["foo"]"#); - - Ok(()) - } - - #[test] - fn for_loop_with_old_style_iteration_protocol() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - class OldStyleIterable: - def __getitem__(self, key: int) -> int: - return 42 - - for x in OldStyleIterable(): - pass - ", - )?; - - assert_public_ty(&db, "src/a.py", "x", "Unbound | int"); - - Ok(()) - } - - /// This tests that we understand that `async` for loops - /// do not work according to the synchronous iteration protocol - #[test] - fn invalid_async_for_loop() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - async def foo(): - class Iterator: - def __next__(self) -> int: - return 42 - - class Iterable: - def __iter__(self) -> Iterator: - return Iterator() - - async for x in Iterator(): - pass - ", - )?; - - // We currently return `Todo` for all `async for` loops, - // including loops that have invalid syntax - assert_scope_ty(&db, "src/a.py", &["foo"], "x", "Unbound | @Todo"); - - Ok(()) - } - - #[test] - fn basic_async_for_loop() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - async def foo(): - class IntAsyncIterator: - async def __anext__(self) -> int: - return 42 - - class IntAsyncIterable: - def __aiter__(self) -> IntAsyncIterator: - return IntAsyncIterator() - - async for x in IntAsyncIterable(): - pass - ", - )?; - - // TODO(Alex) async iterables/iterators! - assert_scope_ty(&db, "src/a.py", &["foo"], "x", "Unbound | @Todo"); - - Ok(()) - } - - #[test] - fn for_loop_with_heterogenous_tuple() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - for x in (1, 'a', b'foo'): - pass - ", - )?; - - assert_public_ty( - &db, - "src/a.py", - "x", - r#"Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]"#, - ); - - Ok(()) - } - - #[test] - fn for_loop_non_callable_iter() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - class NotIterable: - if flag: - __iter__ = 1 - else: - __iter__ = None - - for x in NotIterable(): - pass - ", - )?; - - assert_file_diagnostics( - &db, - "src/a.py", - &["Object of type `NotIterable` is not iterable"], - ); - assert_public_ty(&db, "src/a.py", "x", "Unbound | Unknown"); - - Ok(()) - } - - #[test] - fn except_handler_single_exception() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - import re - from typing_extensions import reveal_type - - try: - x - except NameError as e: - reveal_type(e) - except re.error as f: - reveal_type(f) - ", - )?; - - assert_file_diagnostics( - &db, - "src/a.py", - &["Revealed type is `NameError`", "Revealed type is `error`"], - ); - - Ok(()) - } - - #[test] - fn unknown_type_in_except_handler_does_not_cause_spurious_diagnostic() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - from nonexistent_module import foo - from typing_extensions import reveal_type - - try: - x - except foo as e: - reveal_type(foo) - reveal_type(e) - ", - )?; - - assert_file_diagnostics( - &db, - "src/a.py", - &[ - "Cannot resolve import `nonexistent_module`", - "Revealed type is `Unknown`", - "Revealed type is `Unknown`", - ], - ); - - Ok(()) - } - - #[test] - fn except_handler_multiple_exceptions() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - from typing_extensions import reveal_type - - EXCEPTIONS = (AttributeError, TypeError) - - try: - x - except (RuntimeError, OSError) as e: - reveal_type(e) - except EXCEPTIONS as f: - reveal_type(f) - ", - )?; - - let expected_diagnostics = &[ - "Revealed type is `RuntimeError | OSError`", - "Revealed type is `AttributeError | TypeError`", - ]; - - assert_file_diagnostics(&db, "src/a.py", expected_diagnostics); - - Ok(()) - } - - #[test] - fn except_handler_dynamic_exceptions() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - from typing_extensions import reveal_type - - def foo( - x: type[AttributeError], - y: tuple[type[OSError], type[RuntimeError]], - z: tuple[type[BaseException], ...] - ): - try: - w - except x as e: - reveal_type(e) - except y as f: - reveal_type(f) - except z as g: - reveal_type(g) - ", - )?; - - let expected_diagnostics = &[ - // Should be `AttributeError`: - "Revealed type is `@Todo`", - // Should be `OSError | RuntimeError`: - "Revealed type is `@Todo`", - // Should be `BaseException`: - "Revealed type is `@Todo`", - ]; - - assert_file_diagnostics(&db, "src/a.py", expected_diagnostics); - - Ok(()) - } - - #[test] - fn exception_handler_with_invalid_syntax() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - from typing_extensions import reveal_type - - try: - x - except as e: - reveal_type(e) - ", - )?; - - assert_file_diagnostics(&db, "src/a.py", &["Revealed type is `Unknown`"]); - - Ok(()) - } - - #[test] - fn except_star_handler_baseexception() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - from typing_extensions import reveal_type - - try: - x - except* BaseException as e: - reveal_type(e) - ", - )?; - - // TODO: once we support `sys.version_info` branches, - // we can set `--target-version=py311` in this test - // and the inferred type will just be `BaseExceptionGroup` --Alex - assert_file_diagnostics( - &db, - "src/a.py", - &["Revealed type is `Unknown | BaseExceptionGroup`"], - ); - - Ok(()) - } - - #[test] - fn except_star_handler() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - from typing_extensions import reveal_type - - try: - x - except* OSError as e: - reveal_type(e) - ", - )?; - - // TODO: once we support `sys.version_info` branches, - // we can set `--target-version=py311` in this test - // and the inferred type will just be `BaseExceptionGroup` --Alex - // - // TODO more precise would be `ExceptionGroup[OSError]` --Alex - assert_file_diagnostics( - &db, - "src/a.py", - &["Revealed type is `Unknown | BaseExceptionGroup`"], - ); - - Ok(()) - } - - #[test] - fn except_star_handler_multiple_types() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - from typing_extensions import reveal_type - - try: - x - except* (TypeError, AttributeError) as e: - reveal_type(e) - ", - )?; - - // TODO: once we support `sys.version_info` branches, - // we can set `--target-version=py311` in this test - // and the inferred type will just be `BaseExceptionGroup` --Alex - // - // TODO more precise would be `ExceptionGroup[TypeError | AttributeError]` --Alex - assert_file_diagnostics( - &db, - "src/a.py", - &["Revealed type is `Unknown | BaseExceptionGroup`"], - ); - - Ok(()) - } - - #[test] - fn basic_comprehension() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - def foo(): - [x for y in IterableOfIterables() for x in y] - - class IntIterator: - def __next__(self) -> int: - return 42 - - class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - - class IteratorOfIterables: - def __next__(self) -> IntIterable: - return IntIterable() - - class IterableOfIterables: - def __iter__(self) -> IteratorOfIterables: - return IteratorOfIterables() - ", - )?; - - assert_scope_ty(&db, "src/a.py", &["foo", ""], "x", "int"); - assert_scope_ty(&db, "src/a.py", &["foo", ""], "y", "IntIterable"); - - Ok(()) - } - - #[test] - fn comprehension_inside_comprehension() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - def foo(): - [[x for x in iter1] for y in iter2] - - class IntIterator: - def __next__(self) -> int: - return 42 - - class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - - iter1 = IntIterable() - iter2 = IntIterable() - ", - )?; - - assert_scope_ty( - &db, - "src/a.py", - &["foo", "", ""], - "x", - "int", - ); - assert_scope_ty(&db, "src/a.py", &["foo", ""], "y", "int"); - - Ok(()) - } - - #[test] - fn inner_comprehension_referencing_outer_comprehension() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - def foo(): - [[x for x in y] for y in z] - - class IntIterator: - def __next__(self) -> int: - return 42 - - class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - - class IteratorOfIterables: - def __next__(self) -> IntIterable: - return IntIterable() - - class IterableOfIterables: - def __iter__(self) -> IteratorOfIterables: - return IteratorOfIterables() - - z = IterableOfIterables() - ", - )?; - - assert_scope_ty( - &db, - "src/a.py", - &["foo", "", ""], - "x", - "int", - ); - assert_scope_ty(&db, "src/a.py", &["foo", ""], "y", "IntIterable"); - - Ok(()) - } - - #[test] - fn comprehension_with_unbound_iter() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented("src/a.py", "[z for z in x]")?; - - assert_scope_ty(&db, "src/a.py", &[""], "x", "Unbound"); - - // Iterating over an `Unbound` yields `Unknown`: - assert_scope_ty(&db, "src/a.py", &[""], "z", "Unknown"); - - // TODO: not the greatest error message in the world! --Alex - assert_file_diagnostics( - &db, - "src/a.py", - &["Object of type `Unbound` is not iterable"], - ); - - Ok(()) - } - - #[test] - fn comprehension_with_not_iterable_iter_in_second_comprehension() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - def foo(): - [z for x in IntIterable() for z in x] - - class IntIterator: - def __next__(self) -> int: - return 42 - - class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - ", - )?; - - assert_scope_ty(&db, "src/a.py", &["foo", ""], "x", "int"); - assert_scope_ty(&db, "src/a.py", &["foo", ""], "z", "Unknown"); - assert_file_diagnostics(&db, "src/a.py", &["Object of type `int` is not iterable"]); - - Ok(()) - } - - #[test] - fn dict_comprehension_variable_key() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - def foo(): - {x: 0 for x in IntIterable()} - - class IntIterator: - def __next__(self) -> int: - return 42 - - class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - ", - )?; - - assert_scope_ty(&db, "src/a.py", &["foo", ""], "x", "int"); - assert_file_diagnostics(&db, "src/a.py", &[]); - - Ok(()) - } - - #[test] - fn dict_comprehension_variable_value() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - def foo(): - {0: x for x in IntIterable()} - - class IntIterator: - def __next__(self) -> int: - return 42 - - class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - ", - )?; - - assert_scope_ty(&db, "src/a.py", &["foo", ""], "x", "int"); - assert_file_diagnostics(&db, "src/a.py", &[]); - - Ok(()) - } - - #[test] - fn comprehension_with_missing_in_keyword() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - def foo(): - [z for z IntIterable()] - - class IntIterator: - def __next__(self) -> int: - return 42 - - class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - ", - )?; - - // We'll emit a diagnostic separately for invalid syntax, - // but it's reasonably clear here what they *meant* to write, - // so we'll still infer the correct type: - assert_scope_ty(&db, "src/a.py", &["foo", ""], "z", "int"); - Ok(()) - } - - #[test] - fn comprehension_with_missing_iter() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - def foo(): - [z for in IntIterable()] - - class IntIterator: - def __next__(self) -> int: - return 42 - - class IntIterable: - def __iter__(self) -> IntIterator: - return IntIterator() - ", - )?; - - 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", &[]); - - Ok(()) - } - - #[test] - fn comprehension_with_missing_for() -> anyhow::Result<()> { - let mut db = setup_db(); - db.write_dedented("src/a.py", "[z for z in]")?; - assert_scope_ty(&db, "src/a.py", &[""], "z", "Unknown"); - Ok(()) - } - - #[test] - fn comprehension_with_missing_in_keyword_and_missing_iter() -> anyhow::Result<()> { - let mut db = setup_db(); - db.write_dedented("src/a.py", "[z for z]")?; - assert_scope_ty(&db, "src/a.py", &[""], "z", "Unknown"); - Ok(()) - } - - /// This tests that we understand that `async` comprehensions - /// do not work according to the synchronous iteration protocol - #[test] - fn invalid_async_comprehension() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - async def foo(): - [x async for x in Iterable()] - class Iterator: - def __next__(self) -> int: - return 42 - class Iterable: - def __iter__(self) -> Iterator: - return Iterator() - ", - )?; - - // We currently return `Todo` for all async comprehensions, - // including comprehensions that have invalid syntax - assert_scope_ty(&db, "src/a.py", &["foo", ""], "x", "@Todo"); - - Ok(()) - } - - #[test] - fn basic_async_comprehension() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - async def foo(): - [x async for x in AsyncIterable()] - class AsyncIterator: - async def __anext__(self) -> int: - return 42 - class AsyncIterable: - def __aiter__(self) -> AsyncIterator: - return AsyncIterator() - ", - )?; - - // TODO async iterables/iterators! --Alex - assert_scope_ty(&db, "src/a.py", &["foo", ""], "x", "@Todo"); - - Ok(()) - } - - #[test] - fn invalid_iterable() { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - nonsense = 123 - for x in nonsense: - pass - ", - ) - .unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &["Object of type `Literal[123]` is not iterable"], - ); - } - - #[test] - fn new_iteration_protocol_takes_precedence_over_old_style() { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - class NotIterable: - def __getitem__(self, key: int) -> int: - return 42 - - __iter__ = None - - for x in NotIterable(): - pass - ", - ) - .unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &["Object of type `NotIterable` is not iterable"], - ); - } - - #[test] - fn starred_expressions_must_be_iterable() { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - class NotIterable: pass - - class Iterator: - def __next__(self) -> int: - return 42 - - class Iterable: - def __iter__(self) -> Iterator: - - x = [*NotIterable()] - y = [*Iterable()] - ", - ) - .unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &["Object of type `NotIterable` is not iterable"], - ); - } - - #[test] - fn yield_from_expression_must_be_iterable() { - let mut db = setup_db(); - - db.write_dedented( - "src/a.py", - " - class NotIterable: pass - - class Iterator: - def __next__(self) -> int: - return 42 - - class Iterable: - def __iter__(self) -> Iterator: - - def generator_function(): - yield from Iterable() - yield from NotIterable() - ", - ) - .unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &["Object of type `NotIterable` is not iterable"], - ); - } - - #[test] - fn assignment_violates_own_annotation() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x: int = 'foo' - ", - ) - .unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &[r#"Object of type `Literal["foo"]` is not assignable to `int`"#], - ); - } - - #[test] - fn assignment_violates_previous_annotation() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x: int - x = 'foo' - ", - ) - .unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &[r#"Object of type `Literal["foo"]` is not assignable to `int`"#], - ); - } - - #[test] - fn shadowing_is_ok() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x: str = 'foo' - x: int = 1 - ", - ) - .unwrap(); - - assert_file_diagnostics(&db, "/src/a.py", &[]); - } - - #[test] - fn shadowing_parameter_is_ok() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - def f(x: str): - x: int = int(x) - ", - ) - .unwrap(); - - assert_file_diagnostics(&db, "/src/a.py", &[]); - } - - #[test] - fn declaration_violates_previous_assignment() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - x = 1 - x: str - ", - ) - .unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &[r"Cannot declare type `str` for inferred type `Literal[1]`"], - ); - } - - #[test] - fn incompatible_declarations() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - if flag: - x: str - else: - x: int - x = 1 - ", - ) - .unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &[r"Conflicting declared types for `x`: str, int"], - ); - } - - #[test] - fn partial_declarations() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - if flag: - x: int - x = 1 - ", - ) - .unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &[r"Conflicting declared types for `x`: Unknown, int"], - ); - } - - #[test] - fn incompatible_declarations_bad_assignment() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - if flag: - x: str - else: - x: int - x = b'foo' - ", - ) - .unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &[ - r"Conflicting declared types for `x`: str, int", - r#"Object of type `Literal[b"foo"]` is not assignable to `str | int`"#, - ], - ); - } - - #[test] - fn partial_declarations_questionable_assignment() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - if flag: - x: int - x = 'foo' - ", - ) - .unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &[r"Conflicting declared types for `x`: Unknown, int"], - ); - } - - #[test] - fn shadow_after_incompatible_declarations_is_ok() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - if flag: - x: str - else: - x: int - x: bytes = b'foo' - ", - ) - .unwrap(); - - assert_file_diagnostics(&db, "/src/a.py", &[]); - } - - #[test] - fn no_implicit_shadow_function() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - def f(): pass - f = 1 - ", - ) - .unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &["Implicit shadowing of function `f`; annotate to make it explicit if this is intentional"], - ); - } - - #[test] - fn no_implicit_shadow_class() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - class C: pass - C = 1 - ", - ) - .unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &["Implicit shadowing of class `C`; annotate to make it explicit if this is intentional"], - ); - } - - #[test] - fn explicit_shadow_function() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - def f(): pass - f: int = 1 - ", - ) - .unwrap(); - - assert_file_diagnostics(&db, "/src/a.py", &[]); - } - - #[test] - fn explicit_shadow_class() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - class C(): pass - C: int = 1 - ", - ) - .unwrap(); - - assert_file_diagnostics(&db, "/src/a.py", &[]); - } - - #[test] - fn no_implicit_shadow_import() { - let mut db = setup_db(); - - db.write_dedented( - "/src/a.py", - " - from b import x - - x = 'foo' - ", - ) - .unwrap(); - - db.write_file("/src/b.py", "x: int").unwrap(); - - assert_file_diagnostics( - &db, - "/src/a.py", - &[r#"Object of type `Literal["foo"]` is not assignable to `int`"#], - ); - } - - #[test] - fn import_from_conditional_reimport() { - let mut db = setup_db(); - - db.write_file("/src/a.py", "from b import f").unwrap(); - db.write_dedented( - "/src/b.py", - " - if flag: - from c import f - else: - def f(): ... - ", - ) - .unwrap(); - db.write_file("/src/c.py", "def f(): ...").unwrap(); - - // TODO we should really disambiguate in such cases: Literal[b.f, c.f] - assert_public_ty(&db, "/src/a.py", "f", "Literal[f, f]"); - } - - #[test] - fn import_from_conditional_reimport_vs_non_declaration() { - let mut db = setup_db(); - - db.write_file("/src/a.py", "from b import x").unwrap(); - db.write_dedented( - "/src/b.py", - " - if flag: - from c import x - else: - x = 1 - ", - ) - .unwrap(); - db.write_file("/src/c.pyi", "x: int").unwrap(); - - assert_public_ty(&db, "/src/a.py", "x", "int"); - } - - // Incremental inference tests - - fn first_public_binding<'db>(db: &'db TestDb, file: File, name: &str) -> Definition<'db> { - let scope = global_scope(db, file); - use_def_map(db, scope) - .public_bindings(symbol_table(db, scope).symbol_id_by_name(name).unwrap()) - .next() - .unwrap() - .binding - } - - #[test] - fn dependency_public_symbol_type_change() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_files([ - ("/src/a.py", "from foo import x"), - ("/src/foo.py", "x = 10\ndef foo(): ..."), - ])?; - - let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let x_ty = global_symbol_ty(&db, a, "x"); - - assert_eq!(x_ty.display(&db).to_string(), "Literal[10]"); - - // Change `x` to a different value - db.write_file("/src/foo.py", "x = 20\ndef foo(): ...")?; - - let a = system_path_to_file(&db, "/src/a.py").unwrap(); - - let x_ty_2 = global_symbol_ty(&db, a, "x"); - - assert_eq!(x_ty_2.display(&db).to_string(), "Literal[20]"); - - Ok(()) - } - - #[test] - fn dependency_internal_symbol_change() -> anyhow::Result<()> { - let mut db = setup_db(); - - db.write_files([ - ("/src/a.py", "from foo import x"), - ("/src/foo.py", "x = 10\ndef foo(): y = 1"), - ])?; - - let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let x_ty = global_symbol_ty(&db, a, "x"); - - assert_eq!(x_ty.display(&db).to_string(), "Literal[10]"); - - db.write_file("/src/foo.py", "x = 10\ndef foo(): pass")?; - - let a = system_path_to_file(&db, "/src/a.py").unwrap(); - - db.clear_salsa_events(); - - let x_ty_2 = global_symbol_ty(&db, a, "x"); - - assert_eq!(x_ty_2.display(&db).to_string(), "Literal[10]"); + " + from typing_extensions import reveal_type - let events = db.take_salsa_events(); + try: + x + except as e: + reveal_type(e) + ", + )?; - assert_function_query_was_not_run( - &db, - infer_definition_types, - first_public_binding(&db, a, "x"), - &events, - ); + assert_file_diagnostics(&db, "src/a.py", &["Revealed type is `Unknown`"]); Ok(()) } #[test] - fn dependency_unrelated_symbol() -> anyhow::Result<()> { + fn basic_comprehension() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_files([ - ("/src/a.py", "from foo import x"), - ("/src/foo.py", "x = 10\ny = 20"), - ])?; - - let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let x_ty = global_symbol_ty(&db, a, "x"); - - assert_eq!(x_ty.display(&db).to_string(), "Literal[10]"); - - db.write_file("/src/foo.py", "x = 10\ny = 30")?; + db.write_dedented( + "src/a.py", + " + def foo(): + [x for y in IterableOfIterables() for x in y] - let a = system_path_to_file(&db, "/src/a.py").unwrap(); + class IntIterator: + def __next__(self) -> int: + return 42 - db.clear_salsa_events(); + class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() - let x_ty_2 = global_symbol_ty(&db, a, "x"); + class IteratorOfIterables: + def __next__(self) -> IntIterable: + return IntIterable() - assert_eq!(x_ty_2.display(&db).to_string(), "Literal[10]"); + class IterableOfIterables: + def __iter__(self) -> IteratorOfIterables: + return IteratorOfIterables() + ", + )?; - let events = db.take_salsa_events(); + assert_scope_ty(&db, "src/a.py", &["foo", ""], "x", "int"); + assert_scope_ty(&db, "src/a.py", &["foo", ""], "y", "IntIterable"); - assert_function_query_was_not_run( - &db, - infer_definition_types, - first_public_binding(&db, a, "x"), - &events, - ); Ok(()) } #[test] - fn subscript_tuple() -> anyhow::Result<()> { + fn comprehension_inside_comprehension() -> anyhow::Result<()> { let mut db = setup_db(); db.write_dedented( - "/src/a.py", - " - t = (1, 'a', 'b') - - a = t[0] - b = t[1] - c = t[-1] - d = t[-2] - e = t[4] - f = t[-4] - ", - )?; - - assert_public_ty(&db, "/src/a.py", "a", "Literal[1]"); - assert_public_ty(&db, "/src/a.py", "b", "Literal[\"a\"]"); - assert_public_ty(&db, "/src/a.py", "c", "Literal[\"b\"]"); - assert_public_ty(&db, "/src/a.py", "d", "Literal[\"a\"]"); - assert_public_ty(&db, "/src/a.py", "e", "Unknown"); - assert_public_ty(&db, "/src/a.py", "f", "Unknown"); - - assert_file_diagnostics( - &db, "src/a.py", - &["Index 4 is out of bounds for tuple of type `tuple[Literal[1], Literal[\"a\"], Literal[\"b\"]]` with length 3", "Index -4 is out of bounds for tuple of type `tuple[Literal[1], Literal[\"a\"], Literal[\"b\"]]` with length 3"], - ); + " + def foo(): + [[x for x in iter1] for y in iter2] - Ok(()) - } + class IntIterator: + def __next__(self) -> int: + return 42 - #[test] - fn subscript_literal_string() -> anyhow::Result<()> { - let mut db = setup_db(); + class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() - db.write_dedented( - "/src/a.py", - " - s = 'abcde' - - a = s[0] - b = s[1] - c = s[-1] - d = s[-2] - e = s[8] - f = s[-8] + iter1 = IntIterable() + iter2 = IntIterable() ", )?; - assert_public_ty(&db, "/src/a.py", "a", "Literal[\"a\"]"); - assert_public_ty(&db, "/src/a.py", "b", "Literal[\"b\"]"); - assert_public_ty(&db, "/src/a.py", "c", "Literal[\"e\"]"); - assert_public_ty(&db, "/src/a.py", "d", "Literal[\"d\"]"); - assert_public_ty(&db, "/src/a.py", "e", "Unknown"); - assert_public_ty(&db, "/src/a.py", "f", "Unknown"); - - assert_file_diagnostics( + assert_scope_ty( &db, "src/a.py", - &[ - "Index 8 is out of bounds for string `Literal[\"abcde\"]` with length 5", - "Index -8 is out of bounds for string `Literal[\"abcde\"]` with length 5", - ], + &["foo", "", ""], + "x", + "int", ); + assert_scope_ty(&db, "src/a.py", &["foo", ""], "y", "int"); Ok(()) } #[test] - fn subscript_getitem_unbound() -> anyhow::Result<()> { + fn inner_comprehension_referencing_outer_comprehension() -> anyhow::Result<()> { let mut db = setup_db(); db.write_dedented( - "/src/a.py", + "src/a.py", " - class NotSubscriptable: - pass - - a = NotSubscriptable()[0] - ", - )?; + def foo(): + [[x for x in y] for y in z] - assert_public_ty(&db, "/src/a.py", "a", "Unknown"); - assert_file_diagnostics( - &db, - "/src/a.py", - &["Cannot subscript object of type `NotSubscriptable` with no `__getitem__` method"], - ); + class IntIterator: + def __next__(self) -> int: + return 42 - Ok(()) - } + class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() - #[test] - fn subscript_class_getitem_unbound() -> anyhow::Result<()> { - let mut db = setup_db(); + class IteratorOfIterables: + def __next__(self) -> IntIterable: + return IntIterable() - db.write_dedented( - "/src/a.py", - " - class NotSubscriptable: - pass + class IterableOfIterables: + def __iter__(self) -> IteratorOfIterables: + return IteratorOfIterables() - a = NotSubscriptable[0] + z = IterableOfIterables() ", )?; - assert_public_ty(&db, "/src/a.py", "a", "Unknown"); - assert_file_diagnostics( + assert_scope_ty( &db, - "/src/a.py", - &["Cannot subscript object of type `Literal[NotSubscriptable]` with no `__class_getitem__` method"], + "src/a.py", + &["foo", "", ""], + "x", + "int", ); + assert_scope_ty(&db, "src/a.py", &["foo", ""], "y", "IntIterable"); Ok(()) } #[test] - fn subscript_not_callable_getitem() -> anyhow::Result<()> { + fn comprehension_with_unbound_iter() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_dedented( - "/src/a.py", - " - class NotSubscriptable: - __getitem__ = None + db.write_dedented("src/a.py", "[z for z in x]")?; - a = NotSubscriptable()[0] - ", - )?; + assert_scope_ty(&db, "src/a.py", &[""], "x", "Unbound"); + + // Iterating over an `Unbound` yields `Unknown`: + assert_scope_ty(&db, "src/a.py", &[""], "z", "Unknown"); - assert_public_ty(&db, "/src/a.py", "a", "Unknown"); + // TODO: not the greatest error message in the world! --Alex assert_file_diagnostics( &db, - "/src/a.py", - &["Method `__getitem__` of type `None` is not callable on object of type `NotSubscriptable`"], + "src/a.py", + &["Object of type `Unbound` is not iterable"], ); Ok(()) } #[test] - fn subscript_str_literal() -> anyhow::Result<()> { + fn comprehension_with_not_iterable_iter_in_second_comprehension() -> anyhow::Result<()> { let mut db = setup_db(); db.write_dedented( - "/src/a.py", + "src/a.py", " - def add(x: int, y: int) -> int: - return x + y + def foo(): + [z for x in IntIterable() for z in x] + + class IntIterator: + def __next__(self) -> int: + return 42 - a = 'abcde'[add(0, 1)] + class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() ", )?; - // TODO overloads... - assert_public_ty(&db, "/src/a.py", "a", "@Todo"); + assert_scope_ty(&db, "src/a.py", &["foo", ""], "x", "int"); + assert_scope_ty(&db, "src/a.py", &["foo", ""], "z", "Unknown"); + assert_file_diagnostics(&db, "src/a.py", &["Object of type `int` is not iterable"]); Ok(()) } #[test] - fn subscript_getitem() -> anyhow::Result<()> { + fn dict_comprehension_variable_key() -> anyhow::Result<()> { let mut db = setup_db(); db.write_dedented( - "/src/a.py", + "src/a.py", " - class Identity: - def __getitem__(self, index: int) -> int: - return index + def foo(): + {x: 0 for x in IntIterable()} + + class IntIterator: + def __next__(self) -> int: + return 42 - a = Identity()[0] + class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() ", )?; - assert_public_ty(&db, "/src/a.py", "a", "int"); + assert_scope_ty(&db, "src/a.py", &["foo", ""], "x", "int"); + assert_file_diagnostics(&db, "src/a.py", &[]); Ok(()) } #[test] - fn subscript_class_getitem() -> anyhow::Result<()> { + fn dict_comprehension_variable_value() -> anyhow::Result<()> { let mut db = setup_db(); db.write_dedented( - "/src/a.py", + "src/a.py", " - class Identity: - def __class_getitem__(cls, item: int) -> str: - return item + def foo(): + {0: x for x in IntIterable()} + + class IntIterator: + def __next__(self) -> int: + return 42 - a = Identity[0] + class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() ", )?; - assert_public_ty(&db, "/src/a.py", "a", "str"); + assert_scope_ty(&db, "src/a.py", &["foo", ""], "x", "int"); + assert_file_diagnostics(&db, "src/a.py", &[]); Ok(()) } #[test] - fn subscript_getitem_union() -> anyhow::Result<()> { + fn comprehension_with_missing_in_keyword() -> anyhow::Result<()> { let mut db = setup_db(); db.write_dedented( - "/src/a.py", + "src/a.py", " - flag = True + def foo(): + [z for z IntIterable()] - class Identity: - if flag: - def __getitem__(self, index: int) -> int: - return index - else: - def __getitem__(self, index: int) -> str: - return str(index) + class IntIterator: + def __next__(self) -> int: + return 42 - a = Identity()[0] + class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() ", )?; - assert_public_ty(&db, "/src/a.py", "a", "int | str"); - + // We'll emit a diagnostic separately for invalid syntax, + // but it's reasonably clear here what they *meant* to write, + // so we'll still infer the correct type: + assert_scope_ty(&db, "src/a.py", &["foo", ""], "z", "int"); Ok(()) } #[test] - fn subscript_class_getitem_union() -> anyhow::Result<()> { + fn comprehension_with_missing_iter() -> anyhow::Result<()> { let mut db = setup_db(); db.write_dedented( - "/src/a.py", + "src/a.py", " - flag = True + def foo(): + [z for in IntIterable()] - class Identity: - if flag: - def __class_getitem__(cls, item: int) -> str: - return item - else: - def __class_getitem__(cls, item: int) -> int: - return item + class IntIterator: + def __next__(self) -> int: + return 42 - a = Identity[0] + class IntIterable: + def __iter__(self) -> IntIterator: + return IntIterator() ", )?; - assert_public_ty(&db, "/src/a.py", "a", "str | int"); + 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", &[]); Ok(()) } #[test] - fn subscript_class_getitem_class_union() -> anyhow::Result<()> { + fn comprehension_with_missing_for() -> anyhow::Result<()> { let mut db = setup_db(); + db.write_dedented("src/a.py", "[z for z in]")?; + assert_scope_ty(&db, "src/a.py", &[""], "z", "Unknown"); + Ok(()) + } - db.write_dedented( - "/src/a.py", - " - flag = True - - class Identity1: - def __class_getitem__(cls, item: int) -> str: - return item - - class Identity2: - def __class_getitem__(cls, item: int) -> int: - return item - - if flag: - a = Identity1 - else: - a = Identity2 - - b = a[0] - ", - )?; - - assert_public_ty(&db, "/src/a.py", "a", "Literal[Identity1, Identity2]"); - assert_public_ty(&db, "/src/a.py", "b", "str | int"); - + #[test] + fn comprehension_with_missing_in_keyword_and_missing_iter() -> anyhow::Result<()> { + let mut db = setup_db(); + db.write_dedented("src/a.py", "[z for z]")?; + assert_scope_ty(&db, "src/a.py", &[""], "z", "Unknown"); Ok(()) } + /// This tests that we understand that `async` comprehensions + /// do not work according to the synchronous iteration protocol #[test] - fn subscript_class_getitem_unbound_method_union() -> anyhow::Result<()> { + fn invalid_async_comprehension() -> anyhow::Result<()> { let mut db = setup_db(); db.write_dedented( - "/src/a.py", + "src/a.py", " - flag = True - - if flag: - class Identity: - def __class_getitem__(self, x: int) -> str: - pass - else: - class Identity: - pass - - a = Identity[42] + async def foo(): + [x async for x in Iterable()] + class Iterator: + def __next__(self) -> int: + return 42 + class Iterable: + def __iter__(self) -> Iterator: + return Iterator() ", )?; - assert_public_ty(&db, "/src/a.py", "a", "str | Unknown"); - - assert_file_diagnostics( - &db, - "/src/a.py", - &["Method `__class_getitem__` of type `Literal[__class_getitem__] | Unbound` is not callable on object of type `Literal[Identity, Identity]`"], - ); + // We currently return `Todo` for all async comprehensions, + // including comprehensions that have invalid syntax + assert_scope_ty(&db, "src/a.py", &["foo", ""], "x", "@Todo"); Ok(()) } #[test] - fn subscript_class_getitem_non_class_union() -> anyhow::Result<()> { + fn basic_async_comprehension() -> anyhow::Result<()> { let mut db = setup_db(); db.write_dedented( - "/src/a.py", + "src/a.py", " - flag = True - - if flag: - class Identity: - def __class_getitem__(self, x: int) -> str: - pass - else: - Identity = 1 - - a = Identity[42] + async def foo(): + [x async for x in AsyncIterable()] + class AsyncIterator: + async def __anext__(self) -> int: + return 42 + class AsyncIterable: + def __aiter__(self) -> AsyncIterator: + return AsyncIterator() ", )?; - // TODO this should _probably_ emit `str | Unknown` instead of `Unknown`. - assert_public_ty(&db, "/src/a.py", "a", "Unknown"); - - assert_file_diagnostics( - &db, - "/src/a.py", - &["Cannot subscript object of type `Literal[Identity] | Literal[1]` with no `__getitem__` method"], - ); + // TODO async iterables/iterators! --Alex + assert_scope_ty(&db, "src/a.py", &["foo", ""], "x", "@Todo"); Ok(()) } #[test] - fn dunder_call() -> anyhow::Result<()> { + fn starred_expressions_must_be_iterable() { let mut db = setup_db(); db.write_dedented( - "/src/a.py", + "src/a.py", " - class Multiplier: - def __init__(self, factor: float): - self.factor = factor - - def __call__(self, number: float) -> float: - return number * self.factor + class NotIterable: pass - a = Multiplier(2.0)(3.0) + class Iterator: + def __next__(self) -> int: + return 42 - class Unit: - ... + class Iterable: + def __iter__(self) -> Iterator: - b = Unit()(3.0) + x = [*NotIterable()] + y = [*Iterable()] ", - )?; + ) + .unwrap(); - assert_public_ty(&db, "/src/a.py", "a", "float"); - assert_public_ty(&db, "/src/a.py", "b", "Unknown"); + assert_file_diagnostics( + &db, + "/src/a.py", + &["Object of type `NotIterable` is not iterable"], + ); + } - assert_file_diagnostics(&db, "src/a.py", &["Object of type `Unit` is not callable"]); + // Incremental inference tests - Ok(()) + fn first_public_binding<'db>(db: &'db TestDb, file: File, name: &str) -> Definition<'db> { + let scope = global_scope(db, file); + use_def_map(db, scope) + .public_bindings(symbol_table(db, scope).symbol_id_by_name(name).unwrap()) + .next() + .unwrap() + .binding } #[test] - fn boolean_or_expression() -> anyhow::Result<()> { + fn dependency_public_symbol_type_change() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_dedented( - "/src/a.py", - " - def foo() -> str: - pass + db.write_files([ + ("/src/a.py", "from foo import x"), + ("/src/foo.py", "x = 10\ndef foo(): ..."), + ])?; - a = True or False - b = 'x' or 'y' or 'z' - c = '' or 'y' or 'z' - d = False or 'z' - e = False or True - f = False or False - g = foo() or False - h = foo() or True - ", - )?; + let a = system_path_to_file(&db, "/src/a.py").unwrap(); + let x_ty = global_symbol_ty(&db, a, "x"); - assert_public_ty(&db, "/src/a.py", "a", "Literal[True]"); - assert_public_ty(&db, "/src/a.py", "b", r#"Literal["x"]"#); - assert_public_ty(&db, "/src/a.py", "c", r#"Literal["y"]"#); - assert_public_ty(&db, "/src/a.py", "d", r#"Literal["z"]"#); - assert_public_ty(&db, "/src/a.py", "e", "Literal[True]"); - assert_public_ty(&db, "/src/a.py", "f", "Literal[False]"); - assert_public_ty(&db, "/src/a.py", "g", "str | Literal[False]"); - assert_public_ty(&db, "/src/a.py", "h", "str | Literal[True]"); + assert_eq!(x_ty.display(&db).to_string(), "Literal[10]"); - Ok(()) - } + // Change `x` to a different value + db.write_file("/src/foo.py", "x = 20\ndef foo(): ...")?; - #[test] - fn boolean_and_expression() -> anyhow::Result<()> { - let mut db = setup_db(); + let a = system_path_to_file(&db, "/src/a.py").unwrap(); - db.write_dedented( - "/src/a.py", - " - def foo() -> str: - pass + let x_ty_2 = global_symbol_ty(&db, a, "x"); - a = True and False - b = False and True - c = foo() and False - d = foo() and True - e = 'x' and 'y' and 'z' - f = 'x' and 'y' and '' - g = '' and 'y' - ", - )?; + assert_eq!(x_ty_2.display(&db).to_string(), "Literal[20]"); - assert_public_ty(&db, "/src/a.py", "a", "Literal[False]"); - assert_public_ty(&db, "/src/a.py", "b", "Literal[False]"); - assert_public_ty(&db, "/src/a.py", "c", "str | Literal[False]"); - assert_public_ty(&db, "/src/a.py", "d", "str | Literal[True]"); - assert_public_ty(&db, "/src/a.py", "e", r#"Literal["z"]"#); - assert_public_ty(&db, "/src/a.py", "f", r#"Literal[""]"#); - assert_public_ty(&db, "/src/a.py", "g", r#"Literal[""]"#); Ok(()) } #[test] - fn boolean_complex_expression() -> anyhow::Result<()> { + fn dependency_internal_symbol_change() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_dedented( - "/src/a.py", - r#" - def foo() -> str: - pass + db.write_files([ + ("/src/a.py", "from foo import x"), + ("/src/foo.py", "x = 10\ndef foo(): y = 1"), + ])?; - a = "x" and "y" or "z" - b = "x" or "y" and "z" - c = "" and "y" or "z" - d = "" or "y" and "z" - e = "x" and "y" or "" - f = "x" or "y" and "" + let a = system_path_to_file(&db, "/src/a.py").unwrap(); + let x_ty = global_symbol_ty(&db, a, "x"); - "#, - )?; + assert_eq!(x_ty.display(&db).to_string(), "Literal[10]"); - assert_public_ty(&db, "/src/a.py", "a", r#"Literal["y"]"#); - assert_public_ty(&db, "/src/a.py", "b", r#"Literal["x"]"#); - assert_public_ty(&db, "/src/a.py", "c", r#"Literal["z"]"#); - assert_public_ty(&db, "/src/a.py", "d", r#"Literal["z"]"#); - assert_public_ty(&db, "/src/a.py", "e", r#"Literal["y"]"#); - assert_public_ty(&db, "/src/a.py", "f", r#"Literal["x"]"#); - Ok(()) - } + db.write_file("/src/foo.py", "x = 10\ndef foo(): pass")?; - #[test] - fn bool_function_falsy_values() -> anyhow::Result<()> { - let mut db = setup_db(); - db.write_dedented( - "/src/a.py", - r#" - a = bool(0) - b = bool(()) - c = bool(None) - d = bool("") - e = bool(False) - f = bool() - "#, - )?; - assert_public_ty(&db, "/src/a.py", "a", "Literal[False]"); - assert_public_ty(&db, "/src/a.py", "b", "Literal[False]"); - assert_public_ty(&db, "/src/a.py", "c", "Literal[False]"); - assert_public_ty(&db, "/src/a.py", "d", "Literal[False]"); - assert_public_ty(&db, "/src/a.py", "e", "Literal[False]"); - assert_public_ty(&db, "/src/a.py", "f", "Literal[False]"); - Ok(()) - } + let a = system_path_to_file(&db, "/src/a.py").unwrap(); - #[test] - fn builtin_bool_function_detected() -> anyhow::Result<()> { - let mut db = setup_db(); - db.write_dedented( - "/src/a.py", - " - redefined_builtin_bool = bool + db.clear_salsa_events(); - def my_bool(x)-> bool: pass - ", - )?; - db.write_dedented( - "/src/b.py", - " - from a import redefined_builtin_bool, my_bool - a = redefined_builtin_bool(0) - b = my_bool(0) - ", - )?; - assert_public_ty(&db, "/src/b.py", "a", "Literal[False]"); - assert_public_ty(&db, "/src/b.py", "b", "bool"); - Ok(()) - } + let x_ty_2 = global_symbol_ty(&db, a, "x"); - #[test] - fn bool_function_truthy_values() -> anyhow::Result<()> { - let mut db = setup_db(); - db.write_dedented( - "/src/a.py", - r#" - a = bool(1) - b = bool((0,)) - c = bool("NON EMPTY") - d = bool(True) + assert_eq!(x_ty_2.display(&db).to_string(), "Literal[10]"); - def foo(): pass - e = bool(foo) - "#, - )?; + let events = db.take_salsa_events(); + + assert_function_query_was_not_run( + &db, + infer_definition_types, + first_public_binding(&db, a, "x"), + &events, + ); - assert_public_ty(&db, "/src/a.py", "a", "Literal[True]"); - assert_public_ty(&db, "/src/a.py", "b", "Literal[True]"); - assert_public_ty(&db, "/src/a.py", "c", "Literal[True]"); - assert_public_ty(&db, "/src/a.py", "d", "Literal[True]"); - assert_public_ty(&db, "/src/a.py", "e", "Literal[True]"); Ok(()) } #[test] - fn bool_function_ambiguous_values() -> anyhow::Result<()> { + fn dependency_unrelated_symbol() -> anyhow::Result<()> { let mut db = setup_db(); - db.write_dedented( - "/src/a.py", - " - a = bool([]) - b = bool({}) - c = bool(set()) - ", - )?; - assert_public_ty(&db, "/src/a.py", "a", "bool"); - assert_public_ty(&db, "/src/a.py", "b", "bool"); - assert_public_ty(&db, "/src/a.py", "c", "bool"); - Ok(()) - } + db.write_files([ + ("/src/a.py", "from foo import x"), + ("/src/foo.py", "x = 10\ny = 20"), + ])?; - #[test] - fn unary_add() -> anyhow::Result<()> { - let mut db = setup_db(); + let a = system_path_to_file(&db, "/src/a.py").unwrap(); + let x_ty = global_symbol_ty(&db, a, "x"); - db.write_dedented( - "/src/a.py", - " - a = +0 - b = +1 - c = +True - ", - )?; + assert_eq!(x_ty.display(&db).to_string(), "Literal[10]"); - assert_public_ty(&db, "/src/a.py", "a", "Literal[0]"); - assert_public_ty(&db, "/src/a.py", "b", "Literal[1]"); - assert_public_ty(&db, "/src/a.py", "c", "Literal[1]"); - Ok(()) - } + db.write_file("/src/foo.py", "x = 10\ny = 30")?; - #[test] - fn unary_sub() -> anyhow::Result<()> { - let mut db = setup_db(); + let a = system_path_to_file(&db, "/src/a.py").unwrap(); - db.write_dedented( - "/src/a.py", - " - a = -0 - b = -1 - c = -True - ", - )?; + db.clear_salsa_events(); - assert_public_ty(&db, "/src/a.py", "a", "Literal[0]"); - assert_public_ty(&db, "/src/a.py", "b", "Literal[-1]"); - assert_public_ty(&db, "/src/a.py", "c", "Literal[-1]"); - Ok(()) - } + let x_ty_2 = global_symbol_ty(&db, a, "x"); - #[test] - fn unary_invert() -> anyhow::Result<()> { - let mut db = setup_db(); + assert_eq!(x_ty_2.display(&db).to_string(), "Literal[10]"); - db.write_dedented( - "/src/a.py", - " - a = ~0 - b = ~1 - c = ~True - ", - )?; + let events = db.take_salsa_events(); - assert_public_ty(&db, "/src/a.py", "a", "Literal[-1]"); - assert_public_ty(&db, "/src/a.py", "b", "Literal[-2]"); - assert_public_ty(&db, "/src/a.py", "c", "Literal[-2]"); + assert_function_query_was_not_run( + &db, + infer_definition_types, + first_public_binding(&db, a, "x"), + &events, + ); Ok(()) } } diff --git a/crates/red_knot_test/src/parser.rs b/crates/red_knot_test/src/parser.rs index 17a24d9beaf48..9d760f9036231 100644 --- a/crates/red_knot_test/src/parser.rs +++ b/crates/red_knot_test/src/parser.rs @@ -141,7 +141,7 @@ static HEADER_RE: Lazy = /// Matches a code block fenced by triple backticks, possibly with language and `key=val` /// configuration items following the opening backticks (in the "tag string" of the code block). static CODE_RE: Lazy = Lazy::new(|| { - Regex::new(r"^```(?\w+)(?( +\S+)*)\s*\n(?(.|\n)*?)\n```\s*\n").unwrap() + Regex::new(r"^```(?\w+)(?( +\S+)*)\s*\n(?(.|\n)*?)\n?```\s*\n").unwrap() }); #[derive(Debug)]