From 47f39ed1a07d9dde4b7a70383f169db36f4aa74e Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 21 Nov 2024 09:59:47 +0100 Subject: [PATCH] [red-knot] Meta data for `Type::Todo` (#14500) ## Summary Adds meta information to `Type::Todo`, allowing developers to easily trace back the origin of a particular `@Todo` type they encounter. Instead of `Type::Todo`, we now write either `type_todo!()` which creates a `@Todo[path/to/source.rs:123]` type with file and line information, or using `type_todo!("PEP 604 unions not supported")`, which creates a variant with a custom message. `Type::Todo` now contains a `TodoType` field. In release mode, this is just a zero-sized struct, in order not to create any overhead. In debug mode, this is an `enum` that contains the meta information. `Type` implements `Copy`, which means that `TodoType` also needs to be copyable. This limits the design space. We could intern `TodoType`, but I discarded this option, as it would require us to have access to the salsa DB everywhere we want to use `Type::Todo`. And it would have made the macro invocations less ergonomic (requiring us to pass `db`). So for now, the meta information is simply a `&'static str` / `u32` for the file/line variant, or a `&'static str` for the custom message. Anything involving a chain/backtrace of several `@Todo`s or similar is therefore currently not implemented. Also because we currently don't see any direct use cases for this, and because all of this will eventually go away. Note that the size of `Type` increases from 16 to 24 bytes, but only in debug mode. ## Test Plan - Observed the changes in Markdown tests. - Added custom messages for all `Type::Todo`s that were revealed in the tests - Ran red knot in release and debug mode on the following Python file: ```py def f(x: int) -> int: reveal_type(x) ``` Prints `@Todo` in release mode and `@Todo(function parameter type)` in debug mode. --- .../resources/mdtest/annotations/starred.md | 4 +- .../mdtest/assignment/annotations.md | 8 +- .../resources/mdtest/binary/instances.md | 6 +- .../resources/mdtest/call/function.md | 4 +- .../resources/mdtest/exception/basic.md | 6 +- .../resources/mdtest/generics.md | 4 +- .../resources/mdtest/literal/literal.md | 2 +- .../resources/mdtest/loops/async_for.md | 4 +- .../resources/mdtest/metaclass.md | 2 +- .../mdtest/scopes/moduletype_attrs.md | 9 +- .../resources/mdtest/subscript/bytes.md | 6 +- .../resources/mdtest/subscript/lists.md | 6 +- .../resources/mdtest/subscript/string.md | 6 +- .../resources/mdtest/subscript/tuple.md | 2 +- .../resources/mdtest/sys_version_info.md | 6 +- .../resources/mdtest/unpacking.md | 22 +- .../resources/mdtest/with/async.md | 2 +- crates/red_knot_python_semantic/src/types.rs | 195 ++++++++++++++---- .../src/types/builder.rs | 6 +- .../src/types/display.rs | 2 +- .../src/types/infer.rs | 108 +++++----- .../red_knot_python_semantic/src/types/mro.rs | 6 +- .../src/types/signatures.rs | 11 +- .../src/types/unpacker.rs | 6 +- 24 files changed, 280 insertions(+), 153 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/starred.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/starred.md index a125afce48..d3fcae5ce2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/starred.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/starred.md @@ -9,10 +9,10 @@ Ts = TypeVarTuple("Ts") def append_int(*args: *Ts) -> tuple[*Ts, int]: # TODO: should show some representation of the variadic generic type - reveal_type(args) # revealed: @Todo + reveal_type(args) # revealed: @Todo(function parameter type) return (*args, 1) # TODO should be tuple[Literal[True], Literal["a"], int] -reveal_type(append_int(True, "a")) # revealed: @Todo +reveal_type(append_int(True, "a")) # revealed: @Todo(full tuple[...] support) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md b/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md index 1dc8156d8b..ef401d4bf4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/annotations.md @@ -51,12 +51,12 @@ reveal_type(c) # revealed: tuple[str, int] reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]] # TODO: homogenous tuples, PEP-646 tuples -reveal_type(e) # revealed: @Todo -reveal_type(f) # revealed: @Todo -reveal_type(g) # revealed: @Todo +reveal_type(e) # revealed: @Todo(full tuple[...] support) +reveal_type(f) # revealed: @Todo(full tuple[...] support) +reveal_type(g) # revealed: @Todo(full tuple[...] support) # TODO: support more kinds of type expressions in annotations -reveal_type(h) # revealed: @Todo +reveal_type(h) # revealed: @Todo(full tuple[...] support) reveal_type(i) # revealed: tuple[str | int, str | int] reveal_type(j) # revealed: tuple[str | int] diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index efc619132c..15029fead9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -317,7 +317,7 @@ reveal_type(1 + A()) # revealed: int reveal_type(A() + "foo") # revealed: A # TODO should be `A` since `str.__add__` doesn't support `A` instances # TODO overloads -reveal_type("foo" + A()) # revealed: @Todo +reveal_type("foo" + A()) # revealed: @Todo(return type) reveal_type(A() + b"foo") # revealed: A # TODO should be `A` since `bytes.__add__` doesn't support `A` instances @@ -325,7 +325,7 @@ reveal_type(b"foo" + A()) # revealed: bytes reveal_type(A() + ()) # revealed: A # TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances -reveal_type(() + A()) # revealed: @Todo +reveal_type(() + A()) # revealed: @Todo(return type) literal_string_instance = "foo" * 1_000_000_000 # the test is not testing what it's meant to be testing if this isn't a `LiteralString`: @@ -334,7 +334,7 @@ reveal_type(literal_string_instance) # revealed: LiteralString reveal_type(A() + literal_string_instance) # revealed: A # TODO should be `A` since `str.__add__` doesn't support `A` instances # TODO overloads -reveal_type(literal_string_instance + A()) # revealed: @Todo +reveal_type(literal_string_instance + A()) # revealed: @Todo(return type) ``` ## Operations involving instances of classes inheriting from `Any` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/function.md b/crates/red_knot_python_semantic/resources/mdtest/call/function.md index 2cafcbe5cc..b67b14d558 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/function.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/function.md @@ -16,7 +16,7 @@ async def get_int_async() -> int: return 42 # TODO: we don't yet support `types.CoroutineType`, should be generic `Coroutine[Any, Any, int]` -reveal_type(get_int_async()) # revealed: @Todo +reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType) ``` ## Generic @@ -44,7 +44,7 @@ def bar() -> str: return "bar" # TODO: should reveal `int`, as the decorator replaces `bar` with `foo` -reveal_type(bar()) # revealed: @Todo +reveal_type(bar()) # revealed: @Todo(return type) ``` ## Invalid callable diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md b/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md index e03e51dcc6..c0d2d1e2f0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md +++ b/crates/red_knot_python_semantic/resources/mdtest/exception/basic.md @@ -50,11 +50,11 @@ def foo( help() except x as e: # TODO: should be `AttributeError` - reveal_type(e) # revealed: @Todo + reveal_type(e) # revealed: @Todo(exception type) except y as f: # TODO: should be `OSError | RuntimeError` - reveal_type(f) # revealed: @Todo + reveal_type(f) # revealed: @Todo(exception type) except z as g: # TODO: should be `BaseException` - reveal_type(g) # revealed: @Todo + reveal_type(g) # revealed: @Todo(exception type) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics.md b/crates/red_knot_python_semantic/resources/mdtest/generics.md index efb850b305..cc1eb392a9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics.md @@ -18,7 +18,7 @@ box: MyBox[int] = MyBox(5) wrong_innards: MyBox[int] = MyBox("five") # TODO reveal int -reveal_type(box.data) # revealed: @Todo +reveal_type(box.data) # revealed: @Todo(instance attributes) reveal_type(MyBox.box_model_number) # revealed: Literal[695] ``` @@ -39,7 +39,7 @@ class MySecureBox[T](MyBox[T]): ... secure_box: MySecureBox[int] = MySecureBox(5) reveal_type(secure_box) # revealed: MySecureBox # TODO reveal int -reveal_type(secure_box.data) # revealed: @Todo +reveal_type(secure_box.data) # revealed: @Todo(instance attributes) ``` ## Cyclical class definition diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/literal.md b/crates/red_knot_python_semantic/resources/mdtest/literal/literal.md index 92e76b45fd..0fb01df9a7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/literal/literal.md +++ b/crates/red_knot_python_semantic/resources/mdtest/literal/literal.md @@ -78,7 +78,7 @@ from other import Literal a1: Literal[26] def f(): - reveal_type(a1) # revealed: @Todo + reveal_type(a1) # revealed: @Todo(generics) ``` ## Detecting typing_extensions.Literal diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/async_for.md b/crates/red_knot_python_semantic/resources/mdtest/loops/async_for.md index b9a6d083ec..c073576876 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/async_for.md +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/async_for.md @@ -18,7 +18,7 @@ async def foo(): pass # TODO: should reveal `Unknown` because `__aiter__` is not defined - # revealed: @Todo + # revealed: @Todo(async iterables/iterators) # error: [possibly-unresolved-reference] reveal_type(x) ``` @@ -40,6 +40,6 @@ async def foo(): pass # error: [possibly-unresolved-reference] - # revealed: @Todo + # revealed: @Todo(async iterables/iterators) reveal_type(x) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md index 1e9d726622..e6bcd6e43c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/metaclass.md +++ b/crates/red_knot_python_semantic/resources/mdtest/metaclass.md @@ -171,7 +171,7 @@ def f(*args, **kwargs) -> int: ... class A(metaclass=f): ... # TODO should be `type[int]` -reveal_type(A.__class__) # revealed: @Todo +reveal_type(A.__class__) # revealed: @Todo(metaclass not a class) ``` ## Cyclic diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md index 652d796825..df04fd71fb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md @@ -17,8 +17,7 @@ reveal_type(__doc__) # revealed: str | None # (needs support for `*` imports) reveal_type(__spec__) # revealed: Unknown | None -# TODO: generics -reveal_type(__path__) # revealed: @Todo +reveal_type(__path__) # revealed: @Todo(generics) class X: reveal_type(__name__) # revealed: str @@ -64,7 +63,7 @@ reveal_type(typing.__class__) # revealed: Literal[type] # TODO: needs support for attribute access on instances, properties and generics; # should be `dict[str, Any]` -reveal_type(typing.__dict__) # revealed: @Todo +reveal_type(typing.__dict__) # revealed: @Todo(instance attributes) ``` Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with @@ -96,8 +95,8 @@ from foo import __dict__ as foo_dict # TODO: needs support for attribute access on instances, properties, and generics; # should be `dict[str, Any]` for both of these: -reveal_type(foo.__dict__) # revealed: @Todo -reveal_type(foo_dict) # revealed: @Todo +reveal_type(foo.__dict__) # revealed: @Todo(instance attributes) +reveal_type(foo_dict) # revealed: @Todo(instance attributes) ``` ## Conditionally global or `ModuleType` attribute diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md index acaa115035..71c7775b76 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md @@ -27,7 +27,7 @@ def int_instance() -> int: a = b"abcde"[int_instance()] # TODO: Support overloads... Should be `bytes` -reveal_type(a) # revealed: @Todo +reveal_type(a) # revealed: @Todo(return type) ``` ## Slices @@ -47,11 +47,11 @@ def int_instance() -> int: ... byte_slice1 = b[int_instance() : int_instance()] # TODO: Support overloads... Should be `bytes` -reveal_type(byte_slice1) # revealed: @Todo +reveal_type(byte_slice1) # revealed: @Todo(return type) def bytes_instance() -> bytes: ... byte_slice2 = bytes_instance()[0:5] # TODO: Support overloads... Should be `bytes` -reveal_type(byte_slice2) # revealed: @Todo +reveal_type(byte_slice2) # revealed: @Todo(return type) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md index 8e5fe60788..0ebd8c83af 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md @@ -12,13 +12,13 @@ x = [1, 2, 3] reveal_type(x) # revealed: list # TODO reveal int -reveal_type(x[0]) # revealed: @Todo +reveal_type(x[0]) # revealed: @Todo(return type) # TODO reveal list -reveal_type(x[0:1]) # revealed: @Todo +reveal_type(x[0:1]) # revealed: @Todo(return type) # TODO error -reveal_type(x["a"]) # revealed: @Todo +reveal_type(x["a"]) # revealed: @Todo(return type) ``` ## Assignments within list assignment diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md index ae8099196d..4627407459 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md @@ -23,7 +23,7 @@ def int_instance() -> int: ... a = "abcde"[int_instance()] # TODO: Support overloads... Should be `str` -reveal_type(a) # revealed: @Todo +reveal_type(a) # revealed: @Todo(return type) ``` ## Slices @@ -78,13 +78,13 @@ def int_instance() -> int: ... substring1 = s[int_instance() : int_instance()] # TODO: Support overloads... Should be `LiteralString` -reveal_type(substring1) # revealed: @Todo +reveal_type(substring1) # revealed: @Todo(return type) def str_instance() -> str: ... substring2 = str_instance()[0:5] # TODO: Support overloads... Should be `str` -reveal_type(substring2) # revealed: @Todo +reveal_type(substring2) # revealed: @Todo(return type) ``` ## Unsupported slice types diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md index 38dea938eb..cb088f009f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md @@ -71,5 +71,5 @@ def int_instance() -> int: ... tuple_slice = t[int_instance() : int_instance()] # TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]` -reveal_type(tuple_slice) # revealed: @Todo +reveal_type(tuple_slice) # revealed: @Todo(return type) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/sys_version_info.md b/crates/red_knot_python_semantic/resources/mdtest/sys_version_info.md index a9c175a0e7..1622720220 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/sys_version_info.md +++ b/crates/red_knot_python_semantic/resources/mdtest/sys_version_info.md @@ -112,9 +112,9 @@ properties on instance types: ```py path=b.py import sys -reveal_type(sys.version_info.micro) # revealed: @Todo -reveal_type(sys.version_info.releaselevel) # revealed: @Todo -reveal_type(sys.version_info.serial) # revealed: @Todo +reveal_type(sys.version_info.micro) # revealed: @Todo(instance attributes) +reveal_type(sys.version_info.releaselevel) # revealed: @Todo(instance attributes) +reveal_type(sys.version_info.serial) # revealed: @Todo(instance attributes) ``` ## Accessing fields by index/slice diff --git a/crates/red_knot_python_semantic/resources/mdtest/unpacking.md b/crates/red_knot_python_semantic/resources/mdtest/unpacking.md index 6781cbb438..0e9c3943a9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/unpacking.md +++ b/crates/red_knot_python_semantic/resources/mdtest/unpacking.md @@ -84,7 +84,7 @@ reveal_type(b) # revealed: Literal[2] [a, *b, c, d] = (1, 2) reveal_type(a) # revealed: Literal[1] # TODO: Should be list[Any] once support for assigning to starred expression is added -reveal_type(b) # revealed: @Todo +reveal_type(b) # revealed: @Todo(starred unpacking) reveal_type(c) # revealed: Literal[2] reveal_type(d) # revealed: Unknown ``` @@ -95,7 +95,7 @@ reveal_type(d) # revealed: Unknown [a, *b, c] = (1, 2) reveal_type(a) # revealed: Literal[1] # TODO: Should be list[Any] once support for assigning to starred expression is added -reveal_type(b) # revealed: @Todo +reveal_type(b) # revealed: @Todo(starred unpacking) reveal_type(c) # revealed: Literal[2] ``` @@ -105,7 +105,7 @@ reveal_type(c) # revealed: Literal[2] [a, *b, c] = (1, 2, 3) reveal_type(a) # revealed: Literal[1] # TODO: Should be list[int] once support for assigning to starred expression is added -reveal_type(b) # revealed: @Todo +reveal_type(b) # revealed: @Todo(starred unpacking) reveal_type(c) # revealed: Literal[3] ``` @@ -115,7 +115,7 @@ reveal_type(c) # revealed: Literal[3] [a, *b, c, d] = (1, 2, 3, 4, 5, 6) reveal_type(a) # revealed: Literal[1] # TODO: Should be list[int] once support for assigning to starred expression is added -reveal_type(b) # revealed: @Todo +reveal_type(b) # revealed: @Todo(starred unpacking) reveal_type(c) # revealed: Literal[5] reveal_type(d) # revealed: Literal[6] ``` @@ -127,7 +127,7 @@ reveal_type(d) # revealed: Literal[6] reveal_type(a) # revealed: Literal[1] reveal_type(b) # revealed: Literal[2] # TODO: Should be list[int] once support for assigning to starred expression is added -reveal_type(c) # revealed: @Todo +reveal_type(c) # revealed: @Todo(starred unpacking) ``` ### Starred expression (6) @@ -138,7 +138,7 @@ reveal_type(c) # revealed: @Todo reveal_type(a) # revealed: Literal[1] reveal_type(b) # revealed: Unknown reveal_type(c) # revealed: Unknown -reveal_type(d) # revealed: @Todo +reveal_type(d) # revealed: @Todo(starred unpacking) reveal_type(e) # revealed: Unknown reveal_type(f) # revealed: Unknown ``` @@ -222,7 +222,7 @@ reveal_type(b) # revealed: LiteralString (a, *b, c, d) = "ab" reveal_type(a) # revealed: LiteralString # TODO: Should be list[LiteralString] once support for assigning to starred expression is added -reveal_type(b) # revealed: @Todo +reveal_type(b) # revealed: @Todo(starred unpacking) reveal_type(c) # revealed: LiteralString reveal_type(d) # revealed: Unknown ``` @@ -233,7 +233,7 @@ reveal_type(d) # revealed: Unknown (a, *b, c) = "ab" reveal_type(a) # revealed: LiteralString # TODO: Should be list[Any] once support for assigning to starred expression is added -reveal_type(b) # revealed: @Todo +reveal_type(b) # revealed: @Todo(starred unpacking) reveal_type(c) # revealed: LiteralString ``` @@ -243,7 +243,7 @@ reveal_type(c) # revealed: LiteralString (a, *b, c) = "abc" reveal_type(a) # revealed: LiteralString # TODO: Should be list[LiteralString] once support for assigning to starred expression is added -reveal_type(b) # revealed: @Todo +reveal_type(b) # revealed: @Todo(starred unpacking) reveal_type(c) # revealed: LiteralString ``` @@ -253,7 +253,7 @@ reveal_type(c) # revealed: LiteralString (a, *b, c, d) = "abcdef" reveal_type(a) # revealed: LiteralString # TODO: Should be list[LiteralString] once support for assigning to starred expression is added -reveal_type(b) # revealed: @Todo +reveal_type(b) # revealed: @Todo(starred unpacking) reveal_type(c) # revealed: LiteralString reveal_type(d) # revealed: LiteralString ``` @@ -265,5 +265,5 @@ reveal_type(d) # revealed: LiteralString reveal_type(a) # revealed: LiteralString reveal_type(b) # revealed: LiteralString # TODO: Should be list[int] once support for assigning to starred expression is added -reveal_type(c) # revealed: @Todo +reveal_type(c) # revealed: @Todo(starred unpacking) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/with/async.md b/crates/red_knot_python_semantic/resources/mdtest/with/async.md index 72aa4cad13..9b2b1439fb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/with/async.md +++ b/crates/red_knot_python_semantic/resources/mdtest/with/async.md @@ -17,5 +17,5 @@ class Manager: async def test(): async with Manager() as f: - reveal_type(f) # revealed: @Todo + reveal_type(f) # revealed: @Todo(async with statement) ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index f4ffa4e0a4..50362c34cc 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -324,6 +324,61 @@ fn declarations_ty<'db>( } } +/// Meta data for `Type::Todo`, which represents a known limitation in red-knot. +#[cfg(debug_assertions)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum TodoType { + FileAndLine(&'static str, u32), + Message(&'static str), +} + +#[cfg(debug_assertions)] +impl std::fmt::Display for TodoType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TodoType::FileAndLine(file, line) => write!(f, "[{file}:{line}]"), + TodoType::Message(msg) => write!(f, "({msg})"), + } + } +} + +#[cfg(not(debug_assertions))] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct TodoType; + +#[cfg(not(debug_assertions))] +impl std::fmt::Display for TodoType { + fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } +} + +/// Create a `Type::Todo` variant to represent a known limitation in the type system. +/// +/// It can be used with a custom message (preferred): `todo_type!("PEP 604 not supported")`, +/// or simply using `todo_type!()`, which will include information about the file and line. +#[cfg(debug_assertions)] +macro_rules! todo_type { + () => { + Type::Todo(crate::types::TodoType::FileAndLine(file!(), line!())) + }; + ($message:literal) => { + Type::Todo(crate::types::TodoType::Message($message)) + }; +} + +#[cfg(not(debug_assertions))] +macro_rules! todo_type { + () => { + Type::Todo(crate::types::TodoType) + }; + ($message:literal) => { + Type::Todo(crate::types::TodoType) + }; +} + +pub(crate) use todo_type; + /// Representation of a type: a set of possible values at runtime. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] pub enum Type<'db> { @@ -340,7 +395,9 @@ pub enum Type<'db> { /// General rule: `Todo` should only propagate when the presence of the input `Todo` caused the /// output to be unknown. An output should only be `Todo` if fixing all `Todo` inputs to be not /// `Todo` would change the output type. - Todo, + /// + /// This variant should be created with the `todo_type!` macro. + Todo(TodoType), /// The empty set of values Never, /// A specific function object @@ -384,7 +441,7 @@ impl<'db> Type<'db> { } pub const fn is_todo(&self) -> bool { - matches!(self, Type::Todo) + matches!(self, Type::Todo(_)) } pub const fn class_literal(class: Class<'db>) -> Self { @@ -530,8 +587,8 @@ impl<'db> Type<'db> { return true; } match (self, target) { - (Type::Unknown | Type::Any | Type::Todo, _) => false, - (_, Type::Unknown | Type::Any | Type::Todo) => false, + (Type::Unknown | Type::Any | Type::Todo(_), _) => false, + (_, Type::Unknown | Type::Any | Type::Todo(_)) => false, (Type::Never, _) => true, (_, Type::Never) => false, (_, Type::Instance(InstanceType { class })) @@ -666,8 +723,8 @@ impl<'db> Type<'db> { return true; } match (self, target) { - (Type::Unknown | Type::Any | Type::Todo, _) => true, - (_, Type::Unknown | Type::Any | Type::Todo) => true, + (Type::Unknown | Type::Any | Type::Todo(_), _) => true, + (_, Type::Unknown | Type::Any | Type::Todo(_)) => true, (Type::Union(union), ty) => union .elements(db) .iter() @@ -703,6 +760,7 @@ impl<'db> Type<'db> { // of `NoneType` and `NoDefaultType` in typeshed. This should not be required anymore once // we understand `sys.version_info` branches. self == other + || matches!((self, other), (Type::Todo(_), Type::Todo(_))) || matches!((self, other), ( Type::Instance(InstanceType { class: self_class }), @@ -726,7 +784,7 @@ impl<'db> Type<'db> { (Type::Any, _) | (_, Type::Any) => false, (Type::Unknown, _) | (_, Type::Unknown) => false, - (Type::Todo, _) | (_, Type::Todo) => false, + (Type::Todo(_), _) | (_, Type::Todo(_)) => false, (Type::Union(union), other) | (other, Type::Union(union)) => union .elements(db) @@ -931,7 +989,7 @@ impl<'db> Type<'db> { Type::Any | Type::Never | Type::Unknown - | Type::Todo + | Type::Todo(_) | Type::IntLiteral(..) | Type::StringLiteral(..) | Type::BytesLiteral(..) @@ -1034,7 +1092,7 @@ impl<'db> Type<'db> { Type::Any | Type::Never | Type::Unknown - | Type::Todo + | Type::Todo(_) | Type::Union(..) | Type::Intersection(..) | Type::LiteralString => false, @@ -1052,12 +1110,12 @@ impl<'db> Type<'db> { Type::Any => Type::Any.into(), Type::Never => { // TODO: attribute lookup on Never type - Type::Todo.into() + todo_type!().into() } Type::Unknown => Type::Unknown.into(), Type::FunctionLiteral(_) => { // TODO: attribute lookup on function type - Type::Todo.into() + todo_type!().into() } Type::ModuleLiteral(file) => { // `__dict__` is a very special member that is never overridden by module globals; @@ -1107,7 +1165,7 @@ impl<'db> Type<'db> { Type::IntLiteral(Program::get(db).target_version(db).minor.into()) } // TODO MRO? get_own_instance_member, get_instance_member - _ => Type::Todo, + _ => todo_type!("instance attributes"), }; ty.into() } @@ -1149,36 +1207,36 @@ impl<'db> Type<'db> { Type::Intersection(_) => { // TODO perform the get_member on each type in the intersection // TODO return the intersection of those results - Type::Todo.into() + todo_type!().into() } Type::IntLiteral(_) => { // TODO raise error - Type::Todo.into() + todo_type!().into() } - Type::BooleanLiteral(_) => Type::Todo.into(), + Type::BooleanLiteral(_) => todo_type!().into(), Type::StringLiteral(_) => { // TODO defer to `typing.LiteralString`/`builtins.str` methods // from typeshed's stubs - Type::Todo.into() + todo_type!().into() } Type::LiteralString => { // TODO defer to `typing.LiteralString`/`builtins.str` methods // from typeshed's stubs - Type::Todo.into() + todo_type!().into() } Type::BytesLiteral(_) => { // TODO defer to Type::Instance().member - Type::Todo.into() + todo_type!().into() } Type::SliceLiteral(_) => { // TODO defer to `builtins.slice` methods - Type::Todo.into() + todo_type!().into() } Type::Tuple(_) => { // TODO: implement tuple methods - Type::Todo.into() + todo_type!().into() } - Type::Todo => Type::Todo.into(), + &todo @ Type::Todo(_) => todo.into(), } } @@ -1188,7 +1246,7 @@ impl<'db> Type<'db> { /// when `bool(x)` is called on an object `x`. fn bool(&self, db: &'db dyn Db) -> Truthiness { match self { - Type::Any | Type::Todo | Type::Never | Type::Unknown => Truthiness::Ambiguous, + Type::Any | Type::Todo(_) | Type::Never | Type::Unknown => Truthiness::Ambiguous, Type::FunctionLiteral(_) => Truthiness::AlwaysTrue, Type::ModuleLiteral(_) => Truthiness::AlwaysTrue, Type::ClassLiteral(_) => { @@ -1329,7 +1387,7 @@ impl<'db> Type<'db> { // `Any` is callable, and its return type is also `Any`. Type::Any => CallOutcome::callable(Type::Any), - Type::Todo => CallOutcome::callable(Type::Todo), + Type::Todo(_) => CallOutcome::callable(todo_type!()), Type::Unknown => CallOutcome::callable(Type::Unknown), @@ -1342,7 +1400,7 @@ impl<'db> Type<'db> { ), // TODO: intersection types - Type::Intersection(_) => CallOutcome::callable(Type::Todo), + Type::Intersection(_) => CallOutcome::callable(todo_type!()), _ => CallOutcome::not_callable(self), } @@ -1381,7 +1439,7 @@ impl<'db> Type<'db> { }; } - if matches!(self, Type::Unknown | Type::Any | Type::Todo) { + if matches!(self, Type::Unknown | Type::Any | Type::Todo(_)) { // Explicit handling of `Unknown` and `Any` necessary until `type[Unknown]` and // `type[Any]` are not defined as `Todo` anymore. return IterationOutcome::Iterable { element_ty: self }; @@ -1440,14 +1498,14 @@ impl<'db> Type<'db> { pub fn to_instance(&self, db: &'db dyn Db) -> Type<'db> { match self { Type::Any => Type::Any, - Type::Todo => Type::Todo, + todo @ Type::Todo(_) => *todo, Type::Unknown => Type::Unknown, Type::Never => Type::Never, Type::ClassLiteral(ClassLiteralType { class }) => Type::instance(*class), Type::SubclassOf(SubclassOfType { class }) => Type::instance(*class), Type::Union(union) => union.map(db, |element| element.to_instance(db)), // TODO: we can probably do better here: --Alex - Type::Intersection(_) => Type::Todo, + Type::Intersection(_) => todo_type!(), // TODO: calling `.to_instance()` on any of these should result in a diagnostic, // since they already indicate that the object is an instance of some kind: Type::BooleanLiteral(_) @@ -1478,7 +1536,7 @@ impl<'db> Type<'db> { Type::Unknown => Type::Unknown, // TODO map this to a new `Type::TypeVar` variant Type::KnownInstance(KnownInstanceType::TypeVar(_)) => *self, - _ => Type::Todo, + _ => todo_type!(), } } @@ -1553,8 +1611,8 @@ impl<'db> Type<'db> { // TODO: `type[Unknown]`? Type::Unknown => Type::Unknown, // TODO intersections - Type::Intersection(_) => Type::Todo, - Type::Todo => Type::Todo, + Type::Intersection(_) => todo_type!(), + todo @ Type::Todo(_) => *todo, } } @@ -2607,7 +2665,7 @@ impl<'db> Class<'db> { // TODO: If the metaclass is not a class, we should verify that it's a callable // which accepts the same arguments as `type.__new__` (otherwise error), and return // the meta-type of its return type. (And validate that is a class type?) - return Ok(Type::Todo); + return Ok(todo_type!("metaclass not a class")); }; // Reconcile all base classes' metaclasses with the candidate metaclass. @@ -2907,6 +2965,10 @@ impl<'db> TupleType<'db> { } } +// Make sure that the `Type` enum does not grow unexpectedly. +#[cfg(not(debug_assertions))] +static_assertions::assert_eq_size!(Type, [u8; 16]); + #[cfg(test)] pub(crate) mod tests { use super::*; @@ -2922,13 +2984,6 @@ pub(crate) mod tests { use ruff_python_ast as ast; use test_case::test_case; - #[cfg(target_pointer_width = "64")] - #[test] - fn no_bloat_enum_sizes() { - use std::mem::size_of; - assert_eq!(size_of::(), 16); - } - pub(crate) fn setup_db() -> TestDb { let db = TestDb::new(); @@ -2982,7 +3037,7 @@ pub(crate) mod tests { Ty::Unknown => Type::Unknown, Ty::None => Type::none(db), Ty::Any => Type::Any, - Ty::Todo => Type::Todo, + Ty::Todo => todo_type!("Ty::Todo"), Ty::IntLiteral(n) => Type::IntLiteral(n), Ty::StringLiteral(s) => Type::string_literal(db, s), Ty::BooleanLiteral(b) => Type::BooleanLiteral(b), @@ -3573,4 +3628,66 @@ pub(crate) mod tests { Ok(()) } + + /// All other tests also make sure that `Type::Todo` works as expected. This particular + /// test makes sure that we handle `Todo` types correctly, even if they originate from + /// different sources. + #[test] + fn todo_types() { + let db = setup_db(); + + let todo1 = todo_type!("1"); + let todo2 = todo_type!("2"); + let todo3 = todo_type!(); + let todo4 = todo_type!(); + + assert!(todo1.is_equivalent_to(&db, todo2)); + assert!(todo3.is_equivalent_to(&db, todo4)); + assert!(todo1.is_equivalent_to(&db, todo3)); + + assert!(todo1.is_subtype_of(&db, todo2)); + assert!(todo2.is_subtype_of(&db, todo1)); + + assert!(todo3.is_subtype_of(&db, todo4)); + assert!(todo4.is_subtype_of(&db, todo3)); + + assert!(todo1.is_subtype_of(&db, todo3)); + assert!(todo3.is_subtype_of(&db, todo1)); + + let int = KnownClass::Int.to_instance(&db); + + assert!(int.is_assignable_to(&db, todo1)); + assert!(int.is_assignable_to(&db, todo3)); + + assert!(todo1.is_assignable_to(&db, int)); + assert!(todo3.is_assignable_to(&db, int)); + + // We lose information when combining several `Todo` types. This is an + // acknowledged limitation of the current implementation. We can not + // easily store the meta information of several `Todo`s in a single + // variant, as `TodoType` needs to implement `Copy`, meaning it can't + // contain `Vec`/`Box`/etc., and can't be boxed itself. + // + // Lifting this restriction would require us to intern `TodoType` in + // salsa, but that would mean we would have to pass in `db` everywhere. + + // A union of several `Todo` types collapses to a single `Todo` type: + assert!(UnionType::from_elements(&db, vec![todo1, todo2, todo3, todo4]).is_todo()); + + // And similar for intersection types: + assert!(IntersectionBuilder::new(&db) + .add_positive(todo1) + .add_positive(todo2) + .add_positive(todo3) + .add_positive(todo4) + .build() + .is_todo()); + assert!(IntersectionBuilder::new(&db) + .add_positive(todo1) + .add_negative(todo2) + .add_positive(todo3) + .add_negative(todo4) + .build() + .is_todo()); + } } diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index d14716b537..067fccdcf2 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -309,7 +309,7 @@ impl<'db> InnerIntersectionBuilder<'db> { self.add_positive(db, *neg); } } - ty @ (Type::Any | Type::Unknown | Type::Todo) => { + ty @ (Type::Any | Type::Unknown | Type::Todo(_)) => { // Adding any of these types to the negative side of an intersection // is equivalent to adding it to the positive side. We do this to // simplify the representation. @@ -379,7 +379,7 @@ mod tests { use crate::program::{Program, SearchPathSettings}; use crate::python_version::PythonVersion; use crate::stdlib::typing_symbol; - use crate::types::{global_symbol, KnownClass, UnionBuilder}; + use crate::types::{global_symbol, todo_type, KnownClass, UnionBuilder}; use crate::ProgramSettings; use ruff_db::files::system_path_to_file; use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; @@ -987,7 +987,7 @@ mod tests { #[test_case(Type::Any)] #[test_case(Type::Unknown)] - #[test_case(Type::Todo)] + #[test_case(todo_type!())] fn build_intersection_t_and_negative_t_does_not_simplify(ty: Type) { let db = setup_db(); diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index fcfb5a0cdf..cf86187dbb 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -77,7 +77,7 @@ impl Display for DisplayRepresentation<'_> { } // `[Type::Todo]`'s display should be explicit that is not a valid display of // any other type - Type::Todo => f.write_str("@Todo"), + Type::Todo(todo) => write!(f, "@Todo{todo}"), Type::ModuleLiteral(file) => { write!(f, "", file.path(self.db)) } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 3f6cc5cfa7..a00f9d4e8b 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -52,11 +52,12 @@ use crate::types::diagnostic::{TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder use crate::types::mro::MroErrorKind; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ - bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, typing_extensions_symbol, - Boundness, Class, ClassLiteralType, FunctionType, InstanceType, IntersectionBuilder, - IntersectionType, IterationOutcome, KnownClass, KnownFunction, KnownInstanceType, - MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, Symbol, Truthiness, TupleType, Type, - TypeArrayDisplay, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType, + bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, todo_type, + typing_extensions_symbol, Boundness, Class, ClassLiteralType, FunctionType, InstanceType, + IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction, + KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, Symbol, + Truthiness, TupleType, Type, TypeArrayDisplay, TypeVarBoundOrConstraints, TypeVarInstance, + UnionBuilder, UnionType, }; use crate::unpack::Unpack; use crate::util::subscript::{PyIndex, PySlice}; @@ -1027,7 +1028,7 @@ impl<'db> TypeInferenceBuilder<'db> { ) { // TODO(dhruvmanila): Annotation expression is resolved at the enclosing scope, infer the // parameter type from there - let annotated_ty = Type::Todo; + let annotated_ty = todo_type!("function parameter type"); if parameter.annotation.is_some() { self.add_declaration_with_binding( parameter.into(), @@ -1236,7 +1237,7 @@ impl<'db> TypeInferenceBuilder<'db> { ) -> Type<'db> { // TODO: Handle async with statements (they use `aenter` and `aexit`) if is_async { - return Type::Todo; + return todo_type!("async with statement"); } let context_manager_ty = context_expression_ty.to_meta_type(self.db); @@ -1386,12 +1387,12 @@ impl<'db> TypeInferenceBuilder<'db> { self.db, tuple.elements(self.db).iter().map(|ty| { ty.into_class_literal() - .map_or(Type::Todo, |ClassLiteralType { class }| { + .map_or(todo_type!(), |ClassLiteralType { class }| { Type::instance(class) }) }), ), - _ => Type::Todo, + _ => todo_type!("exception type"), } }; @@ -1461,7 +1462,7 @@ impl<'db> TypeInferenceBuilder<'db> { default, } = node; self.infer_optional_expression(default.as_deref()); - self.add_declaration_with_binding(node.into(), definition, Type::Todo, Type::Todo); + self.add_declaration_with_binding(node.into(), definition, todo_type!(), todo_type!()); } fn infer_typevartuple_definition( @@ -1475,7 +1476,7 @@ impl<'db> TypeInferenceBuilder<'db> { default, } = node; self.infer_optional_expression(default.as_deref()); - self.add_declaration_with_binding(node.into(), definition, Type::Todo, Type::Todo); + self.add_declaration_with_binding(node.into(), definition, todo_type!(), todo_type!()); } fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) { @@ -1510,7 +1511,7 @@ impl<'db> TypeInferenceBuilder<'db> { // against the subject expression type (which we can query via `infer_expression_types`) // and extract the type at the `index` position if the pattern matches. This will be // similar to the logic in `self.infer_assignment_definition`. - self.add_binding(pattern.into(), definition, Type::Todo); + self.add_binding(pattern.into(), definition, todo_type!()); } fn infer_match_pattern(&mut self, pattern: &ast::Pattern) { @@ -1874,8 +1875,7 @@ impl<'db> TypeInferenceBuilder<'db> { let iterable_ty = self.infer_standalone_expression(iterable); let loop_var_value_ty = if is_async { - // TODO(Alex): async iterables/iterators! - Type::Todo + todo_type!("async iterables/iterators") } else { iterable_ty .iterate(self.db) @@ -2203,7 +2203,7 @@ impl<'db> TypeInferenceBuilder<'db> { ast::Expr::Await(await_expression) => self.infer_await_expression(await_expression), ast::Expr::IpyEscapeCommand(_) => { // TODO Implement Ipy escape command support - Type::Todo + todo_type!() } }; @@ -2397,7 +2397,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_first_comprehension_iter(generators); // TODO generator type - Type::Todo + todo_type!() } fn infer_list_comprehension_expression(&mut self, listcomp: &ast::ExprListComp) -> Type<'db> { @@ -2410,7 +2410,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_first_comprehension_iter(generators); // TODO list type - Type::Todo + todo_type!() } fn infer_dict_comprehension_expression(&mut self, dictcomp: &ast::ExprDictComp) -> Type<'db> { @@ -2424,7 +2424,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_first_comprehension_iter(generators); // TODO dict type - Type::Todo + todo_type!() } fn infer_set_comprehension_expression(&mut self, setcomp: &ast::ExprSetComp) -> Type<'db> { @@ -2437,7 +2437,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_first_comprehension_iter(generators); // TODO set type - Type::Todo + todo_type!() } fn infer_generator_expression_scope(&mut self, generator: &ast::ExprGenerator) { @@ -2552,7 +2552,7 @@ impl<'db> TypeInferenceBuilder<'db> { let target_ty = if is_async { // TODO: async iterables/iterators! -- Alex - Type::Todo + todo_type!("async iterables/iterators") } else { iterable_ty .iterate(self.db) @@ -2642,7 +2642,7 @@ impl<'db> TypeInferenceBuilder<'db> { } // TODO function type - Type::Todo + todo_type!() } fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> { @@ -2673,7 +2673,7 @@ impl<'db> TypeInferenceBuilder<'db> { .unwrap_with_diagnostic(value.as_ref().into(), &mut self.diagnostics); // TODO - Type::Todo + todo_type!() } fn infer_yield_expression(&mut self, yield_expression: &ast::ExprYield) -> Type<'db> { @@ -2682,7 +2682,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_optional_expression(value.as_deref()); // TODO awaitable type - Type::Todo + todo_type!() } fn infer_yield_from_expression(&mut self, yield_from: &ast::ExprYieldFrom) -> Type<'db> { @@ -2694,7 +2694,7 @@ impl<'db> TypeInferenceBuilder<'db> { .unwrap_with_diagnostic(value.as_ref().into(), &mut self.diagnostics); // TODO get type from `ReturnType` of generator - Type::Todo + todo_type!() } fn infer_await_expression(&mut self, await_expression: &ast::ExprAwait) -> Type<'db> { @@ -2703,7 +2703,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_expression(value); // TODO awaitable type - Type::Todo + todo_type!() } /// Look up a name reference that isn't bound in the local scope. @@ -2979,7 +2979,7 @@ impl<'db> TypeInferenceBuilder<'db> { Type::Unknown } } - _ => Type::Todo, // TODO other unary op types + _ => todo_type!(), // TODO other unary op types } } @@ -3227,7 +3227,7 @@ impl<'db> TypeInferenceBuilder<'db> { (left, Type::BooleanLiteral(bool_value), op) => { self.infer_binary_expression_type(left, Type::IntLiteral(i64::from(bool_value)), op) } - _ => Some(Type::Todo), // TODO + _ => Some(todo_type!()), // TODO } } @@ -3665,7 +3665,7 @@ impl<'db> TypeInferenceBuilder<'db> { ).expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`"); match eq_result { - Type::Todo => return Ok(Type::Todo), + todo @ Type::Todo(_) => return Ok(todo), ty => match ty.bool(self.db) { Truthiness::AlwaysTrue => eq_count += 1, Truthiness::AlwaysFalse => not_eq_count += 1, @@ -3690,7 +3690,7 @@ impl<'db> TypeInferenceBuilder<'db> { ); Ok(match eq_result { - Type::Todo => Type::Todo, + todo @ Type::Todo(_) => todo, ty => match ty.bool(self.db) { Truthiness::AlwaysFalse => Type::BooleanLiteral(op.is_is_not()), _ => KnownClass::Bool.to_instance(self.db), @@ -3745,7 +3745,7 @@ impl<'db> TypeInferenceBuilder<'db> { // TODO: handle more types _ => match op { ast::CmpOp::Is | ast::CmpOp::IsNot => Ok(KnownClass::Bool.to_instance(self.db)), - _ => Ok(Type::Todo), + _ => Ok(todo_type!()), }, } } @@ -3773,7 +3773,7 @@ impl<'db> TypeInferenceBuilder<'db> { match pairwise_eq_result { // If propagation is required, return the result as is - Type::Todo => return Ok(Type::Todo), + todo @ Type::Todo(_) => return Ok(todo), ty => match ty.bool(self.db) { // - AlwaysTrue : Continue to the next pair for lexicographic comparison Truthiness::AlwaysTrue => continue, @@ -4263,7 +4263,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_name_expression(name).in_type_expression(self.db) } ast::ExprContext::Invalid => Type::Unknown, - ast::ExprContext::Store | ast::ExprContext::Del => Type::Todo, + ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(), }, ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx { @@ -4271,7 +4271,7 @@ impl<'db> TypeInferenceBuilder<'db> { .infer_attribute_expression(attribute_expression) .in_type_expression(self.db), ast::ExprContext::Invalid => Type::Unknown, - ast::ExprContext::Store | ast::ExprContext::Del => Type::Todo, + ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(), }, ast::Expr::NoneLiteral(_literal) => Type::none(self.db), @@ -4281,14 +4281,14 @@ impl<'db> TypeInferenceBuilder<'db> { // TODO: an Ellipsis literal *on its own* does not have any meaning in annotation // expressions, but is meaningful in the context of a number of special forms. - ast::Expr::EllipsisLiteral(_literal) => Type::Todo, + ast::Expr::EllipsisLiteral(_literal) => todo_type!(), // Other literals do not have meaningful values in the annotation expression context. // However, we will we want to handle these differently when working with special forms, // since (e.g.) `123` is not valid in an annotation expression but `Literal[123]` is. - ast::Expr::BytesLiteral(_literal) => Type::Todo, - ast::Expr::NumberLiteral(_literal) => Type::Todo, - ast::Expr::BooleanLiteral(_literal) => Type::Todo, + ast::Expr::BytesLiteral(_literal) => todo_type!(), + ast::Expr::NumberLiteral(_literal) => todo_type!(), + ast::Expr::BooleanLiteral(_literal) => todo_type!(), ast::Expr::Subscript(subscript) => { let ast::ExprSubscript { @@ -4331,7 +4331,7 @@ impl<'db> TypeInferenceBuilder<'db> { // TODO PEP 646 ast::Expr::Starred(starred) => { self.infer_starred_expression(starred); - Type::Todo + todo_type!() } // Forms which are invalid in the context of annotation expressions: we infer their @@ -4476,7 +4476,7 @@ impl<'db> TypeInferenceBuilder<'db> { } let ty = if return_todo { - Type::Todo + todo_type!("full tuple[...] support") } else { Type::tuple(self.db, &element_types) }; @@ -4491,7 +4491,7 @@ impl<'db> TypeInferenceBuilder<'db> { single_element => { let single_element_ty = self.infer_type_expression(single_element); if element_could_alter_type_of_whole_tuple(single_element, single_element_ty) { - Type::Todo + todo_type!() } else { Type::tuple(self.db, &[single_element_ty]) } @@ -4507,13 +4507,13 @@ impl<'db> TypeInferenceBuilder<'db> { if let Some(ClassLiteralType { class }) = name_ty.into_class_literal() { Type::subclass_of(class) } else { - Type::Todo + todo_type!() } } // TODO: attributes, unions, subscripts, etc. _ => { self.infer_type_expression(slice); - Type::Todo + todo_type!() } } } @@ -4536,7 +4536,7 @@ impl<'db> TypeInferenceBuilder<'db> { } _ => { self.infer_type_expression(slice); - Type::Todo // TODO: generics + todo_type!("generics") } } } @@ -4574,7 +4574,7 @@ impl<'db> TypeInferenceBuilder<'db> { ), _ => self.infer_type_expression(parameters), }, - KnownInstanceType::TypeVar(_) => Type::Todo, + KnownInstanceType::TypeVar(_) => todo_type!(), } } @@ -4942,8 +4942,8 @@ fn perform_membership_test_comparison<'db>( compare_result_opt .map(|ty| { - if matches!(ty, Type::Todo) { - return Type::Todo; + if matches!(ty, Type::Todo(_)) { + return ty; } match op { @@ -5914,7 +5914,13 @@ mod tests { // We currently return `Todo` for all async comprehensions, // including comprehensions that have invalid syntax - assert_scope_ty(&db, "src/a.py", &["foo", ""], "x", "@Todo"); + assert_scope_ty( + &db, + "src/a.py", + &["foo", ""], + "x", + "@Todo(async iterables/iterators)", + ); Ok(()) } @@ -5938,7 +5944,13 @@ mod tests { )?; // TODO async iterables/iterators! --Alex - assert_scope_ty(&db, "src/a.py", &["foo", ""], "x", "@Todo"); + assert_scope_ty( + &db, + "src/a.py", + &["foo", ""], + "x", + "@Todo(async iterables/iterators)", + ); Ok(()) } diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/red_knot_python_semantic/src/types/mro.rs index 6883b396c6..5d37617fa8 100644 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ b/crates/red_knot_python_semantic/src/types/mro.rs @@ -5,7 +5,7 @@ use itertools::Either; use rustc_hash::FxHashSet; use super::{Class, ClassLiteralType, KnownClass, KnownInstanceType, Type}; -use crate::Db; +use crate::{types::todo_type, Db}; /// The inferred method resolution order of a given class. /// @@ -354,7 +354,7 @@ impl<'db> ClassBase<'db> { match ty { Type::Any => Some(Self::Any), Type::Unknown => Some(Self::Unknown), - Type::Todo => Some(Self::Todo), + Type::Todo(_) => Some(Self::Todo), Type::ClassLiteral(ClassLiteralType { class }) => Some(Self::Class(class)), Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs? Type::Intersection(_) => None, // TODO -- probably incorrect? @@ -406,7 +406,7 @@ impl<'db> From> for Type<'db> { fn from(value: ClassBase<'db>) -> Self { match value { ClassBase::Any => Type::Any, - ClassBase::Todo => Type::Todo, + ClassBase::Todo => todo_type!(), ClassBase::Unknown => Type::Unknown, ClassBase::Class(class) => Type::class_literal(class), } diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 976c472da4..406da42c25 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use super::{definition_expression_ty, Type}; -use crate::semantic_index::definition::Definition; use crate::Db; +use crate::{semantic_index::definition::Definition, types::todo_type}; use ruff_python_ast::{self as ast, name::Name}; /// A typed callable signature. @@ -18,7 +18,7 @@ impl<'db> Signature<'db> { pub(crate) fn todo() -> Self { Self { parameters: Parameters::todo(), - return_ty: Type::Todo, + return_ty: todo_type!("return type"), } } @@ -33,8 +33,7 @@ impl<'db> Signature<'db> { .as_ref() .map(|returns| { if function_node.is_async { - // TODO: generic `types.CoroutineType`! - Type::Todo + todo_type!("generic types.CoroutineType") } else { definition_expression_ty(db, definition, returns.as_ref()) } @@ -81,11 +80,11 @@ impl<'db> Parameters<'db> { Self { variadic: Some(Parameter { name: Some(Name::new_static("args")), - annotated_ty: Type::Todo, + annotated_ty: todo_type!(), }), keywords: Some(Parameter { name: Some(Name::new_static("kwargs")), - annotated_ty: Type::Todo, + annotated_ty: todo_type!(), }), ..Default::default() } diff --git a/crates/red_knot_python_semantic/src/types/unpacker.rs b/crates/red_knot_python_semantic/src/types/unpacker.rs index efbc0df6ff..2e8c39a9b2 100644 --- a/crates/red_knot_python_semantic/src/types/unpacker.rs +++ b/crates/red_knot_python_semantic/src/types/unpacker.rs @@ -6,7 +6,7 @@ use rustc_hash::FxHashMap; use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId}; use crate::semantic_index::symbol::ScopeId; -use crate::types::{Type, TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder}; +use crate::types::{todo_type, Type, TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder}; use crate::Db; /// Unpacks the value expression type to their respective targets. @@ -59,7 +59,7 @@ impl<'db> Unpacker<'db> { // TODO: Combine the types into a list type. If the // starred_element_types is empty, then it should be `List[Any]`. // combine_types(starred_element_types); - element_types.push(Type::Todo); + element_types.push(todo_type!("starred unpacking")); element_types.extend_from_slice( // SAFETY: Safe because of the length check above. @@ -72,7 +72,7 @@ impl<'db> Unpacker<'db> { // index. element_types.resize(elts.len() - 1, Type::Unknown); // TODO: This should be `list[Unknown]` - element_types.insert(starred_index, Type::Todo); + element_types.insert(starred_index, todo_type!("starred unpacking")); Cow::Owned(element_types) } } else {