diff --git a/crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md b/crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md index cc87c85d12..8939daaaa2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md +++ b/crates/red_knot_python_semantic/resources/mdtest/assignment/augmented.md @@ -40,7 +40,7 @@ class C: return 42 x = C() -# error: [invalid-argument-type] +# error: [unsupported-operator] "Operator `-=` is unsupported between objects of type `C` and `Literal[1]`" x -= 1 reveal_type(x) # revealed: 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 84116fa2c5..42af9383f7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -244,10 +244,7 @@ class B: def __rsub__(self, other: A) -> B: return B() -# TODO: this should be `B` (the return annotation of `B.__rsub__`), -# because `A.__sub__` is annotated as only accepting `A`, -# but `B.__rsub__` will accept `A`. -reveal_type(A() - B()) # revealed: A +reveal_type(A() - B()) # revealed: B ``` ## Callable instances as dunders @@ -263,7 +260,10 @@ class B: __add__ = A() # TODO: this could be `int` if we declare `B.__add__` using a `Callable` type -reveal_type(B() + B()) # revealed: Unknown | int +# TODO: Should not be an error: `A` instance is not a method descriptor, don't prepend `self` arg. +# Revealed type should be `Unknown | int`. +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `B` and `B`" +reveal_type(B() + B()) # revealed: Unknown ``` ## Integration test: numbers from typeshed @@ -277,22 +277,14 @@ return annotations from the widening, and preserve a bit more precision here? reveal_type(3j + 3.14) # revealed: int | float | complex reveal_type(4.2 + 42) # revealed: int | float reveal_type(3j + 3) # revealed: int | float | complex - -# TODO should be int | float | complex, need to check arg type and fall back to `rhs.__radd__` -reveal_type(3.14 + 3j) # revealed: int | float - -# TODO should be int | float, need to check arg type and fall back to `rhs.__radd__` -reveal_type(42 + 4.2) # revealed: int - -# TODO should be int | float | complex, need to check arg type and fall back to `rhs.__radd__` -reveal_type(3 + 3j) # revealed: int +reveal_type(3.14 + 3j) # revealed: int | float | complex +reveal_type(42 + 4.2) # revealed: int | float +reveal_type(3 + 3j) # revealed: int | float | complex def _(x: bool, y: int): reveal_type(x + y) # revealed: int reveal_type(4.2 + x) # revealed: int | float - - # TODO should be float, need to check arg type and fall back to `rhs.__radd__` - reveal_type(y + 4.12) # revealed: int + reveal_type(y + 4.12) # revealed: int | float ``` ## With literal types @@ -309,8 +301,7 @@ class A: return self reveal_type(A() + 1) # revealed: A -# TODO should be `A` since `int.__add__` doesn't support `A` instances -reveal_type(1 + A()) # revealed: int +reveal_type(1 + A()) # revealed: A reveal_type(A() + "foo") # revealed: A # TODO should be `A` since `str.__add__` doesn't support `A` instances diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md index 0eb5a2cb31..042585e9ec 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md @@ -10,9 +10,10 @@ reveal_type(-3 // 3) # revealed: Literal[-1] reveal_type(-3 / 3) # revealed: float reveal_type(5 % 3) # revealed: Literal[2] -# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared -# formal parameter type. -reveal_type(2 + "f") # revealed: int +# TODO: This should emit an unsupported-operator error but we don't currently +# verify that the actual parameter to `int.__add__` matches the declared +# formal parameter type. +reveal_type(2 + "f") # revealed: Unknown def lhs(x: int): reveal_type(x + 1) # revealed: int diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md b/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md index 10678ef2ba..0283c8a60c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/callable_instance.md @@ -52,7 +52,7 @@ class NonCallable: __call__ = 1 a = NonCallable() -# error: "Object of type `Unknown | Literal[1]` is not callable (due to union element `Literal[1]`)" +# error: [call-non-callable] "Object of type `Literal[1]` is not callable" reveal_type(a()) # revealed: Unknown ``` @@ -67,8 +67,8 @@ def _(flag: bool): def __call__(self) -> int: ... a = NonCallable() - # error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)" - reveal_type(a()) # revealed: Unknown | int + # error: [call-non-callable] "Object of type `Literal[1]` is not callable" + reveal_type(a()) # revealed: int | Unknown ``` ## Call binding errors @@ -99,3 +99,26 @@ c = C() # error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of function `__call__`; expected type `int`" reveal_type(c()) # revealed: int ``` + +## Union over callables + +### Possibly unbound `__call__` + +```py +def outer(cond1: bool): + class Test: + if cond1: + def __call__(self): ... + + class Other: + def __call__(self): ... + + def inner(cond2: bool): + if cond2: + a = Test() + else: + a = Other() + + # error: [call-non-callable] "Object of type `Test` is not callable (possibly unbound `__call__` method)" + a() +``` 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 dafe8a89f8..823031fa9b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/function.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/function.md @@ -278,10 +278,10 @@ proper diagnostics in case of missing or superfluous arguments. from typing_extensions import reveal_type # error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`" -reveal_type() # revealed: Unknown +reveal_type() # error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2" -reveal_type(1, 2) # revealed: Literal[1] +reveal_type(1, 2) ``` ### `static_assert` @@ -290,7 +290,6 @@ reveal_type(1, 2) # revealed: Literal[1] from knot_extensions import static_assert # error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`" -# error: [static-assert-error] static_assert() # error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3" diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/union.md b/crates/red_knot_python_semantic/resources/mdtest/call/union.md index e917bd42b0..086bfa8447 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/union.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/union.md @@ -39,8 +39,8 @@ def _(flag: bool): else: def f() -> int: return 1 - x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)" - reveal_type(x) # revealed: Unknown | int + x = f() # error: [call-non-callable] "Object of type `Literal[1]` is not callable" + reveal_type(x) # revealed: int | Unknown ``` ## Multiple non-callable elements in a union @@ -56,8 +56,8 @@ def _(flag: bool, flag2: bool): else: def f() -> int: return 1 - # error: "Object of type `Literal[1, "foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])" - # revealed: Unknown | int + # error: [call-non-callable] "Object of type `Literal[1]` is not callable" + # revealed: int | Unknown reveal_type(f()) ``` @@ -72,6 +72,39 @@ def _(flag: bool): else: f = "foo" - x = f() # error: "Object of type `Literal[1, "foo"]` is not callable" + x = f() # error: [call-non-callable] "Object of type `Literal[1, "foo"]` is not callable" + reveal_type(x) # revealed: Unknown +``` + +## Mismatching signatures + +Calling a union where the arguments don't match the signature of all variants. + +```py +def f1(a: int) -> int: ... +def f2(a: str) -> str: ... +def _(flag: bool): + if flag: + f = f1 + else: + f = f2 + + # error: [invalid-argument-type] "Object of type `Literal[3]` cannot be assigned to parameter 1 (`a`) of function `f2`; expected type `str`" + x = f(3) + reveal_type(x) # revealed: int | str +``` + +## Any non-callable variant + +```py +def f1(a: int): ... +def _(flag: bool): + if flag: + f = f1 + else: + f = "This is a string literal" + + # error: [call-non-callable] "Object of type `Literal["This is a string literal"]` is not callable" + x = f(3) reveal_type(x) # revealed: Unknown ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md index 9f9b5bce10..90dc173474 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md @@ -21,8 +21,9 @@ class A: reveal_type("hello" in A()) # revealed: bool reveal_type("hello" not in A()) # revealed: bool -# TODO: should emit diagnostic, need to check arg type, will fail +# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`" reveal_type(42 in A()) # revealed: bool +# error: [unsupported-operator] "Operator `not in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`" reveal_type(42 not in A()) # revealed: bool ``` @@ -126,9 +127,9 @@ class A: reveal_type(CheckContains() in A()) # revealed: bool -# TODO: should emit diagnostic, need to check arg type, -# should not fall back to __iter__ or __getitem__ +# error: [unsupported-operator] "Operator `in` is not supported for types `CheckIter` and `A`" reveal_type(CheckIter() in A()) # revealed: bool +# error: [unsupported-operator] "Operator `in` is not supported for types `CheckGetItem` and `A`" reveal_type(CheckGetItem() in A()) # revealed: bool class B: @@ -154,7 +155,8 @@ class A: def __getitem__(self, key: str) -> str: return "foo" -# TODO should emit a diagnostic +# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`" reveal_type(42 in A()) # revealed: bool +# error: [unsupported-operator] "Operator `in` is not supported for types `str` and `A`, in comparing `Literal["hello"]` with `A`" reveal_type("hello" in A()) # revealed: bool ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md index 29fb516e23..70f4427af7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md @@ -117,14 +117,11 @@ class B: def __ne__(self, other: str) -> B: return B() -# TODO: should be `int` and `bytearray`. -# Need to check arg type and fall back to `rhs.__eq__` and `rhs.__ne__`. -# # Because `object.__eq__` and `object.__ne__` accept `object` in typeshed, # this can only happen with an invalid override of these methods, # but we still support it. -reveal_type(B() == A()) # revealed: B -reveal_type(B() != A()) # revealed: B +reveal_type(B() == A()) # revealed: int +reveal_type(B() != A()) # revealed: bytearray reveal_type(B() < A()) # revealed: list reveal_type(B() <= A()) # revealed: set @@ -222,9 +219,8 @@ class B(A): def __gt__(self, other: int) -> B: return B() -# TODO: should be `A`, need to check argument type and fall back to LHS method -reveal_type(A() < B()) # revealed: B -reveal_type(A() > B()) # revealed: B +reveal_type(A() < B()) # revealed: A +reveal_type(A() > B()) # revealed: A ``` ## Operations involving instances of classes inheriting from `Any` @@ -272,9 +268,8 @@ class A: def __ne__(self, other: int) -> A: return A() -# TODO: it should be `bool`, need to check arg type and fall back to `is` and `is not` -reveal_type(A() == A()) # revealed: A -reveal_type(A() != A()) # revealed: A +reveal_type(A() == A()) # revealed: bool +reveal_type(A() != A()) # revealed: bool ``` ## Object Comparisons with Typeshed @@ -305,12 +300,14 @@ reveal_type(1 >= 1.0) # revealed: bool reveal_type(1 == 2j) # revealed: bool reveal_type(1 != 2j) # revealed: bool -# TODO: should be Unknown and emit diagnostic, -# need to check arg type and should be failed -reveal_type(1 < 2j) # revealed: bool -reveal_type(1 <= 2j) # revealed: bool -reveal_type(1 > 2j) # revealed: bool -reveal_type(1 >= 2j) # revealed: bool +# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`" +reveal_type(1 < 2j) # revealed: Unknown +# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`" +reveal_type(1 <= 2j) # revealed: Unknown +# error: [unsupported-operator] "Operator `>` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`" +reveal_type(1 > 2j) # revealed: Unknown +# error: [unsupported-operator] "Operator `>=` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`" +reveal_type(1 >= 2j) # revealed: Unknown def f(x: bool, y: int): reveal_type(x < y) # revealed: bool diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md index a59e1510bf..bf956e8413 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md @@ -12,8 +12,8 @@ reveal_type(1 is 1) # revealed: bool reveal_type(1 is not 1) # revealed: bool reveal_type(1 is 2) # revealed: Literal[False] reveal_type(1 is not 7) # revealed: Literal[True] -# TODO: should be Unknown, and emit diagnostic, once we check call argument types -reveal_type(1 <= "" and 0 < 1) # revealed: bool +# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`" +reveal_type(1 <= "" and 0 < 1) # revealed: Unknown & ~AlwaysTruthy | Literal[True] ``` ## Integer instance diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md index 4cbe9de116..20c8c914ac 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/intersections.md @@ -8,7 +8,9 @@ types, we can infer that the result for the intersection type is also true/false ```py from typing import Literal -class Base: ... +class Base: + def __gt__(self, other) -> bool: + return False class Child1(Base): def __eq__(self, other) -> Literal[True]: diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md index e34afd6a05..0702c2de54 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md @@ -23,6 +23,7 @@ from __future__ import annotations class A: def __lt__(self, other) -> A: ... + def __gt__(self, other) -> bool: ... class B: def __lt__(self, other) -> B: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md index 963d8121b6..273572104e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.md @@ -92,11 +92,14 @@ reveal_type(a == b) # revealed: bool # TODO: should be Literal[True], once we implement (in)equality for mismatched literals reveal_type(a != b) # revealed: bool -# TODO: should be Unknown and add more informative diagnostics -reveal_type(a < b) # revealed: bool -reveal_type(a <= b) # revealed: bool -reveal_type(a > b) # revealed: bool -reveal_type(a >= b) # revealed: bool +# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`" +reveal_type(a < b) # revealed: Unknown +# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`" +reveal_type(a <= b) # revealed: Unknown +# error: [unsupported-operator] "Operator `>` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`" +reveal_type(a > b) # revealed: Unknown +# error: [unsupported-operator] "Operator `>=` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`" +reveal_type(a >= b) # revealed: Unknown ``` However, if the lexicographic comparison completes without reaching a point where str and int are diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md index eee53de4a1..f3e57c886d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md @@ -9,28 +9,22 @@ def _(flag: bool, flag1: bool, flag2: bool): b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`" reveal_type(b) # revealed: bool - # TODO: should error, once operand type check is implemented - # ("Operator `<` is not supported for types `object` and `int`") + # error: [unsupported-operator] "Operator `<` is not supported for types `object` and `int`, in comparing `object` with `Literal[5]`" c = object() < 5 - # TODO: should be Unknown, once operand type check is implemented - reveal_type(c) # revealed: bool + reveal_type(c) # revealed: Unknown - # TODO: should error, once operand type check is implemented - # ("Operator `<` is not supported for types `int` and `object`") + # error: [unsupported-operator] "Operator `<` is not supported for types `int` and `object`, in comparing `Literal[5]` with `object`" d = 5 < object() - # TODO: should be Unknown, once operand type check is implemented - reveal_type(d) # revealed: bool + reveal_type(d) # revealed: Unknown int_literal_or_str_literal = 1 if flag else "foo" # error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1, "foo"]`" e = 42 in int_literal_or_str_literal reveal_type(e) # revealed: bool - # TODO: should error, need to check if __lt__ signature is valid for right operand - # error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]` + # error: [unsupported-operator] "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`" f = (1, 2) < (1, "hello") - # TODO: should be Unknown, once operand type check is implemented - reveal_type(f) # revealed: bool + reveal_type(f) # revealed: Unknown # error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`" g = (flag1, A()) < (flag2, A()) diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/for.md b/crates/red_knot_python_semantic/resources/mdtest/loops/for.md index 6ad8c2be49..06fcd44be5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/for.md +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/for.md @@ -245,9 +245,10 @@ class Test2: return 42 def _(flag: bool): + # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989) # error: "Object of type `Test | Test2` is not iterable" for x in Test() if flag else Test2(): - reveal_type(x) # revealed: Unknown + reveal_type(x) # revealed: int ``` ## Union type as iterator where one union element has no `__next__` method @@ -263,5 +264,5 @@ class Test: # error: [not-iterable] "Object of type `Test` is not iterable" for x in Test(): - reveal_type(x) # revealed: Unknown + reveal_type(x) # revealed: int ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/with/sync.md b/crates/red_knot_python_semantic/resources/mdtest/with/sync.md index 6d19288ed5..58e0e6f466 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/with/sync.md +++ b/crates/red_knot_python_semantic/resources/mdtest/with/sync.md @@ -80,7 +80,7 @@ class Manager: def __exit__(self, exc_tpe, exc_value, traceback): ... -# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` of type `int` is not callable" +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`" with Manager(): ... ``` @@ -95,7 +95,7 @@ class Manager: __exit__: int = 32 -# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__exit__` of type `int` is not callable" +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__exit__`" with Manager(): ... ``` @@ -134,3 +134,19 @@ def _(flag: bool): with Manager() as f: reveal_type(f) # revealed: str ``` + +## Invalid `__enter__` signature + +```py +class Manager: + def __enter__() -> str: + return "foo" + + def __exit__(self, exc_type, exc_value, traceback): ... + +context_expr = Manager() + +# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`" +with context_expr as f: + reveal_type(f) # revealed: str +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index d7e58fd841..b05fda4306 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1,11 +1,11 @@ use std::hash::Hash; use bitflags::bitflags; +use call::{CallDunderError, CallError}; use context::InferContext; use diagnostic::{report_not_iterable, report_not_iterable_possibly_unbound}; use indexmap::IndexSet; use itertools::Itertools; -use ruff_db::diagnostic::Severity; use ruff_db::files::File; use ruff_python_ast as ast; use ruff_python_ast::python_version::PythonVersion; @@ -36,9 +36,7 @@ use crate::symbol::{ global_symbol, imported_symbol, known_module_symbol, symbol, symbol_from_bindings, symbol_from_declarations, Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers, }; -use crate::types::call::{ - bind_call, CallArguments, CallBinding, CallDunderResult, CallOutcome, StaticAssertionErrorKind, -}; +use crate::types::call::{bind_call, CallArguments, CallBinding, CallOutcome}; use crate::types::class_base::ClassBase; use crate::types::diagnostic::INVALID_TYPE_FORM; use crate::types::infer::infer_unpack_types; @@ -1469,9 +1467,9 @@ impl<'db> Type<'db> { return Truthiness::Ambiguous; }; - if let Some(Type::BooleanLiteral(bool_val)) = bool_method + if let Ok(Type::BooleanLiteral(bool_val)) = bool_method .call_bound(db, instance_ty, &CallArguments::positional([])) - .return_type(db) + .map(|outcome| outcome.return_type(db)) { bool_val.into() } else { @@ -1544,72 +1542,39 @@ impl<'db> Type<'db> { } let return_ty = match self.call_dunder(db, "__len__", &CallArguments::positional([*self])) { - // TODO: emit a diagnostic - CallDunderResult::MethodNotAvailable => return None, + Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => outcome.return_type(db), - CallDunderResult::CallOutcome(outcome) | CallDunderResult::PossiblyUnbound(outcome) => { - outcome.return_type(db)? - } + // TODO: emit a diagnostic + Err(err) => err.return_type(db)?, }; non_negative_int_literal(db, return_ty) } - /// Return the outcome of calling an object of this type. - #[must_use] - fn call(self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) -> CallOutcome<'db> { + /// Calls `self` + /// + /// Returns `Ok` if the call with the given arguments is successful and `Err` otherwise. + fn call( + self, + db: &'db dyn Db, + arguments: &CallArguments<'_, 'db>, + ) -> Result, CallError<'db>> { match self { Type::FunctionLiteral(function_type) => { let mut binding = bind_call(db, arguments, function_type.signature(db), self); match function_type.known(db) { - Some(KnownFunction::RevealType) => { - let revealed_ty = binding.one_parameter_type().unwrap_or(Type::unknown()); - CallOutcome::revealed(binding, revealed_ty) - } - Some(KnownFunction::StaticAssert) => { - if let Some((parameter_ty, message)) = binding.two_parameter_types() { - let truthiness = parameter_ty.bool(db); - - if truthiness.is_always_true() { - CallOutcome::callable(binding) - } else { - let error_kind = if let Some(message) = - message.into_string_literal().map(|s| &**s.value(db)) - { - StaticAssertionErrorKind::CustomError(message) - } else if parameter_ty == Type::BooleanLiteral(false) { - StaticAssertionErrorKind::ArgumentIsFalse - } else if truthiness.is_always_false() { - StaticAssertionErrorKind::ArgumentIsFalsy(parameter_ty) - } else { - StaticAssertionErrorKind::ArgumentTruthinessIsAmbiguous( - parameter_ty, - ) - }; - - CallOutcome::StaticAssertionError { - binding, - error_kind, - } - } - } else { - CallOutcome::callable(binding) - } - } Some(KnownFunction::IsEquivalentTo) => { let (ty_a, ty_b) = binding .two_parameter_types() .unwrap_or((Type::unknown(), Type::unknown())); binding .set_return_type(Type::BooleanLiteral(ty_a.is_equivalent_to(db, ty_b))); - CallOutcome::callable(binding) } Some(KnownFunction::IsSubtypeOf) => { let (ty_a, ty_b) = binding .two_parameter_types() .unwrap_or((Type::unknown(), Type::unknown())); binding.set_return_type(Type::BooleanLiteral(ty_a.is_subtype_of(db, ty_b))); - CallOutcome::callable(binding) } Some(KnownFunction::IsAssignableTo) => { let (ty_a, ty_b) = binding @@ -1617,7 +1582,6 @@ impl<'db> Type<'db> { .unwrap_or((Type::unknown(), Type::unknown())); binding .set_return_type(Type::BooleanLiteral(ty_a.is_assignable_to(db, ty_b))); - CallOutcome::callable(binding) } Some(KnownFunction::IsDisjointFrom) => { let (ty_a, ty_b) = binding @@ -1625,7 +1589,6 @@ impl<'db> Type<'db> { .unwrap_or((Type::unknown(), Type::unknown())); binding .set_return_type(Type::BooleanLiteral(ty_a.is_disjoint_from(db, ty_b))); - CallOutcome::callable(binding) } Some(KnownFunction::IsGradualEquivalentTo) => { let (ty_a, ty_b) = binding @@ -1634,22 +1597,18 @@ impl<'db> Type<'db> { binding.set_return_type(Type::BooleanLiteral( ty_a.is_gradual_equivalent_to(db, ty_b), )); - CallOutcome::callable(binding) } Some(KnownFunction::IsFullyStatic) => { let ty = binding.one_parameter_type().unwrap_or(Type::unknown()); binding.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db))); - CallOutcome::callable(binding) } Some(KnownFunction::IsSingleton) => { let ty = binding.one_parameter_type().unwrap_or(Type::unknown()); binding.set_return_type(Type::BooleanLiteral(ty.is_singleton(db))); - CallOutcome::callable(binding) } Some(KnownFunction::IsSingleValued) => { let ty = binding.one_parameter_type().unwrap_or(Type::unknown()); binding.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db))); - CallOutcome::callable(binding) } Some(KnownFunction::Len) => { @@ -1658,108 +1617,111 @@ impl<'db> Type<'db> { binding.set_return_type(len_ty); } }; - - CallOutcome::callable(binding) } Some(KnownFunction::Repr) => { if let Some(first_arg) = binding.one_parameter_type() { binding.set_return_type(first_arg.repr(db)); }; - - CallOutcome::callable(binding) - } - - Some(KnownFunction::AssertType) => { - let Some((_, asserted_ty)) = binding.two_parameter_types() else { - return CallOutcome::callable(binding); - }; - - CallOutcome::asserted(binding, asserted_ty) } Some(KnownFunction::Cast) => { // TODO: Use `.two_parameter_tys()` exclusively // when overloads are supported. - if binding.two_parameter_types().is_none() { - return CallOutcome::callable(binding); - }; - if let Some(casted_ty) = arguments.first_argument() { - binding.set_return_type(casted_ty); + if binding.two_parameter_types().is_some() { + binding.set_return_type(casted_ty); + } }; - - CallOutcome::callable(binding) } - _ => CallOutcome::callable(binding), + _ => {} + }; + + if binding.has_binding_errors() { + Err(CallError::BindingError { binding }) + } else { + Ok(CallOutcome::Single(binding)) } } // TODO annotated return type on `__new__` or metaclass `__call__` // TODO check call vs signatures of `__new__` and/or `__init__` Type::ClassLiteral(ClassLiteralType { class }) => { - CallOutcome::callable(CallBinding::from_return_type(match class.known(db) { - // If the class is the builtin-bool class (for example `bool(1)`), we try to - // return the specific truthiness value of the input arg, `Literal[True]` for - // the example above. - Some(KnownClass::Bool) => arguments - .first_argument() - .map(|arg| arg.bool(db).into_type(db)) - .unwrap_or(Type::BooleanLiteral(false)), + Ok(CallOutcome::Single(CallBinding::from_return_type( + match class.known(db) { + // If the class is the builtin-bool class (for example `bool(1)`), we try to + // return the specific truthiness value of the input arg, `Literal[True]` for + // the example above. + Some(KnownClass::Bool) => arguments + .first_argument() + .map(|arg| arg.bool(db).into_type(db)) + .unwrap_or(Type::BooleanLiteral(false)), - Some(KnownClass::Str) => arguments - .first_argument() - .map(|arg| arg.str(db)) - .unwrap_or(Type::string_literal(db, "")), + // TODO: Don't ignore the second and third arguments to `str` + // https://github.com/astral-sh/ruff/pull/16161#discussion_r1958425568 + Some(KnownClass::Str) => arguments + .first_argument() + .map(|arg| arg.str(db)) + .unwrap_or(Type::string_literal(db, "")), - _ => Type::Instance(InstanceType { class }), - })) + _ => Type::Instance(InstanceType { class }), + }, + ))) } instance_ty @ Type::Instance(_) => { - match instance_ty.call_dunder(db, "__call__", &arguments.with_self(instance_ty)) { - CallDunderResult::CallOutcome(CallOutcome::NotCallable { .. }) => { - // Turn "`` not callable" into - // "`X` not callable" - CallOutcome::NotCallable { - not_callable_ty: self, + instance_ty + .call_dunder(db, "__call__", &arguments.with_self(instance_ty)) + .map_err(|err| match err { + CallDunderError::Call(CallError::NotCallable { .. }) => { + // Turn "`` not callable" into + // "`X` not callable" + CallError::NotCallable { + not_callable_ty: self, + } } - } - CallDunderResult::CallOutcome(outcome) => outcome, - CallDunderResult::PossiblyUnbound(call_outcome) => { + CallDunderError::Call(CallError::Union { + called_ty: _, + bindings, + errors, + }) => CallError::Union { + called_ty: self, + bindings, + errors, + }, + CallDunderError::Call(error) => error, // Turn "possibly unbound object of type `Literal['__call__']`" // into "`X` not callable (possibly unbound `__call__` method)" - CallOutcome::PossiblyUnboundDunderCall { - called_ty: self, - call_outcome: Box::new(call_outcome), + CallDunderError::PossiblyUnbound(outcome) => { + CallError::PossiblyUnboundDunderCall { + called_type: self, + outcome: Box::new(outcome), + } } - } - CallDunderResult::MethodNotAvailable => { - // Turn "`X.__call__` unbound" into "`X` not callable" - CallOutcome::NotCallable { - not_callable_ty: self, + CallDunderError::MethodNotAvailable => { + // Turn "`X.__call__` unbound" into "`X` not callable" + CallError::NotCallable { + not_callable_ty: self, + } } - } - } + }) } // Dynamic types are callable, and the return type is the same dynamic type - Type::Dynamic(_) => CallOutcome::callable(CallBinding::from_return_type(self)), + Type::Dynamic(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(self))), - Type::Union(union) => CallOutcome::union( - self, - union - .elements(db) - .iter() - .map(|elem| elem.call(db, arguments)), - ), + Type::Union(union) => { + CallOutcome::try_call_union(db, union, |element| element.call(db, arguments)) + } - Type::Intersection(_) => CallOutcome::callable(CallBinding::from_return_type( + Type::Intersection(_) => Ok(CallOutcome::Single(CallBinding::from_return_type( todo_type!("Type::Intersection.call()"), - )), + ))), - _ => CallOutcome::not_callable(self), + _ => Err(CallError::NotCallable { + not_callable_ty: self, + }), } } @@ -1769,13 +1731,12 @@ impl<'db> Type<'db> { /// `receiver_ty` must be `Type::Instance(_)` or `Type::ClassLiteral`. /// /// TODO: handle `super()` objects properly - #[must_use] fn call_bound( self, db: &'db dyn Db, receiver_ty: &Type<'db>, arguments: &CallArguments<'_, 'db>, - ) -> CallOutcome<'db> { + ) -> Result, CallError<'db>> { debug_assert!(receiver_ty.is_instance() || receiver_ty.is_class_literal()); match self { @@ -1790,22 +1751,20 @@ impl<'db> Type<'db> { self.call(db, arguments) } - Type::Union(union) => CallOutcome::union( - self, - union - .elements(db) - .iter() - .map(|elem| elem.call_bound(db, receiver_ty, arguments)), - ), + Type::Union(union) => CallOutcome::try_call_union(db, union, |element| { + element.call_bound(db, receiver_ty, arguments) + }), - Type::Intersection(_) => CallOutcome::callable(CallBinding::from_return_type( + Type::Intersection(_) => Ok(CallOutcome::Single(CallBinding::from_return_type( todo_type!("Type::Intersection.call_bound()"), - )), + ))), // Cases that duplicate, and thus must be kept in sync with, `Type::call()` - Type::Dynamic(_) => CallOutcome::callable(CallBinding::from_return_type(self)), + Type::Dynamic(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(self))), - _ => CallOutcome::not_callable(self), + _ => Err(CallError::NotCallable { + not_callable_ty: self, + }), } } @@ -1815,15 +1774,14 @@ impl<'db> Type<'db> { db: &'db dyn Db, name: &str, arguments: &CallArguments<'_, 'db>, - ) -> CallDunderResult<'db> { + ) -> Result, CallDunderError<'db>> { match self.to_meta_type(db).member(db, name) { - Symbol::Type(callable_ty, Boundness::Bound) => { - CallDunderResult::CallOutcome(callable_ty.call(db, arguments)) - } + Symbol::Type(callable_ty, Boundness::Bound) => Ok(callable_ty.call(db, arguments)?), Symbol::Type(callable_ty, Boundness::PossiblyUnbound) => { - CallDunderResult::PossiblyUnbound(callable_ty.call(db, arguments)) + let call = callable_ty.call(db, arguments)?; + Err(CallDunderError::PossiblyUnbound(call)) } - Symbol::Unbound => CallDunderResult::MethodNotAvailable, + Symbol::Unbound => Err(CallDunderError::MethodNotAvailable), } } @@ -1844,34 +1802,51 @@ impl<'db> Type<'db> { let dunder_iter_result = self.call_dunder(db, "__iter__", &CallArguments::positional([self])); - match dunder_iter_result { - CallDunderResult::CallOutcome(ref call_outcome) - | CallDunderResult::PossiblyUnbound(ref call_outcome) => { - let Some(iterator_ty) = call_outcome.return_type(db) else { - return IterationOutcome::NotIterable { - not_iterable_ty: self, - }; - }; + match &dunder_iter_result { + Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => { + let iterator_ty = outcome.return_type(db); - return if let Some(element_ty) = iterator_ty - .call_dunder(db, "__next__", &CallArguments::positional([iterator_ty])) - .return_type(db) - { - if matches!(dunder_iter_result, CallDunderResult::PossiblyUnbound(..)) { + return match iterator_ty.call_dunder( + db, + "__next__", + &CallArguments::positional([iterator_ty]), + ) { + Ok(outcome) => { + if matches!( + dunder_iter_result, + Err(CallDunderError::PossiblyUnbound { .. }) + ) { + IterationOutcome::PossiblyUnboundDunderIter { + iterable_ty: self, + element_ty: outcome.return_type(db), + } + } else { + IterationOutcome::Iterable { + element_ty: outcome.return_type(db), + } + } + } + Err(CallDunderError::PossiblyUnbound(outcome)) => { IterationOutcome::PossiblyUnboundDunderIter { iterable_ty: self, - element_ty, + element_ty: outcome.return_type(db), } - } else { - IterationOutcome::Iterable { element_ty } } - } else { - IterationOutcome::NotIterable { + Err(_) => IterationOutcome::NotIterable { not_iterable_ty: self, - } + }, }; } - CallDunderResult::MethodNotAvailable => {} + // If `__iter__` exists but can't be called or doesn't have the expected signature, + // return not iterable over falling back to `__getitem__`. + Err(CallDunderError::Call(_)) => { + return IterationOutcome::NotIterable { + not_iterable_ty: self, + } + } + Err(CallDunderError::MethodNotAvailable) => { + // No `__iter__` attribute, try `__getitem__` next. + } } // Although it's not considered great practice, @@ -1880,19 +1855,23 @@ impl<'db> Type<'db> { // // TODO(Alex) this is only valid if the `__getitem__` method is annotated as // accepting `int` or `SupportsIndex` - if let Some(element_ty) = self - .call_dunder( - db, - "__getitem__", - &CallArguments::positional([self, KnownClass::Int.to_instance(db)]), - ) - .return_type(db) - { - IterationOutcome::Iterable { element_ty } - } else { - IterationOutcome::NotIterable { - not_iterable_ty: self, + match self.call_dunder( + db, + "__getitem__", + &CallArguments::positional([self, KnownClass::Int.to_instance(db)]), + ) { + Ok(outcome) => IterationOutcome::Iterable { + element_ty: outcome.return_type(db), + }, + Err(CallDunderError::PossiblyUnbound(outcome)) => { + IterationOutcome::PossiblyUnboundDunderIter { + iterable_ty: self, + element_ty: outcome.return_type(db), + } } + Err(_) => IterationOutcome::NotIterable { + not_iterable_ty: self, + }, } } @@ -3694,20 +3673,23 @@ impl<'db> Class<'db> { let arguments = CallArguments::positional([name, bases, namespace]); let return_ty_result = match metaclass.call(db, &arguments) { - CallOutcome::NotCallable { not_callable_ty } => Err(MetaclassError { + Ok(outcome) => Ok(outcome.return_type(db)), + + Err(CallError::NotCallable { not_callable_ty }) => Err(MetaclassError { kind: MetaclassErrorKind::NotCallable(not_callable_ty), }), - CallOutcome::Union { - outcomes, + Err(CallError::Union { called_ty, - } => { + errors, + bindings, + }) => { let mut partly_not_callable = false; - let return_ty = outcomes + let return_ty = errors .iter() - .fold(None, |acc, outcome| { - let ty = outcome.return_type(db); + .fold(None, |acc, error| { + let ty = error.return_type(db); match (acc, ty) { (acc, None) => { @@ -3718,7 +3700,13 @@ impl<'db> Class<'db> { (Some(builder), Some(ty)) => Some(builder.add(ty)), } }) - .map(UnionBuilder::build); + .map(|mut builder| { + for binding in bindings { + builder = builder.add(binding.return_type()); + } + + builder.build() + }); if partly_not_callable { Err(MetaclassError { @@ -3729,16 +3717,13 @@ impl<'db> Class<'db> { } } - CallOutcome::PossiblyUnboundDunderCall { called_ty, .. } => Err(MetaclassError { - kind: MetaclassErrorKind::PartlyNotCallable(called_ty), + Err(CallError::PossiblyUnboundDunderCall { .. }) => Err(MetaclassError { + kind: MetaclassErrorKind::PartlyNotCallable(metaclass), }), // TODO we should also check for binding errors that would indicate the metaclass // does not accept the right arguments - CallOutcome::Callable { binding } - | CallOutcome::RevealType { binding, .. } - | CallOutcome::StaticAssertionError { binding, .. } - | CallOutcome::AssertType { binding, .. } => Ok(binding.return_type()), + Err(CallError::BindingError { binding }) => Ok(binding.return_type()), }; return return_ty_result.map(|ty| ty.to_meta_type(db)); diff --git a/crates/red_knot_python_semantic/src/types/call.rs b/crates/red_knot_python_semantic/src/types/call.rs index 13ab169ede..ad91c33ab9 100644 --- a/crates/red_knot_python_semantic/src/types/call.rs +++ b/crates/red_knot_python_semantic/src/types/call.rs @@ -1,423 +1,206 @@ use super::context::InferContext; -use super::diagnostic::{CALL_NON_CALLABLE, TYPE_ASSERTION_FAILURE}; -use super::{Severity, Signature, Type, TypeArrayDisplay, UnionBuilder}; -use crate::types::diagnostic::STATIC_ASSERT_ERROR; +use super::{Signature, Type}; +use crate::types::UnionType; use crate::Db; -use ruff_db::diagnostic::DiagnosticId; -use ruff_python_ast as ast; mod arguments; mod bind; - pub(super) use arguments::{Argument, CallArguments}; pub(super) use bind::{bind_call, CallBinding}; -#[derive(Debug, Clone, PartialEq, Eq)] -pub(super) enum StaticAssertionErrorKind<'db> { - ArgumentIsFalse, - ArgumentIsFalsy(Type<'db>), - ArgumentTruthinessIsAmbiguous(Type<'db>), - CustomError(&'db str), -} - +/// A successfully bound call where all arguments are valid. +/// +/// It's guaranteed that the wrapped bindings have no errors. #[derive(Debug, Clone, PartialEq, Eq)] pub(super) enum CallOutcome<'db> { - Callable { - binding: CallBinding<'db>, - }, - RevealType { - binding: CallBinding<'db>, - revealed_ty: Type<'db>, - }, - NotCallable { - not_callable_ty: Type<'db>, - }, - Union { - called_ty: Type<'db>, - outcomes: Box<[CallOutcome<'db>]>, - }, - PossiblyUnboundDunderCall { - called_ty: Type<'db>, - call_outcome: Box>, - }, - StaticAssertionError { - binding: CallBinding<'db>, - error_kind: StaticAssertionErrorKind<'db>, - }, - AssertType { - binding: CallBinding<'db>, - asserted_ty: Type<'db>, - }, + /// The call resolves to exactly one binding. + Single(CallBinding<'db>), + + /// The call resolves to multiple bindings. + Union(Box<[CallBinding<'db>]>), } impl<'db> CallOutcome<'db> { - /// Create a new `CallOutcome::Callable` with given binding. - pub(super) fn callable(binding: CallBinding<'db>) -> CallOutcome<'db> { - CallOutcome::Callable { binding } - } + /// Calls each union element using the provided `call` function. + /// + /// Returns `Ok` if all variants can be called without error according to the callback and `Err` otherwise. + pub(super) fn try_call_union( + db: &'db dyn Db, + union: UnionType<'db>, + call: F, + ) -> Result> + where + F: Fn(Type<'db>) -> Result>, + { + let elements = union.elements(db); + let mut bindings = Vec::with_capacity(elements.len()); + let mut errors = Vec::new(); + let mut not_callable = true; - /// Create a new `CallOutcome::NotCallable` with given not-callable type. - pub(super) fn not_callable(not_callable_ty: Type<'db>) -> CallOutcome<'db> { - CallOutcome::NotCallable { not_callable_ty } - } + for element in elements { + match call(*element) { + Ok(CallOutcome::Single(binding)) => bindings.push(binding), + Ok(CallOutcome::Union(inner_bindings)) => { + bindings.extend(inner_bindings); + } + Err(error) => { + not_callable |= error.is_not_callable(); + errors.push(error); + } + } + } - /// Create a new `CallOutcome::RevealType` with given revealed and return types. - pub(super) fn revealed(binding: CallBinding<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> { - CallOutcome::RevealType { - binding, - revealed_ty, + if errors.is_empty() { + Ok(CallOutcome::Union(bindings.into())) + } else if bindings.is_empty() && not_callable { + Err(CallError::NotCallable { + not_callable_ty: Type::Union(union), + }) + } else { + Err(CallError::Union { + errors: errors.into(), + bindings: bindings.into(), + called_ty: Type::Union(union), + }) } } - /// Create a new `CallOutcome::Union` with given wrapped outcomes. - pub(super) fn union( - called_ty: Type<'db>, - outcomes: impl IntoIterator>, - ) -> CallOutcome<'db> { - CallOutcome::Union { - called_ty, - outcomes: outcomes.into_iter().collect(), - } - } - - /// Create a new `CallOutcome::AssertType` with given asserted and return types. - pub(super) fn asserted(binding: CallBinding<'db>, asserted_ty: Type<'db>) -> CallOutcome<'db> { - CallOutcome::AssertType { - binding, - asserted_ty, - } - } - - /// Get the return type of the call, or `None` if not callable. - pub(super) fn return_type(&self, db: &'db dyn Db) -> Option> { + /// The type returned by this call. + pub(super) fn return_type(&self, db: &'db dyn Db) -> Type<'db> { match self { - Self::Callable { binding } => Some(binding.return_type()), - Self::RevealType { - binding, - revealed_ty: _, - } => Some(binding.return_type()), - Self::NotCallable { not_callable_ty: _ } => None, - Self::Union { - outcomes, - called_ty: _, - } => outcomes - .iter() - // If all outcomes are NotCallable, we return None; if some outcomes are callable - // and some are not, we return a union including Unknown. - .fold(None, |acc, outcome| { - let ty = outcome.return_type(db); - match (acc, ty) { - (None, None) => None, - (None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)), - (Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::unknown()))), - } - }) - .map(UnionBuilder::build), - Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_type(db), - Self::StaticAssertionError { .. } => Some(Type::none(db)), - Self::AssertType { - binding, - asserted_ty: _, - } => Some(binding.return_type()), - } - } - - /// Get the return type of the call, emitting default diagnostics if needed. - pub(super) fn unwrap_with_diagnostic( - &self, - context: &InferContext<'db>, - node: ast::AnyNodeRef, - ) -> Type<'db> { - match self.return_type_result(context, node) { - Ok(return_ty) => return_ty, - Err(NotCallableError::Type { - not_callable_ty, - return_ty, - }) => { - context.report_lint( - &CALL_NON_CALLABLE, - node, - format_args!( - "Object of type `{}` is not callable", - not_callable_ty.display(context.db()) - ), - ); - return_ty - } - Err(NotCallableError::UnionElement { - not_callable_ty, - called_ty, - return_ty, - }) => { - context.report_lint( - &CALL_NON_CALLABLE, - node, - format_args!( - "Object of type `{}` is not callable (due to union element `{}`)", - called_ty.display(context.db()), - not_callable_ty.display(context.db()), - ), - ); - return_ty - } - Err(NotCallableError::UnionElements { - not_callable_tys, - called_ty, - return_ty, - }) => { - context.report_lint( - &CALL_NON_CALLABLE, - node, - format_args!( - "Object of type `{}` is not callable (due to union elements {})", - called_ty.display(context.db()), - not_callable_tys.display(context.db()), - ), - ); - return_ty - } - Err(NotCallableError::PossiblyUnboundDunderCall { - callable_ty: called_ty, - return_ty, - }) => { - context.report_lint( - &CALL_NON_CALLABLE, - node, - format_args!( - "Object of type `{}` is not callable (possibly unbound `__call__` method)", - called_ty.display(context.db()) - ), - ); - return_ty + Self::Single(binding) => binding.return_type(), + Self::Union(bindings) => { + UnionType::from_elements(db, bindings.iter().map(bind::CallBinding::return_type)) } } } - /// Get the return type of the call as a result. - pub(super) fn return_type_result( - &self, - context: &InferContext<'db>, - node: ast::AnyNodeRef, - ) -> Result, NotCallableError<'db>> { - // TODO should this method emit diagnostics directly, or just return results that allow the - // caller to decide about emitting diagnostics? Currently it emits binding diagnostics, but - // only non-callable diagnostics in the union case, which is inconsistent. + pub(super) fn bindings(&self) -> &[CallBinding<'db>] { match self { - Self::Callable { binding } => { - binding.report_diagnostics(context, node); - Ok(binding.return_type()) - } - Self::RevealType { - binding, - revealed_ty, - } => { - binding.report_diagnostics(context, node); - context.report_diagnostic( - node, - DiagnosticId::RevealedType, - Severity::Info, - format_args!("Revealed type is `{}`", revealed_ty.display(context.db())), - ); - Ok(binding.return_type()) - } - Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type { - not_callable_ty: *not_callable_ty, - return_ty: Type::unknown(), - }), - Self::PossiblyUnboundDunderCall { - called_ty, - call_outcome, - } => Err(NotCallableError::PossiblyUnboundDunderCall { - callable_ty: *called_ty, - return_ty: call_outcome - .return_type(context.db()) - .unwrap_or(Type::unknown()), - }), - Self::Union { - outcomes, - called_ty, - } => { - let mut not_callable = vec![]; - let mut union_builder = UnionBuilder::new(context.db()); - let mut revealed = false; - for outcome in outcomes { - let return_ty = match outcome { - Self::NotCallable { not_callable_ty } => { - not_callable.push(*not_callable_ty); - Type::unknown() - } - Self::RevealType { - binding, - revealed_ty: _, - } => { - if revealed { - binding.return_type() - } else { - revealed = true; - outcome.unwrap_with_diagnostic(context, node) - } - } - _ => outcome.unwrap_with_diagnostic(context, node), - }; - union_builder = union_builder.add(return_ty); - } - let return_ty = union_builder.build(); - match not_callable[..] { - [] => Ok(return_ty), - [elem] => Err(NotCallableError::UnionElement { - not_callable_ty: elem, - called_ty: *called_ty, - return_ty, - }), - _ if not_callable.len() == outcomes.len() => Err(NotCallableError::Type { - not_callable_ty: *called_ty, - return_ty, - }), - _ => Err(NotCallableError::UnionElements { - not_callable_tys: not_callable.into_boxed_slice(), - called_ty: *called_ty, - return_ty, - }), - } - } - Self::StaticAssertionError { - binding, - error_kind, - } => { - binding.report_diagnostics(context, node); - - match error_kind { - StaticAssertionErrorKind::ArgumentIsFalse => { - context.report_lint( - &STATIC_ASSERT_ERROR, - node, - format_args!("Static assertion error: argument evaluates to `False`"), - ); - } - StaticAssertionErrorKind::ArgumentIsFalsy(parameter_ty) => { - context.report_lint( - &STATIC_ASSERT_ERROR, - node, - format_args!( - "Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy", - parameter_ty=parameter_ty.display(context.db()) - ), - ); - } - StaticAssertionErrorKind::ArgumentTruthinessIsAmbiguous(parameter_ty) => { - context.report_lint( - &STATIC_ASSERT_ERROR, - node, - format_args!( - "Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness", - parameter_ty=parameter_ty.display(context.db()) - ), - ); - } - StaticAssertionErrorKind::CustomError(message) => { - context.report_lint( - &STATIC_ASSERT_ERROR, - node, - format_args!("Static assertion error: {message}"), - ); - } - } - - Ok(Type::unknown()) - } - Self::AssertType { - binding, - asserted_ty, - } => { - let [actual_ty, _asserted] = binding.parameter_types() else { - return Ok(binding.return_type()); - }; - - if !actual_ty.is_gradual_equivalent_to(context.db(), *asserted_ty) { - context.report_lint( - &TYPE_ASSERTION_FAILURE, - node, - format_args!( - "Actual type `{}` is not the same as asserted type `{}`", - actual_ty.display(context.db()), - asserted_ty.display(context.db()), - ), - ); - } - - Ok(binding.return_type()) - } - } - } -} - -pub(super) enum CallDunderResult<'db> { - CallOutcome(CallOutcome<'db>), - PossiblyUnbound(CallOutcome<'db>), - MethodNotAvailable, -} - -impl<'db> CallDunderResult<'db> { - pub(super) fn return_type(&self, db: &'db dyn Db) -> Option> { - match self { - Self::CallOutcome(outcome) => outcome.return_type(db), - Self::PossiblyUnbound { .. } => None, - Self::MethodNotAvailable => None, + Self::Single(binding) => std::slice::from_ref(binding), + Self::Union(bindings) => bindings, } } } +/// The reason why calling a type failed. #[derive(Debug, Clone, PartialEq, Eq)] -pub(super) enum NotCallableError<'db> { +pub(super) enum CallError<'db> { /// The type is not callable. - Type { + NotCallable { + /// The type that can't be called. not_callable_ty: Type<'db>, - return_ty: Type<'db>, }, - /// A single union element is not callable. - UnionElement { - not_callable_ty: Type<'db>, + + /// A call to a union failed because at least one variant + /// can't be called with the given arguments. + /// + /// A union where all variants are not callable is represented as a `NotCallable` error. + Union { + /// The variants that can't be called with the given arguments. + errors: Box<[CallError<'db>]>, + + /// The bindings for the callable variants (that have no binding errors). + bindings: Box<[CallBinding<'db>]>, + + /// The union type that we tried calling. called_ty: Type<'db>, - return_ty: Type<'db>, - }, - /// Multiple (but not all) union elements are not callable. - UnionElements { - not_callable_tys: Box<[Type<'db>]>, - called_ty: Type<'db>, - return_ty: Type<'db>, }, + + /// The type has a `__call__` method but it isn't always bound. PossiblyUnboundDunderCall { - callable_ty: Type<'db>, - return_ty: Type<'db>, + called_type: Type<'db>, + outcome: Box>, }, + + /// The type is callable but not with the given arguments. + BindingError { binding: CallBinding<'db> }, } -impl<'db> NotCallableError<'db> { - /// The return type that should be used when a call is not callable. - pub(super) fn return_type(&self) -> Type<'db> { +impl<'db> CallError<'db> { + /// Returns a fallback return type to use that best approximates the return type of the call. + /// + /// Returns `None` if the type isn't callable. + pub(super) fn return_type(&self, db: &'db dyn Db) -> Option> { match self { - Self::Type { return_ty, .. } => *return_ty, - Self::UnionElement { return_ty, .. } => *return_ty, - Self::UnionElements { return_ty, .. } => *return_ty, - Self::PossiblyUnboundDunderCall { return_ty, .. } => *return_ty, + CallError::NotCallable { .. } => None, + // If some variants are callable, and some are not, return the union of the return types of the callable variants + // combined with `Type::Unknown` + CallError::Union { + errors, bindings, .. + } => Some(UnionType::from_elements( + db, + bindings + .iter() + .map(CallBinding::return_type) + .chain(errors.iter().map(|err| err.fallback_return_type(db))), + )), + Self::PossiblyUnboundDunderCall { outcome, .. } => Some(outcome.return_type(db)), + Self::BindingError { binding } => Some(binding.return_type()), } } + /// Returns the return type of the call or a fallback that + /// represents the best guess of the return type (e.g. the actual return type even if the + /// dunder is possibly unbound). + /// + /// If the type is not callable, returns `Type::Unknown`. + pub(super) fn fallback_return_type(&self, db: &'db dyn Db) -> Type<'db> { + self.return_type(db).unwrap_or(Type::unknown()) + } + /// The resolved type that was not callable. /// /// For unions, returns the union type itself, which may contain a mix of callable and /// non-callable types. pub(super) fn called_type(&self) -> Type<'db> { match self { - Self::Type { + Self::NotCallable { not_callable_ty, .. } => *not_callable_ty, - Self::UnionElement { called_ty, .. } => *called_ty, - Self::UnionElements { called_ty, .. } => *called_ty, - Self::PossiblyUnboundDunderCall { - callable_ty: called_ty, - .. - } => *called_ty, + Self::Union { called_ty, .. } => *called_ty, + Self::PossiblyUnboundDunderCall { called_type, .. } => *called_type, + Self::BindingError { binding } => binding.callable_type(), } } + + pub(super) const fn is_not_callable(&self) -> bool { + matches!(self, Self::NotCallable { .. }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum CallDunderError<'db> { + /// The dunder attribute exists but it can't be called with the given arguments. + /// + /// This includes non-callable dunder attributes that are possibly unbound. + Call(CallError<'db>), + + /// The type has the specified dunder method and it is callable + /// with the specified arguments without any binding errors + /// but it is possibly unbound. + PossiblyUnbound(CallOutcome<'db>), + + /// The dunder method with the specified name is missing. + MethodNotAvailable, +} + +impl<'db> CallDunderError<'db> { + pub(super) fn return_type(&self, db: &'db dyn Db) -> Option> { + match self { + Self::Call(error) => error.return_type(db), + Self::PossiblyUnbound(_) => None, + Self::MethodNotAvailable => None, + } + } + + pub(super) fn fallback_return_type(&self, db: &'db dyn Db) -> Type<'db> { + self.return_type(db).unwrap_or(Type::unknown()) + } +} + +impl<'db> From> for CallDunderError<'db> { + fn from(error: CallError<'db>) -> Self { + Self::Call(error) + } } 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 f2fb125d33..eba60e76f9 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -161,6 +161,10 @@ impl<'db> CallBinding<'db> { } } + pub(crate) fn callable_type(&self) -> Type<'db> { + self.callable_ty + } + pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) { self.return_ty = return_ty; } @@ -195,12 +199,16 @@ impl<'db> CallBinding<'db> { } } - pub(super) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) { + pub(crate) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) { let callable_name = self.callable_name(context.db()); for error in &self.errors { error.report_diagnostic(context, node, callable_name); } } + + pub(crate) fn has_binding_errors(&self) -> bool { + !self.errors.is_empty() + } } /// Information needed to emit a diagnostic regarding a parameter. diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 0b9f6d21ea..5c24ae8195 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -29,6 +29,7 @@ use std::num::NonZeroU32; use itertools::{Either, Itertools}; +use ruff_db::diagnostic::{DiagnosticId, Severity}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext}; @@ -66,29 +67,30 @@ use crate::types::diagnostic::{ use crate::types::mro::MroErrorKind; use crate::types::unpacker::{UnpackResult, Unpacker}; use crate::types::{ - todo_type, Boundness, CallDunderResult, Class, ClassLiteralType, DynamicType, FunctionType, - InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, - KnownFunction, KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, - SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, - TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, - TypeVarInstance, UnionBuilder, UnionType, + todo_type, Boundness, Class, ClassLiteralType, DynamicType, FunctionType, InstanceType, + IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction, + KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, SubclassOfType, + Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, + TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, + UnionType, }; use crate::unpack::Unpack; use crate::util::subscript::{PyIndex, PySlice}; use crate::Db; +use super::call::CallError; use super::context::{InNoTypeCheck, InferContext, WithDiagnostics}; use super::diagnostic::{ report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause, report_invalid_exception_raised, report_non_subscriptable, report_possibly_unresolved_reference, report_slice_step_size_zero, report_unresolved_reference, - INVALID_METACLASS, SUBCLASS_OF_FINAL_CLASS, + INVALID_METACLASS, STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE, }; use super::slots::check_class_slots; use super::string_annotation::{ parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, }; -use super::{global_symbol, ParameterExpectation, ParameterExpectations}; +use super::{global_symbol, CallDunderError, ParameterExpectation, ParameterExpectations}; /// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope. /// Use when checking a scope, or needing to provide a type for an arbitrary expression in the @@ -1616,16 +1618,20 @@ impl<'db> TypeInferenceBuilder<'db> { let target_ty = enter_ty .call(self.db(), &CallArguments::positional([context_expression_ty])) - .return_type_result(&self.context, context_expression.into()) - .unwrap_or_else(|err| { + .map(|outcome| outcome.return_type(self.db())) + .unwrap_or_else(|err| { + // TODO: Use more specific error messages for the different error cases. + // E.g. hint toward the union variant that doesn't correctly implement enter, + // distinguish between a not callable `__enter__` attribute and a wrong signature. self.context.report_lint( &INVALID_CONTEXT_MANAGER, context_expression.into(), format_args!(" - Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` of type `{enter_ty}` is not callable", context_expression = context_expression_ty.display(self.db()), enter_ty = enter_ty.display(self.db()) + Object of type `{context_expression}` cannot be used with `with` because it does not correctly implement `__enter__`", + context_expression = context_expression_ty.display(self.db()), ), ); - err.return_type() + err.fallback_return_type(self.db()) }); match exit { @@ -1663,16 +1669,17 @@ impl<'db> TypeInferenceBuilder<'db> { Type::none(self.db()), ]), ) - .return_type_result(&self.context, context_expression.into()) .is_err() { + // TODO: Use more specific error messages for the different error cases. + // E.g. hint toward the union variant that doesn't correctly implement enter, + // distinguish between a not callable `__exit__` attribute and a wrong signature. self.context.report_lint( &INVALID_CONTEXT_MANAGER, context_expression.into(), format_args!( - "Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` of type `{exit_ty}` is not callable", + "Object of type `{context_expression}` cannot be used with `with` because it does not correctly implement `__exit__`", context_expression = context_expression_ty.display(self.db()), - exit_ty = exit_ty.display(self.db()), ), ); } @@ -2207,10 +2214,8 @@ impl<'db> TypeInferenceBuilder<'db> { self.db(), &CallArguments::positional([target_type, value_type]), ); - let augmented_return_ty = match call - .return_type_result(&self.context, AnyNodeRef::StmtAugAssign(assignment)) - { - Ok(t) => t, + let augmented_return_ty = match call { + Ok(t) => t.return_type(self.db()), Err(e) => { self.context.report_lint( &UNSUPPORTED_OPERATOR, @@ -2221,7 +2226,7 @@ impl<'db> TypeInferenceBuilder<'db> { value_type.display(self.db()) ), ); - e.return_type() + e.fallback_return_type(self.db()) } }; @@ -3243,9 +3248,155 @@ impl<'db> TypeInferenceBuilder<'db> { .unwrap_or_default(); let call_arguments = self.infer_arguments(arguments, parameter_expectations); - function_type - .call(self.db(), &call_arguments) - .unwrap_with_diagnostic(&self.context, call_expression.into()) + let call = function_type.call(self.db(), &call_arguments); + + match call { + Ok(outcome) => { + for binding in outcome.bindings() { + let Some(known_function) = binding + .callable_type() + .into_function_literal() + .and_then(|function_type| function_type.known(self.db())) + else { + continue; + }; + + match known_function { + KnownFunction::RevealType => { + if let Some(revealed_type) = binding.one_parameter_type() { + self.context.report_diagnostic( + call_expression.into(), + DiagnosticId::RevealedType, + Severity::Info, + format_args!( + "Revealed type is `{}`", + revealed_type.display(self.db()) + ), + ); + } + } + KnownFunction::AssertType => { + if let [actual_ty, asserted_ty] = binding.parameter_types() { + if !actual_ty.is_gradual_equivalent_to(self.db(), *asserted_ty) { + self.context.report_lint( + &TYPE_ASSERTION_FAILURE, + call_expression.into(), + format_args!( + "Actual type `{}` is not the same as asserted type `{}`", + actual_ty.display(self.db()), + asserted_ty.display(self.db()), + ), + ); + } + } + } + KnownFunction::StaticAssert => { + if let Some((parameter_ty, message)) = binding.two_parameter_types() { + let truthiness = parameter_ty.bool(self.db()); + + if !truthiness.is_always_true() { + if let Some(message) = + message.into_string_literal().map(|s| &**s.value(self.db())) + { + self.context.report_lint( + &STATIC_ASSERT_ERROR, + call_expression.into(), + format_args!("Static assertion error: {message}"), + ); + } else if parameter_ty == Type::BooleanLiteral(false) { + self.context.report_lint( + &STATIC_ASSERT_ERROR, + call_expression.into(), + format_args!("Static assertion error: argument evaluates to `False`"), + ); + } else if truthiness.is_always_false() { + self.context.report_lint( + &STATIC_ASSERT_ERROR, + call_expression.into(), + format_args!( + "Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy", + parameter_ty=parameter_ty.display(self.db()) + ), + ); + } else { + self.context.report_lint( + &STATIC_ASSERT_ERROR, + call_expression.into(), + format_args!( + "Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness", + parameter_ty=parameter_ty.display(self.db()) + ), + ); + }; + } + } + } + _ => {} + } + } + + outcome.return_type(self.db()) + } + Err(err) => { + // TODO: We currently only report the first error. Ideally, we'd report + // an error saying that the union type can't be called, followed by a sub + // diagnostic explaining why. + fn report_call_error( + context: &InferContext, + err: CallError, + call_expression: &ast::ExprCall, + ) { + match err { + CallError::NotCallable { not_callable_ty } => { + context.report_lint( + &CALL_NON_CALLABLE, + call_expression.into(), + format_args!( + "Object of type `{}` is not callable", + not_callable_ty.display(context.db()) + ), + ); + } + + CallError::Union { + called_ty: _, + bindings: _, + errors, + } => { + // TODO: Remove the `Vec::from` call once we use the Rust 2024 edition + // which adds `Box<[T]>::into_iter` + if let Some(first) = Vec::from(errors).into_iter().next() { + report_call_error(context, first, call_expression); + } else { + debug_assert!( + false, + "Expected `CalLError::Union` to at least have one error" + ); + } + } + + CallError::PossiblyUnboundDunderCall { called_type, .. } => { + context.report_lint( + &CALL_NON_CALLABLE, + call_expression.into(), + format_args!( + "Object of type `{}` is not callable (possibly unbound `__call__` method)", + called_type.display(context.db()) + ), + ); + } + CallError::BindingError { binding, .. } => { + binding.report_diagnostics(context, call_expression.into()); + } + } + } + + let return_type = err.fallback_return_type(self.db()); + report_call_error(&self.context, err, call_expression); + + return_type + } + } } fn infer_starred_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> { @@ -3567,37 +3718,23 @@ impl<'db> TypeInferenceBuilder<'db> { } }; - if let CallDunderResult::CallOutcome(call) - | CallDunderResult::PossiblyUnbound(call) = operand_type.call_dunder( + match operand_type.call_dunder( self.db(), unary_dunder_method, &CallArguments::positional([operand_type]), ) { - match call.return_type_result(&self.context, AnyNodeRef::ExprUnaryOp(unary)) { - Ok(t) => t, - Err(e) => { - self.context.report_lint( - &UNSUPPORTED_OPERATOR, - unary.into(), - format_args!( - "Unary operator `{op}` is unsupported for type `{}`", - operand_type.display(self.db()), - ), - ); - e.return_type() - } + Ok(outcome) => outcome.return_type(self.db()), + Err(e) => { + self.context.report_lint( + &UNSUPPORTED_OPERATOR, + unary.into(), + format_args!( + "Unary operator `{op}` is unsupported for type `{}`", + operand_type.display(self.db()), + ), + ); + e.fallback_return_type(self.db()) } - } else { - self.context.report_lint( - &UNSUPPORTED_OPERATOR, - unary.into(), - format_args!( - "Unary operator `{op}` is unsupported for type `{}`", - operand_type.display(self.db()), - ), - ); - - Type::unknown() } } } @@ -3835,25 +3972,28 @@ impl<'db> TypeInferenceBuilder<'db> { reflected_dunder, &CallArguments::positional([right_ty, left_ty]), ) - .return_type(self.db()) - .or_else(|| { + .map(|outcome| outcome.return_type(self.db())) + .or_else(|_| { left_ty .call_dunder( self.db(), op.dunder(), &CallArguments::positional([left_ty, right_ty]), ) - .return_type(self.db()) - }); + .map(|outcome| outcome.return_type(self.db())) + }) + .ok(); } } + // TODO: Use `call_dunder`? let call_on_left_instance = if let Symbol::Type(class_member, _) = left_class.member(self.db(), op.dunder()) { class_member .call(self.db(), &CallArguments::positional([left_ty, right_ty])) - .return_type(self.db()) + .map(|outcome| outcome.return_type(self.db())) + .ok() } else { None }; @@ -3865,9 +4005,11 @@ impl<'db> TypeInferenceBuilder<'db> { if let Symbol::Type(class_member, _) = right_class.member(self.db(), op.reflected_dunder()) { + // TODO: Use `call_dunder` class_member .call(self.db(), &CallArguments::positional([right_ty, left_ty])) - .return_type(self.db()) + .map(|outcome| outcome.return_type(self.db())) + .ok() } else { None } @@ -4626,43 +4768,44 @@ impl<'db> TypeInferenceBuilder<'db> { Type::IntLiteral(i64::from(bool)), ), (value_ty, slice_ty) => { - // Resolve the value to its class. - let value_meta_ty = value_ty.to_meta_type(self.db()); - // If the class defines `__getitem__`, return its return type. // // See: https://docs.python.org/3/reference/datamodel.html#class-getitem-versus-getitem - match value_meta_ty.member(self.db(), "__getitem__") { - Symbol::Unbound => {} - Symbol::Type(dunder_getitem_method, boundness) => { - if boundness == Boundness::PossiblyUnbound { - self.context.report_lint( - &CALL_POSSIBLY_UNBOUND_METHOD, + match value_ty.call_dunder( + self.db(), + "__getitem__", + &CallArguments::positional([value_ty, slice_ty]), + ) { + Ok(outcome) => return outcome.return_type(self.db()), + Err(err @ CallDunderError::PossiblyUnbound { .. }) => { + self.context.report_lint( + &CALL_POSSIBLY_UNBOUND_METHOD, + value_node.into(), + format_args!( + "Method `__getitem__` of type `{}` is possibly unbound", + value_ty.display(self.db()), + ), + ); + + return err.fallback_return_type(self.db()); + } + Err(CallDunderError::Call(err)) => { + self.context.report_lint( + &CALL_NON_CALLABLE, value_node.into(), format_args!( - "Method `__getitem__` of type `{}` is possibly unbound", + "Method `__getitem__` of type `{}` is not callable on object of type `{}`", + err.called_type().display(self.db()), value_ty.display(self.db()), ), ); - } - return dunder_getitem_method - .call(self.db(), &CallArguments::positional([value_ty, slice_ty])) - .return_type_result(&self.context, value_node.into()) - .unwrap_or_else(|err| { - self.context.report_lint( - &CALL_NON_CALLABLE, - value_node.into(), - format_args!( - "Method `__getitem__` of type `{}` is not callable on object of type `{}`", - err.called_type().display(self.db()), - value_ty.display(self.db()), - ), - ); - err.return_type() - }); + return err.fallback_return_type(self.db()); } - } + Err(CallDunderError::MethodNotAvailable) => { + // try `__class_getitem__` + } + }; // Otherwise, if the value is itself a class and defines `__class_getitem__`, // return its return type. @@ -4693,7 +4836,7 @@ impl<'db> TypeInferenceBuilder<'db> { return ty .call(self.db(), &CallArguments::positional([value_ty, slice_ty])) - .return_type_result(&self.context, value_node.into()) + .map(|outcome| outcome.return_type(self.db())) .unwrap_or_else(|err| { self.context.report_lint( &CALL_NON_CALLABLE, @@ -4704,7 +4847,7 @@ impl<'db> TypeInferenceBuilder<'db> { value_ty.display(self.db()), ), ); - err.return_type() + err.fallback_return_type(self.db()) }); } } @@ -5929,23 +6072,20 @@ fn perform_rich_comparison<'db>( ) -> Result, CompareUnsupportedError<'db>> { // The following resource has details about the rich comparison algorithm: // https://snarky.ca/unravelling-rich-comparison-operators/ - // - // TODO: this currently gives the return type even if the arg types are invalid - // (e.g. int.__lt__ with string instance should be errored, currently bool) - - let call_dunder = |op: RichCompareOperator, - left: InstanceType<'db>, - right: InstanceType<'db>| { - match left.class.class_member(db, op.dunder()) { - Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder - .call( - db, - &CallArguments::positional([Type::Instance(left), Type::Instance(right)]), - ) - .return_type(db), - _ => None, - } - }; + let call_dunder = + |op: RichCompareOperator, left: InstanceType<'db>, right: InstanceType<'db>| { + // TODO: How do we want to handle possibly unbound dunder methods? + match left.class.class_member(db, op.dunder()) { + Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder + .call( + db, + &CallArguments::positional([Type::Instance(left), Type::Instance(right)]), + ) + .map(|outcome| outcome.return_type(db)) + .ok(), + _ => None, + } + }; // The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side. if left != right && right.is_subtype_of(db, left) { @@ -5989,7 +6129,8 @@ fn perform_membership_test_comparison<'db>( db, &CallArguments::positional([Type::Instance(right), Type::Instance(left)]), ) - .return_type(db) + .map(|outcome| outcome.return_type(db)) + .ok() } _ => { // iteration-based membership test