diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md index db94217aa2..cf586bacc7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md @@ -73,12 +73,12 @@ qux = (foo, bar) reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]] # TODO: Infer "LiteralString" -reveal_type(foo.join(qux)) # revealed: @Todo(Attribute access on `StringLiteral` types) +reveal_type(foo.join(qux)) # revealed: @Todo(decorated method) template: LiteralString = "{}, {}" reveal_type(template) # revealed: Literal["{}, {}"] # TODO: Infer `LiteralString` -reveal_type(template.format(foo, bar)) # revealed: @Todo(Attribute access on `StringLiteral` types) +reveal_type(template.format(foo, bar)) # revealed: @Todo(decorated method) ``` ### Assignability diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index eee6f9f269..697c82badb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -1005,8 +1005,8 @@ reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None Some attributes are special-cased, however: ```py -reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions) -reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions) +reveal_type(f.__get__) # revealed: +reveal_type(f.__call__) # revealed: ``` ### Int-literal attributes @@ -1015,7 +1015,7 @@ Most attribute accesses on int-literal types are delegated to `builtins.int`, si integers are instances of that class: ```py -reveal_type((2).bit_length) # revealed: @Todo(bound method) +reveal_type((2).bit_length) # revealed: reveal_type((2).denominator) # revealed: @Todo(@property) ``` @@ -1029,11 +1029,11 @@ reveal_type((2).real) # revealed: Literal[2] ### Bool-literal attributes Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal -bols are instances of that class: +bools are instances of that class: ```py -reveal_type(True.__and__) # revealed: @Todo(bound method) -reveal_type(False.__or__) # revealed: @Todo(bound method) +reveal_type(True.__and__) # revealed: @Todo(decorated method) +reveal_type(False.__or__) # revealed: @Todo(decorated method) ``` Some attributes are special-cased, however: @@ -1048,8 +1048,8 @@ reveal_type(False.real) # revealed: Literal[0] All attribute access on literal `bytes` types is currently delegated to `buitins.bytes`: ```py -reveal_type(b"foo".join) # revealed: @Todo(bound method) -reveal_type(b"foo".endswith) # revealed: @Todo(bound method) +reveal_type(b"foo".join) # revealed: +reveal_type(b"foo".endswith) # revealed: ``` ## Instance attribute edge cases @@ -1136,6 +1136,42 @@ class C: reveal_type(C().x) # revealed: Unknown ``` +### Builtin types attributes + +This test can probably be removed eventually, but we currently include it because we do not yet +understand generic bases and protocols, and we want to make sure that we can still use builtin types +in our tests in the meantime. See the corresponding TODO in `Type::static_member` for more +information. + +```py +class C: + a_int: int = 1 + a_str: str = "a" + a_bytes: bytes = b"a" + a_bool: bool = True + a_float: float = 1.0 + a_complex: complex = 1 + 1j + a_tuple: tuple[int] = (1,) + a_range: range = range(1) + a_slice: slice = slice(1) + a_memoryview: memoryview = memoryview(b"a") + a_type: type = int + a_none: None = None + +reveal_type(C.a_int) # revealed: int +reveal_type(C.a_str) # revealed: str +reveal_type(C.a_bytes) # revealed: bytes +reveal_type(C.a_bool) # revealed: bool +reveal_type(C.a_float) # revealed: int | float +reveal_type(C.a_complex) # revealed: int | float | complex +reveal_type(C.a_tuple) # revealed: tuple[int] +reveal_type(C.a_range) # revealed: range +reveal_type(C.a_slice) # revealed: slice +reveal_type(C.a_memoryview) # revealed: memoryview +reveal_type(C.a_type) # revealed: type +reveal_type(C.a_none) # revealed: None +``` + ## References Some of the tests in the *Class and instance variables* section draw inspiration from diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md b/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md new file mode 100644 index 0000000000..20cbbf65ff --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md @@ -0,0 +1,73 @@ +# `inspect.getattr_static` + +`inspect.getattr_static` is a function that returns attributes of an object without invoking the +descriptor protocol (for caveats, see the [official documentation]). + +Consider the following example: + +```py +import inspect + +class Descriptor: + def __get__(self, instance, owner) -> str: + return 1 + +class C: + normal: int = 1 + descriptor: Descriptor = Descriptor() +``` + +If we access attributes on an instance of `C` as usual, the descriptor protocol is invoked, and we +get a type of `str` for the `descriptor` attribute: + +```py +c = C() + +reveal_type(c.normal) # revealed: int +reveal_type(c.descriptor) # revealed: str +``` + +However, if we use `inspect.getattr_static`, we can see the underlying `Descriptor` type: + +```py +reveal_type(inspect.getattr_static(c, "normal")) # revealed: int +reveal_type(inspect.getattr_static(c, "descriptor")) # revealed: Descriptor +``` + +For non-existent attributes, a default value can be provided: + +```py +reveal_type(inspect.getattr_static(C, "normal", "default-arg")) # revealed: int +reveal_type(inspect.getattr_static(C, "non_existent", "default-arg")) # revealed: Literal["default-arg"] +``` + +When a non-existent attribute is accessed without a default value, the runtime raises an +`AttributeError`. We could emit a diagnostic for this case, but that is currently not supported: + +```py +# TODO: we could emit a diagnostic here +reveal_type(inspect.getattr_static(C, "non_existent")) # revealed: Never +``` + +We can access attributes on objects of all kinds: + +```py +import sys + +reveal_type(inspect.getattr_static(sys, "platform")) # revealed: LiteralString +reveal_type(inspect.getattr_static(inspect, "getattr_static")) # revealed: Literal[getattr_static] + +reveal_type(inspect.getattr_static(1, "real")) # revealed: Literal[1] +``` + +(Implicit) instance attributes can also be accessed through `inspect.getattr_static`: + +```py +class D: + def __init__(self) -> None: + self.instance_attr: int = 1 + +reveal_type(inspect.getattr_static(D(), "instance_attr")) # revealed: int +``` + +[official documentation]: https://docs.python.org/3/library/inspect.html#inspect.getattr_static diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md new file mode 100644 index 0000000000..6ebe172564 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -0,0 +1,192 @@ +# Methods + +## Background: Functions as descriptors + +Say we have a simple class `C` with a function definition `f` inside its body: + +```py +class C: + def f(self, x: int) -> str: + return "a" +``` + +Whenever we access the `f` attribute through the class object itself (`C.f`) or through an instance +(`C().f`), this access happens via the descriptor protocol. Functions are (non-data) descriptors +because they implement a `__get__` method. This is crucial in making sure that method calls work as +expected. In general, the signature of the `__get__` method in the descriptor protocol is +`__get__(self, instance, owner)`. The `self` argument is the descriptor object itself (`f`). The +passed value for the `instance` argument depends on whether the attribute is accessed from the class +object (in which case it is `None`), or from an instance (in which case it is the instance of type +`C`). The `owner` argument is the class itself (`C` of type `Literal[C]`). To summarize: + +- `C.f` is equivalent to `getattr_static(C, "f").__get__(None, C)` +- `C().f` is equivalent to `getattr_static(C, "f").__get__(C(), C)` + +Here, `inspect.getattr_static` is used to bypass the descriptor protocol and directly access the +function attribute. The way the special `__get__` method *on functions* works like is as follows. In +the former case, if the `instance` attribute is `None`, it simply returns the function itself. In +the latter case, it returns a *bound method* object: + +```py +from inspect import getattr_static + +reveal_type(getattr_static(C, "f")) # revealed: Literal[f] + +reveal_type(getattr_static(C, "f").__get__) # revealed: + +reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: Literal[f] +reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: +``` + +In conclusion, this is why we see the following two types when accessing the `f` attribute on the +class object `C` and on an instance `C()`: + +```py +reveal_type(C.f) # revealed: Literal[f] +reveal_type(C().f) # revealed: +``` + +A bound method is a callable object that contains a reference to the `instance` that it was called +on (can be inspected via `__self__`), and the function object that it refers to (can be inspected +via `__func__`): + +```py +bound_method = C().f + +reveal_type(bound_method.__self__) # revealed: C +reveal_type(bound_method.__func__) # revealed: Literal[f] +``` + +When we call the bound method, the `instance` is implicitly passed as the first argument (`self`): + +```py +reveal_type(C().f(1)) # revealed: str +reveal_type(bound_method(1)) # revealed: str +``` + +When we call the function object itself, we need to pass the `instance` explicitly: + +```py +C.f(1) # error: [missing-argument] + +reveal_type(C.f(C(), 1)) # revealed: str +``` + +When we access methods from derived classes, they will be bound to instances of the derived class: + +```py +class D(C): + pass + +reveal_type(D().f) # revealed: +``` + +If we access an attribute on a bound method object itself, it will defer to `types.MethodType`: + +```py +reveal_type(bound_method.__hash__) # revealed: +``` + +## Basic method calls on class objects and instances + +```py +class Base: + def method_on_base(self, x: int | None) -> str: + return "a" + +class Derived(Base): + def method_on_derived(self, x: bytes) -> tuple[int, str]: + return (1, "a") + +reveal_type(Base().method_on_base(1)) # revealed: str +reveal_type(Base.method_on_base(Base(), 1)) # revealed: str + +Base().method_on_base("incorrect") # error: [invalid-argument-type] +Base().method_on_base() # error: [missing-argument] +Base().method_on_base(1, 2) # error: [too-many-positional-arguments] + +reveal_type(Derived().method_on_base(1)) # revealed: str +reveal_type(Derived().method_on_derived(b"abc")) # revealed: tuple[int, str] +reveal_type(Derived.method_on_base(Derived(), 1)) # revealed: str +reveal_type(Derived.method_on_derived(Derived(), b"abc")) # revealed: tuple[int, str] +``` + +## Method calls on literals + +### Boolean literals + +```py +reveal_type(True.bit_length()) # revealed: int +reveal_type(True.as_integer_ratio()) # revealed: tuple[int, Literal[1]] +``` + +### Integer literals + +```py +reveal_type((42).bit_length()) # revealed: int +``` + +### String literals + +```py +reveal_type("abcde".find("abc")) # revealed: int +reveal_type("foo".encode(encoding="utf-8")) # revealed: bytes + +"abcde".find(123) # error: [invalid-argument-type] +``` + +### Bytes literals + +```py +reveal_type(b"abcde".startswith(b"abc")) # revealed: bool +``` + +## Method calls on `LiteralString` + +```py +from typing_extensions import LiteralString + +def f(s: LiteralString) -> None: + reveal_type(s.find("a")) # revealed: int +``` + +## Method calls on `tuple` + +```py +def f(t: tuple[int, str]) -> None: + reveal_type(t.index("a")) # revealed: int +``` + +## Method calls on unions + +```py +from typing import Any + +class A: + def f(self) -> int: + return 1 + +class B: + def f(self) -> str: + return "a" + +def f(a_or_b: A | B, any_or_a: Any | A): + reveal_type(a_or_b.f) # revealed: | + reveal_type(a_or_b.f()) # revealed: int | str + + reveal_type(any_or_a.f) # revealed: Any | + reveal_type(any_or_a.f()) # revealed: Any | int +``` + +## Method calls on `KnownInstance` types + +```toml +[environment] +python-version = "3.12" +``` + +```py +type IntOrStr = int | str + +reveal_type(IntOrStr.__or__) # revealed: +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md index 13c79c4aad..1c422eaf4b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md @@ -22,22 +22,25 @@ class Ten: pass class C: - ten = Ten() + ten: Ten = Ten() c = C() -# TODO: this should be `Literal[10]` -reveal_type(c.ten) # revealed: Unknown | Ten +reveal_type(c.ten) # revealed: Literal[10] -# TODO: This should `Literal[10]` -reveal_type(C.ten) # revealed: Unknown | Ten +reveal_type(C.ten) # revealed: Literal[10] # These are fine: -c.ten = 10 +# TODO: This should not be an error +c.ten = 10 # error: [invalid-assignment] C.ten = 10 -# TODO: Both of these should be errors +# TODO: This should be an error, but the message needs to be improved. +# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Ten`" c.ten = 11 + +# TODO: This should be an error, but the message needs to be improved. +# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Literal[10]`" C.ten = 11 ``` @@ -57,24 +60,84 @@ class FlexibleInt: self._value = int(value) class C: - flexible_int = FlexibleInt() + flexible_int: FlexibleInt = FlexibleInt() c = C() -# TODO: should be `int | None` -reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt +reveal_type(c.flexible_int) # revealed: int | None +# TODO: These should not be errors +# error: [invalid-assignment] c.flexible_int = 42 # okay +# error: [invalid-assignment] c.flexible_int = "42" # also okay! -# TODO: should be `int | None` -reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt +reveal_type(c.flexible_int) # revealed: int | None -# TODO: should be an error +# TODO: This should be an error, but the message needs to be improved. +# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `flexible_int` of type `FlexibleInt`" c.flexible_int = None # not okay -# TODO: should be `int | None` -reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt +reveal_type(c.flexible_int) # revealed: int | None +``` + +## Data and non-data descriptors + +Descriptors that define `__set__` or `__delete__` are called *data descriptors* (e.g. properties), +while those that only define `__get__` are called non-data descriptors (e.g. functions, +`classmethod` or `staticmethod`). + +The precedence chain for attribute access is (1) data descriptors, (2) instance attributes, and (3) +non-data descriptors. + +```py +from typing import Literal + +class DataDescriptor: + def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]: + return "data" + + def __set__(self, instance: int, value) -> None: + pass + +class NonDataDescriptor: + def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]: + return "non-data" + +class C: + data_descriptor = DataDescriptor() + non_data_descriptor = NonDataDescriptor() + + def f(self): + # This explains why data descriptors come first in the precendence chain. If + # instance attributes would take priority, we would override the descriptor + # here. Instead, this calls `DataDescriptor.__set__`, i.e. it does not affect + # the type of the `data_descriptor` attribute. + self.data_descriptor = 1 + + # However, for non-data descriptors, instance attributes do take precedence. + # So it is possible to override them. + self.non_data_descriptor = 1 + +c = C() + +# TODO: This should ideally be `Unknown | Literal["data"]`. +# +# - Pyright also wrongly shows `int | Literal['data']` here +# - Mypy shows Literal["data"] here, but also shows Literal["non-data"] below. +# +reveal_type(c.data_descriptor) # revealed: Unknown | Literal["data", 1] + +reveal_type(c.non_data_descriptor) # revealed: Unknown | Literal["non-data", 1] + +reveal_type(C.data_descriptor) # revealed: Unknown | Literal["data"] + +reveal_type(C.non_data_descriptor) # revealed: Unknown | Literal["non-data"] + +# It is possible to override data descriptors via class objects. The following +# assignment does not call `DataDescriptor.__set__`. For this reason, we infer +# `Unknown | …` for all (descriptor) attributes. +C.data_descriptor = "something else" # This is okay ``` ## Built-in `property` descriptor @@ -101,7 +164,7 @@ c = C() reveal_type(c._name) # revealed: str | None # Should be `str` -reveal_type(c.name) # revealed: @Todo(bound method) +reveal_type(c.name) # revealed: @Todo(decorated method) # Should be `builtins.property` reveal_type(C.name) # revealed: Literal[name] @@ -142,7 +205,7 @@ reveal_type(c1) # revealed: @Todo(return type) reveal_type(C.get_name()) # revealed: @Todo(return type) # TODO: should be `str` -reveal_type(C("42").get_name()) # revealed: @Todo(bound method) +reveal_type(C("42").get_name()) # revealed: @Todo(decorated method) ``` ## Descriptors only work when used as class variables @@ -160,9 +223,10 @@ class Ten: class C: def __init__(self): - self.ten = Ten() + self.ten: Ten = Ten() -reveal_type(C().ten) # revealed: Unknown | Ten +# TODO: Should be Ten +reveal_type(C().ten) # revealed: Literal[10] ``` ## Descriptors distinguishing between class and instance access @@ -186,13 +250,115 @@ class Descriptor: return "called on class object" class C: - d = Descriptor() + d: Descriptor = Descriptor() # TODO: should be `Literal["called on class object"] -reveal_type(C.d) # revealed: Unknown | Descriptor +reveal_type(C.d) # revealed: LiteralString # TODO: should be `Literal["called on instance"] -reveal_type(C().d) # revealed: Unknown | Descriptor +reveal_type(C().d) # revealed: LiteralString +``` + +## Undeclared descriptor arguments + +If a descriptor attribute is not declared, we union with `Unknown`, just like for regular +attributes, since that attribute could be overwritten externally. Even data-descriptors with a +`__set__` method can be overwritten when accessed through a class object. + +```py +class Descriptor: + def __get__(self, instance: object, owner: type | None = None) -> int: + return 1 + + def __set__(self, instance: object, value: int) -> None: + pass + +class C: + descriptor = Descriptor() + +C.descriptor = "something else" + +reveal_type(C.descriptor) # revealed: Unknown | int +``` + +## Descriptors with incorrect `__get__` signature + +```py +class Descriptor: + # `__get__` method with missing parameters: + def __get__(self) -> int: + return 1 + +class C: + descriptor: Descriptor = Descriptor() + +# TODO: This should be an error +reveal_type(C.descriptor) # revealed: Descriptor +``` + +## Possibly-unbound `__get__` method + +```py +def _(flag: bool): + class MaybeDescriptor: + if flag: + def __get__(self, instance: object, owner: type | None = None) -> int: + return 1 + + class C: + descriptor: MaybeDescriptor = MaybeDescriptor() + + # TODO: This should be `MaybeDescriptor | int` + reveal_type(C.descriptor) # revealed: int +``` + +## Dunder methods + +Dunder methods are looked up on the meta type, but we still need to invoke the descriptor protocol: + +```py +class SomeCallable: + def __call__(self, x: int) -> str: + return "a" + +class Descriptor: + def __get__(self, instance: object, owner: type | None = None) -> SomeCallable: + return SomeCallable() + +class B: + __call__: Descriptor = Descriptor() + +b_instance = B() +reveal_type(b_instance(1)) # revealed: str + +b_instance("bla") # error: [invalid-argument-type] +``` + +## Functions as descriptors + +Functions are descriptors because they implement a `__get__` method. This is crucial in making sure +that method calls work as expected. See [this test suite](./call/methods.md) for more information. +Here, we only demonstrate how `__get__` works on functions: + +```py +from inspect import getattr_static + +def f(x: int) -> str: + return "a" + +reveal_type(f) # revealed: Literal[f] +reveal_type(f.__get__) # revealed: +reveal_type(f.__get__(None, f)) # revealed: Literal[f] +reveal_type(f.__get__(None, f)(1)) # revealed: str + +reveal_type(getattr_static(f, "__get__")) # revealed: +reveal_type(getattr_static(f, "__get__")(f, None, type(f))) # revealed: Literal[f] + +# Attribute access on the method-wrapper `f.__get__` falls back to `MethodWrapperType`: +reveal_type(f.__get__.__hash__) # revealed: + +# Attribute access on the wrapper-descriptor `getattr_static(f, "__get__")` falls back to `WrapperDescriptorType`: +reveal_type(getattr_static(f, "__get__").__qualname__) # revealed: @Todo(@property) ``` [descriptors]: https://docs.python.org/3/howto/descriptor.html 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 b2082bc1dd..9c585c9f5f 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 @@ -9,7 +9,7 @@ is unbound. ```py reveal_type(__name__) # revealed: str reveal_type(__file__) # revealed: str | None -reveal_type(__loader__) # revealed: LoaderProtocol | None +reveal_type(__loader__) # revealed: @Todo(instance attribute on class with dynamic base) | None reveal_type(__package__) # revealed: str | None reveal_type(__doc__) # revealed: str | None @@ -54,10 +54,10 @@ inside the module: import typing reveal_type(typing.__name__) # revealed: str -reveal_type(typing.__init__) # revealed: @Todo(bound method) +reveal_type(typing.__init__) # revealed: # These come from `builtins.object`, not `types.ModuleType`: -reveal_type(typing.__eq__) # revealed: @Todo(bound method) +reveal_type(typing.__eq__) # revealed: reveal_type(typing.__class__) # revealed: Literal[ModuleType] diff --git a/crates/red_knot_python_semantic/resources/mdtest/sys_platform.md b/crates/red_knot_python_semantic/resources/mdtest/sys_platform.md index 7b9c0cdf9c..6c980b65c6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/sys_platform.md +++ b/crates/red_knot_python_semantic/resources/mdtest/sys_platform.md @@ -66,6 +66,6 @@ It is [recommended](https://docs.python.org/3/library/sys.html#sys.platform) to ```py import sys -reveal_type(sys.platform.startswith("freebsd")) # revealed: @Todo(Attribute access on `LiteralString` types) -reveal_type(sys.platform.startswith("linux")) # revealed: @Todo(Attribute access on `LiteralString` types) +reveal_type(sys.platform.startswith("freebsd")) # revealed: bool +reveal_type(sys.platform.startswith("linux")) # revealed: bool ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md b/crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md index 5a9243a737..13fb8760fe 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md @@ -35,7 +35,7 @@ in strict mode. ```py def f(x: type): reveal_type(x) # revealed: type - reveal_type(x.__repr__) # revealed: @Todo(bound method) + reveal_type(x.__repr__) # revealed: class A: ... @@ -50,7 +50,7 @@ x: type = A() # error: [invalid-assignment] ```py def f(x: type[object]): reveal_type(x) # revealed: type - reveal_type(x.__repr__) # revealed: @Todo(bound method) + reveal_type(x.__repr__) # revealed: class A: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/truthiness.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/truthiness.md index 54f5858c98..85186e400b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/truthiness.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/truthiness.md @@ -75,3 +75,19 @@ class Boom: reveal_type(bool(Boom())) # revealed: bool ``` + +### Possibly unbound __bool__ method + +```py +from typing import Literal + +def flag() -> bool: + return True + +class PossiblyUnboundTrue: + if flag(): + def __bool__(self) -> Literal[True]: + return True + +reveal_type(bool(PossiblyUnboundTrue())) # revealed: bool +``` diff --git a/crates/red_knot_python_semantic/src/module_resolver/module.rs b/crates/red_knot_python_semantic/src/module_resolver/module.rs index 09444e64aa..d85a19a23d 100644 --- a/crates/red_knot_python_semantic/src/module_resolver/module.rs +++ b/crates/red_knot_python_semantic/src/module_resolver/module.rs @@ -109,6 +109,7 @@ pub enum KnownModule { #[allow(dead_code)] Abc, // currently only used in tests Collections, + Inspect, KnotExtensions, } @@ -123,6 +124,7 @@ impl KnownModule { Self::Sys => "sys", Self::Abc => "abc", Self::Collections => "collections", + Self::Inspect => "inspect", Self::KnotExtensions => "knot_extensions", } } @@ -149,6 +151,7 @@ impl KnownModule { "sys" => Some(Self::Sys), "abc" => Some(Self::Abc), "collections" => Some(Self::Collections), + "inspect" => Some(Self::Inspect), "knot_extensions" => Some(Self::KnotExtensions), _ => None, } diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 625a48689f..1173d75d0a 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -203,6 +203,8 @@ pub enum Type<'db> { Never, /// A specific function object FunctionLiteral(FunctionType<'db>), + /// A callable object + Callable(CallableType<'db>), /// A specific module object ModuleLiteral(ModuleLiteralType<'db>), /// A specific class object @@ -262,6 +264,11 @@ impl<'db> Type<'db> { matches!(self, Type::Never) } + fn is_none(&self, db: &'db dyn Db) -> bool { + self.into_instance() + .is_some_and(|instance| instance.class.is_known(db, KnownClass::NoneType)) + } + pub fn is_object(&self, db: &'db dyn Db) -> bool { self.into_instance() .is_some_and(|instance| instance.class.is_object(db)) @@ -461,6 +468,7 @@ impl<'db> Type<'db> { | Type::Dynamic(_) | Type::Never | Type::FunctionLiteral(_) + | Type::Callable(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::KnownInstance(_) @@ -611,6 +619,21 @@ impl<'db> Type<'db> { .to_instance(db) .is_subtype_of(db, target), + // The same reasoning applies for these special callable types: + (Type::Callable(CallableType::BoundMethod(_)), _) => KnownClass::MethodType + .to_instance(db) + .is_subtype_of(db, target), + (Type::Callable(CallableType::MethodWrapperDunderGet(_)), _) => { + KnownClass::WrapperDescriptorType + .to_instance(db) + .is_subtype_of(db, target) + } + (Type::Callable(CallableType::WrapperDescriptorDunderGet), _) => { + KnownClass::WrapperDescriptorType + .to_instance(db) + .is_subtype_of(db, target) + } + // A fully static heterogenous tuple type `A` is a subtype of a fully static heterogeneous tuple type `B` // iff the two tuple types have the same number of elements and each element-type in `A` is a subtype // of the element-type at the same index in `B`. (Now say that 5 times fast.) @@ -909,6 +932,11 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..) | Type::SliceLiteral(..) | Type::FunctionLiteral(..) + | Type::Callable( + CallableType::BoundMethod(..) + | CallableType::MethodWrapperDunderGet(..) + | CallableType::WrapperDescriptorDunderGet, + ) | Type::ModuleLiteral(..) | Type::ClassLiteral(..) | Type::KnownInstance(..)), @@ -918,6 +946,11 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..) | Type::SliceLiteral(..) | Type::FunctionLiteral(..) + | Type::Callable( + CallableType::BoundMethod(..) + | CallableType::MethodWrapperDunderGet(..) + | CallableType::WrapperDescriptorDunderGet, + ) | Type::ModuleLiteral(..) | Type::ClassLiteral(..) | Type::KnownInstance(..)), @@ -932,6 +965,7 @@ impl<'db> Type<'db> { | Type::BooleanLiteral(..) | Type::BytesLiteral(..) | Type::FunctionLiteral(..) + | Type::Callable(..) | Type::IntLiteral(..) | Type::SliceLiteral(..) | Type::StringLiteral(..) @@ -943,6 +977,7 @@ impl<'db> Type<'db> { | Type::BooleanLiteral(..) | Type::BytesLiteral(..) | Type::FunctionLiteral(..) + | Type::Callable(..) | Type::IntLiteral(..) | Type::SliceLiteral(..) | Type::StringLiteral(..) @@ -971,6 +1006,7 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..) | Type::SliceLiteral(..) | Type::FunctionLiteral(..) + | Type::Callable(..) | Type::ModuleLiteral(..), ) | ( @@ -981,6 +1017,7 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..) | Type::SliceLiteral(..) | Type::FunctionLiteral(..) + | Type::Callable(..) | Type::ModuleLiteral(..), Type::SubclassOf(_), ) => true, @@ -1085,6 +1122,33 @@ impl<'db> Type<'db> { !KnownClass::FunctionType.is_subclass_of(db, class) } + ( + Type::Callable(CallableType::BoundMethod(_)), + Type::Instance(InstanceType { class }), + ) + | ( + Type::Instance(InstanceType { class }), + Type::Callable(CallableType::BoundMethod(_)), + ) => !KnownClass::MethodType.is_subclass_of(db, class), + + ( + Type::Callable(CallableType::MethodWrapperDunderGet(_)), + Type::Instance(InstanceType { class }), + ) + | ( + Type::Instance(InstanceType { class }), + Type::Callable(CallableType::MethodWrapperDunderGet(_)), + ) => !KnownClass::MethodWrapperType.is_subclass_of(db, class), + + ( + Type::Callable(CallableType::WrapperDescriptorDunderGet), + Type::Instance(InstanceType { class }), + ) + | ( + Type::Instance(InstanceType { class }), + Type::Callable(CallableType::WrapperDescriptorDunderGet), + ) => !KnownClass::WrapperDescriptorType.is_subclass_of(db, class), + (Type::ModuleLiteral(..), other @ Type::Instance(..)) | (other @ Type::Instance(..), Type::ModuleLiteral(..)) => { // Modules *can* actually be instances of `ModuleType` subclasses @@ -1130,6 +1194,11 @@ impl<'db> Type<'db> { Type::Dynamic(_) => false, Type::Never | Type::FunctionLiteral(..) + | Type::Callable( + CallableType::BoundMethod(_) + | CallableType::MethodWrapperDunderGet(_) + | CallableType::WrapperDescriptorDunderGet, + ) | Type::ModuleLiteral(..) | Type::IntLiteral(_) | Type::BooleanLiteral(_) @@ -1194,6 +1263,11 @@ impl<'db> Type<'db> { Type::SubclassOf(..) => false, Type::BooleanLiteral(_) | Type::FunctionLiteral(..) + | Type::Callable( + CallableType::BoundMethod(_) + | CallableType::MethodWrapperDunderGet(_) + | CallableType::WrapperDescriptorDunderGet, + ) | Type::ClassLiteral(..) | Type::ModuleLiteral(..) | Type::KnownInstance(..) => true, @@ -1232,6 +1306,11 @@ impl<'db> Type<'db> { pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { match self { Type::FunctionLiteral(..) + | Type::Callable( + CallableType::BoundMethod(..) + | CallableType::MethodWrapperDunderGet(..) + | CallableType::WrapperDescriptorDunderGet, + ) | Type::ModuleLiteral(..) | Type::ClassLiteral(..) | Type::IntLiteral(..) @@ -1274,12 +1353,17 @@ impl<'db> Type<'db> { | KnownClass::FrozenSet | KnownClass::Dict | KnownClass::Slice + | KnownClass::Range + | KnownClass::MemoryView | KnownClass::Property | KnownClass::BaseException | KnownClass::BaseExceptionGroup | KnownClass::GenericAlias | KnownClass::ModuleType | KnownClass::FunctionType + | KnownClass::MethodType + | KnownClass::MethodWrapperType + | KnownClass::WrapperDescriptorType | KnownClass::SpecialForm | KnownClass::ChainMap | KnownClass::Counter @@ -1302,13 +1386,12 @@ impl<'db> Type<'db> { } } - /// Resolve a member access of a type. + /// Access an attribute of this type without invoking the descriptor protocol. This + /// method corresponds to `inspect.getattr_static(, name)`. /// - /// For example, if `foo` is `Type::Instance()`, - /// `foo.member(&db, "baz")` returns the type of `baz` attributes - /// as accessed from instances of the `Bar` class. + /// See also: [`Type::member`] #[must_use] - pub(crate) fn member(&self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + fn static_member(&self, db: &'db dyn Db, name: &str) -> Symbol<'db> { if name == "__class__" { return Symbol::bound(self.to_meta_type(db)); } @@ -1318,19 +1401,31 @@ impl<'db> Type<'db> { Type::Never => Symbol::todo("attribute lookup on Never"), - Type::FunctionLiteral(_) => match name { - "__get__" => Symbol::todo("`__get__` method on functions"), - "__call__" => Symbol::todo("`__call__` method on functions"), - _ => KnownClass::FunctionType.to_instance(db).member(db, name), - }, + Type::FunctionLiteral(_) => KnownClass::FunctionType + .to_instance(db) + .static_member(db, name), - Type::ModuleLiteral(module) => module.member(db, name), + Type::Callable(CallableType::BoundMethod(_)) => KnownClass::MethodType + .to_instance(db) + .static_member(db, name), + Type::Callable(CallableType::MethodWrapperDunderGet(_)) => { + KnownClass::MethodWrapperType + .to_instance(db) + .static_member(db, name) + } + Type::Callable(CallableType::WrapperDescriptorDunderGet) => { + KnownClass::WrapperDescriptorType + .to_instance(db) + .static_member(db, name) + } - Type::ClassLiteral(class_ty) => class_ty.member(db, name), + Type::ModuleLiteral(module) => module.static_member(db, name), - Type::SubclassOf(subclass_of_ty) => subclass_of_ty.member(db, name), + Type::ClassLiteral(class_ty) => class_ty.static_member(db, name), - Type::KnownInstance(known_instance) => known_instance.member(db, name), + Type::SubclassOf(subclass_of_ty) => subclass_of_ty.static_member(db, name), + + Type::KnownInstance(known_instance) => known_instance.static_member(db, name), Type::Instance(InstanceType { class }) => match (class.known(db), name) { (Some(KnownClass::VersionInfo), "major") => Symbol::bound(Type::IntLiteral( @@ -1339,99 +1434,204 @@ impl<'db> Type<'db> { (Some(KnownClass::VersionInfo), "minor") => Symbol::bound(Type::IntLiteral( Program::get(db).python_version(db).minor.into(), )), + (Some(KnownClass::FunctionType), "__get__") => { + Symbol::bound(Type::Callable(CallableType::WrapperDescriptorDunderGet)) + } + + // TODO: + // We currently hard-code the knowledge that the following known classes are not + // descriptors, i.e. that they have no `__get__` method. This is not wrong and + // potentially even beneficial for performance, but it's not very principled. + // This case can probably be removed eventually, but we include it at the moment + // because we make extensive use of these types in our test suite. Note that some + // builtin types are not included here, since they do not have generic bases and + // are correctly handled by the `instance_member` method. + ( + Some( + KnownClass::Str + | KnownClass::Bytes + | KnownClass::Tuple + | KnownClass::Slice + | KnownClass::Range + | KnownClass::MemoryView, + ), + "__get__", + ) => Symbol::Unbound, + _ => { let SymbolAndQualifiers(symbol, _) = class.instance_member(db, name); symbol } }, - Type::Union(union) => { - let mut builder = UnionBuilder::new(db); + Type::Union(union) => union.map_with_boundness(db, |elem| elem.static_member(db, name)), - let mut all_unbound = true; - let mut possibly_unbound = false; - for ty in union.elements(db) { - let ty_member = ty.member(db, name); - match ty_member { - Symbol::Unbound => { - possibly_unbound = true; - } - Symbol::Type(ty_member, member_boundness) => { - if member_boundness == Boundness::PossiblyUnbound { - possibly_unbound = true; - } - - all_unbound = false; - builder = builder.add(ty_member); - } - } - } - - if all_unbound { - Symbol::Unbound - } else { - Symbol::Type( - builder.build(), - if possibly_unbound { - Boundness::PossiblyUnbound - } else { - Boundness::Bound - }, - ) - } - } - - Type::Intersection(_) => { - // TODO perform the get_member on each type in the intersection - // TODO return the intersection of those results - Symbol::todo("Attribute access on `Intersection` types") + Type::Intersection(intersection) => { + intersection.map_with_boundness(db, |elem| elem.static_member(db, name)) } Type::IntLiteral(_) => match name { "real" | "numerator" => Symbol::bound(self), // TODO more attributes could probably be usefully special-cased - _ => KnownClass::Int.to_instance(db).member(db, name), + _ => KnownClass::Int.to_instance(db).static_member(db, name), }, Type::BooleanLiteral(bool_value) => match name { "real" | "numerator" => Symbol::bound(Type::IntLiteral(i64::from(*bool_value))), - _ => KnownClass::Bool.to_instance(db).member(db, name), + _ => KnownClass::Bool.to_instance(db).static_member(db, name), }, - Type::StringLiteral(_) => { - // TODO defer to `typing.LiteralString`/`builtins.str` methods - // from typeshed's stubs - Symbol::todo("Attribute access on `StringLiteral` types") + Type::StringLiteral(_) | Type::LiteralString => { + KnownClass::Str.to_instance(db).static_member(db, name) } - Type::LiteralString => { - // TODO defer to `typing.LiteralString`/`builtins.str` methods - // from typeshed's stubs - Symbol::todo("Attribute access on `LiteralString` types") - } - - Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db).member(db, name), + Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db).static_member(db, name), // We could plausibly special-case `start`, `step`, and `stop` here, // but it doesn't seem worth the complexity given the very narrow range of places // where we infer `SliceLiteral` types. - Type::SliceLiteral(_) => KnownClass::Slice.to_instance(db).member(db, name), + Type::SliceLiteral(_) => KnownClass::Slice.to_instance(db).static_member(db, name), - Type::Tuple(_) => { - // TODO: implement tuple methods - Symbol::todo("Attribute access on heterogeneous tuple types") - } + Type::Tuple(_) => KnownClass::Tuple.to_instance(db).static_member(db, name), Type::AlwaysTruthy | Type::AlwaysFalsy => match name { "__bool__" => { // TODO should be `Callable[[], Literal[True/False]]` Symbol::todo("`__bool__` for `AlwaysTruthy`/`AlwaysFalsy` Type variants") } - _ => Type::object(db).member(db, name), + _ => Type::object(db).static_member(db, name), }, } } + /// Call the `__get__(instance, owner)` method on a type, if it exists. + fn try_call_dunder_get( + &self, + db: &'db dyn Db, + instance: Option>, + owner: Type<'db>, + ) -> Option> { + // TODO: Handle possible-unboundness and errors from `__get__` calls. + + match self { + Type::Union(union) => { + let mut builder = UnionBuilder::new(db); + for elem in union.elements(db) { + let ty = if let Some(result) = elem.try_call_dunder_get(db, instance, owner) { + result + } else { + *elem + }; + builder = builder.add(ty); + } + Some(builder.build()) + } + Type::Intersection(intersection) => { + if !intersection.negative(db).is_empty() { + return Some(todo_type!( + "try_call_dunder_get: intersections with negative contributions" + )); + } + + let mut builder = IntersectionBuilder::new(db); + for elem in intersection.positive(db) { + let ty = if let Some(result) = elem.try_call_dunder_get(db, instance, owner) { + result + } else { + *elem + }; + builder = builder.add_positive(ty); + } + Some(builder.build()) + } + _ => { + // TODO: + // - Handle possible-unboundness of `__get__` method + // - Handle errors while calling `__get__` + // + // There are existing tests for both cases in `descriptor_protocol.md`. + + self.member(db, "__get__") + .ignore_possibly_unbound()? + .try_call( + db, + &CallArguments::positional([instance.unwrap_or(Type::none(db)), owner]), + ) + .map(|outcome| Some(outcome.return_type(db))) + .unwrap_or(None) + } + } + } + + /// Access an attribute of this type, potentially invoking the descriptor protocol. + /// Corresponds to `getattr(, name)`. + /// + /// See also: [`Type::static_member`] + #[must_use] + pub(crate) fn member(&self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + if name == "__class__" { + return Symbol::bound(self.to_meta_type(db)); + } + + match self { + Type::FunctionLiteral(function) if name == "__get__" => Symbol::bound(Type::Callable( + CallableType::MethodWrapperDunderGet(*function), + )), + + Type::Callable(CallableType::BoundMethod(bound_method)) => match name { + "__self__" => Symbol::bound(bound_method.self_instance(db)), + "__func__" => Symbol::bound(Type::FunctionLiteral(bound_method.function(db))), + _ => KnownClass::MethodType.to_instance(db).member(db, name), + }, + Type::Callable(CallableType::MethodWrapperDunderGet(_)) => { + KnownClass::MethodWrapperType + .to_instance(db) + .member(db, name) + } + Type::Callable(CallableType::WrapperDescriptorDunderGet) => { + KnownClass::WrapperDescriptorType + .to_instance(db) + .member(db, name) + } + + Type::Instance(..) + | Type::BooleanLiteral(..) + | Type::IntLiteral(..) + | Type::StringLiteral(..) + | Type::BytesLiteral(..) + | Type::LiteralString + | Type::SliceLiteral(..) + | Type::Tuple(..) + | Type::KnownInstance(..) + | Type::FunctionLiteral(..) => { + let member = self.static_member(db, name); + + let instance = Some(*self); + let owner = self.to_meta_type(db); + + member.map_type(|ty| ty.try_call_dunder_get(db, instance, owner).unwrap_or(ty)) + } + Type::ClassLiteral(..) | Type::SubclassOf(..) => { + let member = self.static_member(db, name); + + let instance = None; + let owner = self.to_meta_type(db); + + member.map_type(|ty| ty.try_call_dunder_get(db, instance, owner).unwrap_or(ty)) + } + Type::Union(union) => union.map_with_boundness(db, |elem| elem.member(db, name)), + Type::Intersection(intersection) => { + intersection.map_with_boundness(db, |elem| elem.member(db, name)) + } + + Type::Dynamic(..) + | Type::Never + | Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::ModuleLiteral(..) => self.static_member(db, name), + } + } + /// Resolves the boolean value of a type. /// /// This is used to determine the value that would be returned @@ -1440,6 +1640,7 @@ impl<'db> Type<'db> { match self { Type::Dynamic(_) | Type::Never => Truthiness::Ambiguous, Type::FunctionLiteral(_) => Truthiness::AlwaysTrue, + Type::Callable(_) => Truthiness::AlwaysTrue, Type::ModuleLiteral(_) => Truthiness::AlwaysTrue, Type::ClassLiteral(ClassLiteralType { class }) => { class.metaclass(db).to_instance(db).bool(db) @@ -1451,24 +1652,16 @@ impl<'db> Type<'db> { .unwrap_or(Truthiness::Ambiguous), Type::AlwaysTruthy => Truthiness::AlwaysTrue, Type::AlwaysFalsy => Truthiness::AlwaysFalse, - instance_ty @ Type::Instance(InstanceType { class }) => { + Type::Instance(InstanceType { class }) => { if class.is_known(db, KnownClass::NoneType) { Truthiness::AlwaysFalse } else { // We only check the `__bool__` method for truth testing, even though at // runtime there is a fallback to `__len__`, since `__bool__` takes precedence - // and a subclass could add a `__bool__` method. We don't use - // `Type::call_dunder` here because of the need to check for `__bool__ = bool`. + // and a subclass could add a `__bool__` method. - // Don't trust a maybe-unbound `__bool__` method. - let Symbol::Type(bool_method, Boundness::Bound) = - instance_ty.to_meta_type(db).member(db, "__bool__") - else { - return Truthiness::Ambiguous; - }; - - if let Ok(Type::BooleanLiteral(bool_val)) = bool_method - .try_call_bound(db, instance_ty, &CallArguments::positional([])) + if let Ok(Type::BooleanLiteral(bool_val)) = self + .try_call_dunder(db, "__bool__", &CallArguments::positional([])) .map(|outcome| outcome.return_type(db)) { bool_val.into() @@ -1541,15 +1734,12 @@ impl<'db> Type<'db> { return usize_len.try_into().ok().map(Type::IntLiteral); } - let return_ty = - match self.try_call_dunder(db, "__len__", &CallArguments::positional([*self])) { - Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => { - outcome.return_type(db) - } + let return_ty = match self.try_call_dunder(db, "__len__", &CallArguments::positional([])) { + Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => outcome.return_type(db), - // TODO: emit a diagnostic - Err(err) => err.return_type(db)?, - }; + // TODO: emit a diagnostic + Err(err) => err.return_type(db)?, + }; non_negative_int_literal(db, return_ty) } @@ -1563,6 +1753,44 @@ impl<'db> Type<'db> { arguments: &CallArguments<'_, 'db>, ) -> Result, CallError<'db>> { match self { + Type::Callable(CallableType::BoundMethod(bound_method)) => { + let instance = bound_method.self_instance(db); + let arguments = arguments.with_self(instance); + + let binding = bind_call( + db, + &arguments, + bound_method.function(db).signature(db), + self, + ); + + if binding.has_binding_errors() { + Err(CallError::BindingError { binding }) + } else { + Ok(CallOutcome::Single(binding)) + } + } + Type::Callable(CallableType::MethodWrapperDunderGet(function)) => { + let return_ty = match arguments.first_argument() { + Some(ty) if ty.is_none(db) => Type::FunctionLiteral(function), + Some(instance) => Type::Callable(CallableType::BoundMethod( + BoundMethodType::new(db, function, instance), + )), + _ => Type::unknown(), + }; + Ok(CallOutcome::Single(CallBinding::from_return_type( + return_ty, + ))) + } + Type::Callable(CallableType::WrapperDescriptorDunderGet) => { + let return_ty = match arguments.first_argument() { + Some(f @ Type::FunctionLiteral(_)) => f, + _ => Type::unknown(), + }; + Ok(CallOutcome::Single(CallBinding::from_return_type( + return_ty, + ))) + } Type::FunctionLiteral(function_type) => { let mut binding = bind_call(db, arguments, function_type.signature(db), self); match function_type.known(db) { @@ -1638,6 +1866,30 @@ impl<'db> Type<'db> { }; } + Some(KnownFunction::GetattrStatic) => { + let Some((instance_ty, attr_name, default)) = + binding.three_parameter_types() + else { + return Ok(CallOutcome::Single(binding)); + }; + + let Some(attr_name) = attr_name.into_string_literal() else { + return Ok(CallOutcome::Single(binding)); + }; + + let default = if default.is_unknown() { + Type::Never + } else { + default + }; + + let static_member = instance_ty + .static_member(db, attr_name.value(db)) + .ignore_possibly_unbound() // TODO: we could emit a diagnostic here (if default is not set) + .unwrap_or(default); + binding.set_return_type(static_member); + } + _ => {} }; @@ -1675,7 +1927,7 @@ impl<'db> Type<'db> { instance_ty @ Type::Instance(_) => { instance_ty - .try_call_dunder(db, "__call__", &arguments.with_self(instance_ty)) + .try_call_dunder(db, "__call__", arguments) .map_err(|err| match err { CallDunderError::Call(CallError::NotCallable { .. }) => { // Turn "`` not callable" into @@ -1740,8 +1992,6 @@ impl<'db> Type<'db> { receiver_ty: &Type<'db>, arguments: &CallArguments<'_, 'db>, ) -> Result, CallError<'db>> { - debug_assert!(receiver_ty.is_instance() || receiver_ty.is_class_literal()); - match self { Type::FunctionLiteral(..) => { // Functions are always descriptors, so this would effectively call @@ -1749,10 +1999,7 @@ impl<'db> Type<'db> { self.try_call(db, &arguments.with_self(*receiver_ty)) } - Type::Instance(_) | Type::ClassLiteral(_) => { - // TODO descriptor protocol. For now, assume non-descriptor and call without `self` argument. - self.try_call(db, arguments) - } + Type::Instance(_) | Type::ClassLiteral(_) => self.try_call(db, arguments), Type::Union(union) => CallOutcome::try_call_union(db, union, |element| { element.try_call_bound(db, receiver_ty, arguments) @@ -1779,9 +2026,11 @@ impl<'db> Type<'db> { arguments: &CallArguments<'_, 'db>, ) -> Result, CallDunderError<'db>> { match self.to_meta_type(db).member(db, name) { - Symbol::Type(callable_ty, Boundness::Bound) => Ok(callable_ty.try_call(db, arguments)?), + Symbol::Type(callable_ty, Boundness::Bound) => { + Ok(callable_ty.try_call_bound(db, &self, arguments)?) + } Symbol::Type(callable_ty, Boundness::PossiblyUnbound) => { - let call = callable_ty.try_call(db, arguments)?; + let call = callable_ty.try_call_bound(db, &self, arguments)?; Err(CallDunderError::PossiblyUnbound(call)) } Symbol::Unbound => Err(CallDunderError::MethodNotAvailable), @@ -1804,7 +2053,7 @@ impl<'db> Type<'db> { } let dunder_iter_result = - self.try_call_dunder(db, "__iter__", &CallArguments::positional([self])); + self.try_call_dunder(db, "__iter__", &CallArguments::positional([])); match &dunder_iter_result { Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => { let iterator_ty = outcome.return_type(db); @@ -1812,7 +2061,7 @@ impl<'db> Type<'db> { return match iterator_ty.try_call_dunder( db, "__next__", - &CallArguments::positional([iterator_ty]), + &CallArguments::positional([]), ) { Ok(outcome) => { if matches!( @@ -1861,7 +2110,7 @@ impl<'db> Type<'db> { match self.try_call_dunder( db, "__getitem__", - &CallArguments::positional([self, KnownClass::Int.to_instance(db)]), + &CallArguments::positional([KnownClass::Int.to_instance(db)]), ) { Ok(outcome) => IterationOutcome::Iterable { element_ty: outcome.return_type(db), @@ -1895,6 +2144,7 @@ impl<'db> Type<'db> { Type::BooleanLiteral(_) | Type::BytesLiteral(_) | Type::FunctionLiteral(_) + | Type::Callable(..) | Type::Instance(_) | Type::KnownInstance(_) | Type::ModuleLiteral(_) @@ -2090,6 +2340,15 @@ impl<'db> Type<'db> { Type::SliceLiteral(_) => KnownClass::Slice.to_class_literal(db), Type::IntLiteral(_) => KnownClass::Int.to_class_literal(db), Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db), + Type::Callable(CallableType::BoundMethod(_)) => { + KnownClass::MethodType.to_class_literal(db) + } + Type::Callable(CallableType::MethodWrapperDunderGet(_)) => { + KnownClass::MethodWrapperType.to_class_literal(db) + } + Type::Callable(CallableType::WrapperDescriptorDunderGet) => { + KnownClass::WrapperDescriptorType.to_class_literal(db) + } Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db), Type::ClassLiteral(ClassLiteralType { class }) => class.metaclass(db), @@ -2323,6 +2582,8 @@ pub enum KnownClass { FrozenSet, Dict, Slice, + Range, + MemoryView, Property, BaseException, BaseExceptionGroup, @@ -2330,6 +2591,9 @@ pub enum KnownClass { GenericAlias, ModuleType, FunctionType, + MethodType, + MethodWrapperType, + WrapperDescriptorType, // Typeshed NoneType, // Part of `types` for Python >= 3.10 // Typing @@ -2372,12 +2636,17 @@ impl<'db> KnownClass { Self::List => "list", Self::Type => "type", Self::Slice => "slice", + Self::Range => "range", + Self::MemoryView => "memoryview", Self::Property => "property", Self::BaseException => "BaseException", Self::BaseExceptionGroup => "BaseExceptionGroup", Self::GenericAlias => "GenericAlias", Self::ModuleType => "ModuleType", Self::FunctionType => "FunctionType", + Self::MethodType => "MethodType", + Self::MethodWrapperType => "MethodWrapperType", + Self::WrapperDescriptorType => "WrapperDescriptorType", Self::NoneType => "NoneType", Self::SpecialForm => "_SpecialForm", Self::TypeVar => "TypeVar", @@ -2453,9 +2722,16 @@ impl<'db> KnownClass { | Self::BaseException | Self::BaseExceptionGroup | Self::Slice + | Self::Range + | Self::MemoryView | Self::Property => KnownModule::Builtins, Self::VersionInfo => KnownModule::Sys, - Self::GenericAlias | Self::ModuleType | Self::FunctionType => KnownModule::Types, + Self::GenericAlias + | Self::ModuleType + | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType => KnownModule::Types, Self::NoneType => KnownModule::Typeshed, Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::StdlibAlias => { KnownModule::Typing @@ -2515,10 +2791,15 @@ impl<'db> KnownClass { | Self::List | Self::Type | Self::Slice + | Self::Range + | Self::MemoryView | Self::Property | Self::GenericAlias | Self::ModuleType | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType | Self::SpecialForm | Self::ChainMap | Self::Counter @@ -2551,12 +2832,17 @@ impl<'db> KnownClass { "dict" => Self::Dict, "list" => Self::List, "slice" => Self::Slice, + "range" => Self::Range, + "memoryview" => Self::MemoryView, "BaseException" => Self::BaseException, "BaseExceptionGroup" => Self::BaseExceptionGroup, "GenericAlias" => Self::GenericAlias, "NoneType" => Self::NoneType, "ModuleType" => Self::ModuleType, "FunctionType" => Self::FunctionType, + "MethodType" => Self::MethodType, + "MethodWrapperType" => Self::MethodWrapperType, + "WrapperDescriptorType" => Self::WrapperDescriptorType, "TypeAliasType" => Self::TypeAliasType, "ChainMap" => Self::ChainMap, "Counter" => Self::Counter, @@ -2598,6 +2884,8 @@ impl<'db> KnownClass { | Self::FrozenSet | Self::Dict | Self::Slice + | Self::Range + | Self::MemoryView | Self::Property | Self::GenericAlias | Self::ChainMap @@ -2611,7 +2899,10 @@ impl<'db> KnownClass { | Self::BaseException | Self::EllipsisType | Self::BaseExceptionGroup - | Self::FunctionType => module == self.canonical_module(db), + | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType => module == self.canonical_module(db), Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => { matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) @@ -2946,11 +3237,11 @@ impl<'db> KnownInstanceType<'db> { } } - fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { let ty = match (self, name) { (Self::TypeVar(typevar), "__name__") => Type::string_literal(db, typevar.name(db)), (Self::TypeAliasType(alias), "__name__") => Type::string_literal(db, alias.name(db)), - _ => return self.instance_fallback(db).member(db, name), + _ => return self.instance_fallback(db).static_member(db, name), }; Symbol::bound(ty) } @@ -3197,6 +3488,9 @@ pub enum KnownFunction { /// `typing(_extensions).cast` Cast, + /// `inspect.getattr_static` + GetattrStatic, + /// `knot_extensions.static_assert` StaticAssert, /// `knot_extensions.is_equivalent_to` @@ -3240,6 +3534,7 @@ impl KnownFunction { "no_type_check" => Self::NoTypeCheck, "assert_type" => Self::AssertType, "cast" => Self::Cast, + "getattr_static" => Self::GetattrStatic, "static_assert" => Self::StaticAssert, "is_subtype_of" => Self::IsSubtypeOf, "is_disjoint_from" => Self::IsDisjointFrom, @@ -3269,6 +3564,9 @@ impl KnownFunction { Self::AssertType | Self::Cast | Self::RevealType | Self::Final | Self::NoTypeCheck => { matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) } + Self::GetattrStatic => { + matches!(module, KnownModule::Inspect) + } Self::IsAssignableTo | Self::IsDisjointFrom | Self::IsEquivalentTo @@ -3303,11 +3601,62 @@ impl KnownFunction { | Self::Final | Self::NoTypeCheck | Self::RevealType + | Self::GetattrStatic | Self::StaticAssert => ParameterExpectations::AllValueExpressions, } } } +/// This type represents bound method objects that are created when a method is called +/// on an instance of a class. For example, the expression `Path("a.txt").touch` creates +/// a bound method object that represents the `Path.touch` method which is bound to the +/// instance `Path("a.txt")`. +#[salsa::tracked] +pub struct BoundMethodType<'db> { + /// The function that is being bound. Corresponds to the `__func__` attribute on a + /// bound method object + pub(crate) function: FunctionType<'db>, + /// The instance on which this method has been called. Corresponds to the `__self__` + /// attribute on a bound method object + self_instance: Type<'db>, +} + +/// A type that represents callable objects. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)] +pub enum CallableType<'db> { + /// Represents a callable `instance.method` where `instance` is an instance of a class + /// and `method` is a method (of that class). + /// + /// See [`BoundMethodType`] for more information. + /// + /// TODO: This could eventually be replaced by a more general `Callable` type, if we + /// decide to bind the first argument of method calls early, i.e. if we have a method + /// `def f(self, x: int) -> str`, and see it being called as `instance.f`, we could + /// partially apply (and check) the `instance` argument against the `self` parameter, + /// and return a `Callable[[int], str]`. One drawback would be that we could not show + /// the bound instance when that type is displayed. + BoundMethod(BoundMethodType<'db>), + + /// Represents the callable `f.__get__` where `f` is a function. + /// + /// TODO: This could eventually be replaced by a more general `Callable` type that is + /// also able to represent overloads. It would need to represent the two overloads of + /// `types.FunctionType.__get__`: + /// + /// ```txt + /// * (None, type) -> Literal[function_on_which_it_was_called] + /// * (object, type | None) -> BoundMethod[instance, function_on_which_it_was_called] + /// ``` + MethodWrapperDunderGet(FunctionType<'db>), + + /// Represents the callable `FunctionType.__get__`. + /// + /// TODO: Similar to above, this could eventually be replaced by a generic `Callable` + /// type. We currently add this as a separate variant because `FunctionType.__get__` + /// is an overloaded method and we do not support `@overload` yet. + WrapperDescriptorDunderGet, +} + /// Describes whether the parameters in a function expect value expressions or type expressions. /// /// Whether a specific parameter in the function expects a type expression can be queried @@ -3384,14 +3733,14 @@ pub struct ModuleLiteralType<'db> { } impl<'db> ModuleLiteralType<'db> { - fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { // `__dict__` is a very special member that is never overridden by module globals; // we should always look it up directly as an attribute on `types.ModuleType`, // never in the global scope of the module. if name == "__dict__" { return KnownClass::ModuleType .to_instance(db) - .member(db, "__dict__"); + .static_member(db, "__dict__"); } // If the file that originally imported the module has also imported a submodule @@ -3902,7 +4251,6 @@ impl<'db> Class<'db> { // - `typing.Final` // - Proper diagnostics // - Handling of possibly-undeclared/possibly-unbound attributes - // - The descriptor protocol let body_scope = self.body_scope(db); let table = symbol_table(db, body_scope); @@ -3922,8 +4270,10 @@ impl<'db> Class<'db> { // and non-property methods. if function.has_decorator(db, KnownClass::Property.to_class_literal(db)) { SymbolAndQualifiers::todo("@property") + } else if !function.decorators(db).is_empty() { + SymbolAndQualifiers::todo("decorated method") } else { - SymbolAndQualifiers::todo("bound method") + SymbolAndQualifiers(Symbol::bound(declared_ty), qualifiers) } } else { SymbolAndQualifiers(Symbol::bound(declared_ty), qualifiers) @@ -4041,7 +4391,7 @@ impl<'db> ClassLiteralType<'db> { self.class.body_scope(db) } - fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { self.class.class_member(db, name) } } @@ -4144,6 +4494,46 @@ impl<'db> UnionType<'db> { Self::from_elements(db, self.elements(db).iter().map(transform_fn)) } + pub(crate) fn map_with_boundness( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> Symbol<'db>, + ) -> Symbol<'db> { + let mut builder = UnionBuilder::new(db); + + let mut all_unbound = true; + let mut possibly_unbound = false; + for ty in self.elements(db) { + let ty_member = transform_fn(ty); + match ty_member { + Symbol::Unbound => { + possibly_unbound = true; + } + Symbol::Type(ty_member, member_boundness) => { + if member_boundness == Boundness::PossiblyUnbound { + possibly_unbound = true; + } + + all_unbound = false; + builder = builder.add(ty_member); + } + } + } + + if all_unbound { + Symbol::Unbound + } else { + Symbol::Type( + builder.build(), + if possibly_unbound { + Boundness::PossiblyUnbound + } else { + Boundness::Bound + }, + ) + } + } + pub fn is_fully_static(self, db: &'db dyn Db) -> bool { self.elements(db).iter().all(|ty| ty.is_fully_static(db)) } @@ -4366,6 +4756,49 @@ impl<'db> IntersectionType<'db> { .zip(sorted_other.negative(db)) .all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty)) } + + pub(crate) fn map_with_boundness( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> Symbol<'db>, + ) -> Symbol<'db> { + if !self.negative(db).is_empty() { + return Symbol::todo("map_with_boundness: intersections with negative contributions"); + } + + let mut builder = IntersectionBuilder::new(db); + + let mut any_unbound = false; + let mut any_possibly_unbound = false; + for ty in self.positive(db) { + let ty_member = transform_fn(ty); + match ty_member { + Symbol::Unbound => { + any_unbound = true; + } + Symbol::Type(ty_member, member_boundness) => { + if member_boundness == Boundness::PossiblyUnbound { + any_possibly_unbound = true; + } + + builder = builder.add_positive(ty_member); + } + } + } + + if any_unbound { + Symbol::Unbound + } else { + Symbol::Type( + builder.build(), + if any_possibly_unbound { + Boundness::PossiblyUnbound + } else { + Boundness::Bound + }, + ) + } + } } #[salsa::interned] diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 44c0d298c2..e8cc8c0d26 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -193,6 +193,13 @@ impl<'db> CallBinding<'db> { } } + pub(crate) fn three_parameter_types(&self) -> Option<(Type<'db>, Type<'db>, Type<'db>)> { + match self.parameter_types() { + [first, second, third] => Some((*first, *second, *third)), + _ => None, + } + } + fn callable_name(&self, db: &'db dyn Db) -> Option<&str> { match self.callable_ty { Type::FunctionLiteral(function) => Some(function.name(db)), diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 1dd6881b10..e3ac7bc7d1 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -72,6 +72,7 @@ impl<'db> ClassBase<'db> { Type::Never | Type::BooleanLiteral(_) | Type::FunctionLiteral(_) + | Type::Callable(..) | Type::BytesLiteral(_) | Type::IntLiteral(_) | Type::StringLiteral(_) diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 23b6395ca4..e83975a3c5 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -8,8 +8,8 @@ use ruff_python_literal::escape::AsciiEscape; use crate::types::class_base::ClassBase; use crate::types::{ - ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType, Type, - UnionType, + CallableType, ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType, + Type, UnionType, }; use crate::Db; use rustc_hash::FxHashMap; @@ -88,6 +88,24 @@ impl Display for DisplayRepresentation<'_> { }, Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)), Type::FunctionLiteral(function) => f.write_str(function.name(self.db)), + Type::Callable(CallableType::BoundMethod(bound_method)) => { + f.write_str("") + } + Type::Callable(CallableType::MethodWrapperDunderGet(function)) => { + f.write_str("") + } + Type::Callable(CallableType::WrapperDescriptorDunderGet) => { + f.write_str("") + } Type::Union(union) => union.display(self.db).fmt(f), Type::Intersection(intersection) => intersection.display(self.db).fmt(f), Type::IntLiteral(n) => n.fmt(f), diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 02b269bb2a..80fb43f51f 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -3753,6 +3753,7 @@ impl<'db> TypeInferenceBuilder<'db> { ( op @ (ast::UnaryOp::UAdd | ast::UnaryOp::USub | ast::UnaryOp::Invert), Type::FunctionLiteral(_) + | Type::Callable(..) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::SubclassOf(_) @@ -3780,7 +3781,7 @@ impl<'db> TypeInferenceBuilder<'db> { match operand_type.try_call_dunder( self.db(), unary_dunder_method, - &CallArguments::positional([operand_type]), + &CallArguments::positional([]), ) { Ok(outcome) => outcome.return_type(self.db()), Err(e) => { @@ -3967,6 +3968,7 @@ impl<'db> TypeInferenceBuilder<'db> { // fall back on looking for dunder methods on one of the operand types. ( Type::FunctionLiteral(_) + | Type::Callable(..) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::SubclassOf(_) @@ -3983,6 +3985,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::SliceLiteral(_) | Type::Tuple(_), Type::FunctionLiteral(_) + | Type::Callable(..) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::SubclassOf(_) @@ -4029,7 +4032,7 @@ impl<'db> TypeInferenceBuilder<'db> { .try_call_dunder( self.db(), reflected_dunder, - &CallArguments::positional([right_ty, left_ty]), + &CallArguments::positional([left_ty]), ) .map(|outcome| outcome.return_type(self.db())) .or_else(|_| { @@ -4037,7 +4040,7 @@ impl<'db> TypeInferenceBuilder<'db> { .try_call_dunder( self.db(), op.dunder(), - &CallArguments::positional([left_ty, right_ty]), + &CallArguments::positional([right_ty]), ) .map(|outcome| outcome.return_type(self.db())) }) @@ -4953,7 +4956,7 @@ impl<'db> TypeInferenceBuilder<'db> { match value_ty.try_call_dunder( self.db(), "__getitem__", - &CallArguments::positional([value_ty, slice_ty]), + &CallArguments::positional([slice_ty]), ) { Ok(outcome) => return outcome.return_type(self.db()), Err(err @ CallDunderError::PossiblyUnbound { .. }) => { diff --git a/crates/red_knot_python_semantic/src/types/subclass_of.rs b/crates/red_knot_python_semantic/src/types/subclass_of.rs index 4a0705dfcf..8820993d07 100644 --- a/crates/red_knot_python_semantic/src/types/subclass_of.rs +++ b/crates/red_knot_python_semantic/src/types/subclass_of.rs @@ -64,8 +64,8 @@ impl<'db> SubclassOfType<'db> { !self.is_dynamic() } - pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { - Type::from(self.subclass_of).member(db, name) + pub(crate) fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + Type::from(self.subclass_of).static_member(db, name) } /// Return `true` if `self` is a subtype of `other`. diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index 1b6e63a764..483843feb0 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -1,5 +1,7 @@ use std::cmp::Ordering; +use crate::types::CallableType; + use super::{ class_base::ClassBase, ClassLiteralType, DynamicType, InstanceType, KnownInstanceType, TodoType, Type, @@ -54,6 +56,27 @@ pub(super) fn union_elements_ordering<'db>(left: &Type<'db>, right: &Type<'db>) (Type::FunctionLiteral(_), _) => Ordering::Less, (_, Type::FunctionLiteral(_)) => Ordering::Greater, + ( + Type::Callable(CallableType::BoundMethod(left)), + Type::Callable(CallableType::BoundMethod(right)), + ) => left.cmp(right), + (Type::Callable(CallableType::BoundMethod(_)), _) => Ordering::Less, + (_, Type::Callable(CallableType::BoundMethod(_))) => Ordering::Greater, + + ( + Type::Callable(CallableType::MethodWrapperDunderGet(left)), + Type::Callable(CallableType::MethodWrapperDunderGet(right)), + ) => left.cmp(right), + (Type::Callable(CallableType::MethodWrapperDunderGet(_)), _) => Ordering::Less, + (_, Type::Callable(CallableType::MethodWrapperDunderGet(_))) => Ordering::Greater, + + ( + Type::Callable(CallableType::WrapperDescriptorDunderGet), + Type::Callable(CallableType::WrapperDescriptorDunderGet), + ) => Ordering::Equal, + (Type::Callable(CallableType::WrapperDescriptorDunderGet), _) => Ordering::Less, + (_, Type::Callable(CallableType::WrapperDescriptorDunderGet)) => Ordering::Greater, + (Type::Tuple(left), Type::Tuple(right)) => left.cmp(right), (Type::Tuple(_), _) => Ordering::Less, (_, Type::Tuple(_)) => Ordering::Greater, diff --git a/crates/ruff_benchmark/benches/red_knot.rs b/crates/ruff_benchmark/benches/red_knot.rs index 11ec12577c..b1c82c6c75 100644 --- a/crates/ruff_benchmark/benches/red_knot.rs +++ b/crates/ruff_benchmark/benches/red_knot.rs @@ -67,6 +67,104 @@ static EXPECTED_DIAGNOSTICS: &[KeyDiagnosticFields] = &[ Cow::Borrowed("Module `collections.abc` has no member `Iterable`"), Severity::Error, ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(7642..7645), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(9126..9129), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(9962..9965), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(13339..13342), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(13789..13792), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(14052..14055), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(17164..17167), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(17710..17713), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(17789..17792), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(18535..18538), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(19311..19314), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(19507..19510), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(19689..19692), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(19783..19786), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), // We don't handle intersections in `is_assignable_to` yet ( DiagnosticId::lint("invalid-argument-type"), @@ -89,6 +187,34 @@ static EXPECTED_DIAGNOSTICS: &[KeyDiagnosticFields] = &[ Cow::Borrowed("Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_number`; expected type `Match`"), Severity::Error, ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(21451..21452), + Cow::Borrowed("Object of type `Literal[0]` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(21454..21457), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 4 (`end`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(21572..21573), + Cow::Borrowed("Object of type `Literal[0]` cannot be assigned to parameter 3 (`start`); expected type `SupportsIndex | None`"), + Severity::Error, + ), + ( + DiagnosticId::lint("invalid-argument-type"), + Some("/src/tomllib/_parser.py"), + Some(21575..21578), + Cow::Borrowed("Object of type `Unknown | int` cannot be assigned to parameter 4 (`end`); expected type `SupportsIndex | None`"), + Severity::Error, + ), ( DiagnosticId::lint("unused-ignore-comment"), Some("/src/tomllib/_parser.py"),