mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-27 18:36:35 +00:00
Some checks are pending
CI / cargo fuzz build (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
## Summary
This PR resolves the way diagnostics are reported for an invalid call to
an overloaded function.
If any of the steps in the overload call evaluation algorithm yields a
matching overload but it's type checking that failed, the
`no-matching-overload` diagnostic is incorrect because there is a
matching overload, it's the arguments passed that are invalid as per the
signature. So, this PR improves that by surfacing the diagnostics on the
matching overload directly.
It also provides additional context, specifically the matching overload
where this error occurred and other non-matching overloads. Consider the
following example:
```py
from typing import overload
@overload
def f() -> None: ...
@overload
def f(x: int) -> int: ...
@overload
def f(x: int, y: int) -> int: ...
def f(x: int | None = None, y: int | None = None) -> int | None:
return None
f("a")
```
We get:
<img width="857" alt="Screenshot 2025-06-18 at 11 07 10"
src="https://github.com/user-attachments/assets/8dbcaf13-2a74-4661-aa94-1225c9402ea6"
/>
## Test Plan
Update test cases, resolve existing todos and validate the updated
snapshots.
3.3 KiB
3.3 KiB
Calling a union of function types
[environment]
python-version = "3.12"
A smaller scale example
def f1() -> int:
return 0
def f2(name: str) -> int:
return 0
def _(flag: bool):
if flag:
f = f1
else:
f = f2
# error: [too-many-positional-arguments]
# error: [invalid-argument-type]
x = f(3)
Multiple variants but only one is invalid
This test in particular demonstrates some of the smarts of this diagnostic. Namely, since only one variant is invalid, additional context specific to that variant is added to the diagnostic output. (If more than one variant is invalid, then this additional context is elided to avoid overwhelming the end user.)
def f1(a: int) -> int:
return 0
def f2(name: str) -> int:
return 0
def _(flag: bool):
if flag:
f = f1
else:
f = f2
# error: [invalid-argument-type]
x = f(3)
Try to cover all possible reasons
These tests is likely to become stale over time, but this was added when the union-specific diagnostic was initially created. In each test, we try to cover as much as we can. This is mostly just ensuring that we get test coverage for each of the possible diagnostic messages.
Cover non-keyword related reasons
from inspect import getattr_static
from typing import overload
def f1() -> int:
return 0
def f2(name: str) -> int:
return 0
def f3(a: int, b: int) -> int:
return 0
def f4[T: str](x: T) -> int:
return 0
@overload
def f5() -> None: ...
@overload
def f5(x: str) -> str: ...
def f5(x: str | None = None) -> str | None:
return x
@overload
def f6() -> None: ...
@overload
def f6(x: str, y: str) -> str: ...
def f6(x: str | None = None, y: str | None = None) -> str | None:
return x + y if x and y else None
def _(n: int):
class PossiblyNotCallable:
if n == 0:
def __call__(self) -> int:
return 0
if n == 0:
f = f1
elif n == 1:
f = f2
elif n == 2:
f = f3
elif n == 3:
f = f4
elif n == 4:
f = 5
elif n == 5:
f = f5
elif n == 6:
f = f6
else:
f = PossiblyNotCallable()
# error: [too-many-positional-arguments]
# error: [invalid-argument-type] "Argument to function `f2` is incorrect: Expected `str`, found `Literal[3]`"
# error: [missing-argument]
# error: [invalid-argument-type] "Argument to function `f4` is incorrect: Argument type `Literal[3]` does not satisfy upper bound of type variable `T`"
# error: [invalid-argument-type] "Argument to function `f5` is incorrect: Expected `str`, found `Literal[3]`"
# error: [no-matching-overload] "No overload of function `f6` matches arguments"
# error: [call-non-callable] "Object of type `Literal[5]` is not callable"
# error: [call-non-callable] "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
x = f(3)
Cover keyword argument related reasons
def any(*args, **kwargs) -> int:
return 0
def f1(name: str) -> int:
return 0
def _(n: int):
if n == 0:
f = f1
else:
f = any
# error: [parameter-already-assigned]
# error: [unknown-argument]
y = f("foo", name="bar", unknown="quux")