ruff/crates/red_knot_python_semantic/resources/mdtest/call/union.md
Douglas Creager c03c28d199
[red-knot] Break up call binding into two phases (#16546)
This breaks up call binding into two phases:

- **_Matching parameters_** just looks at the names and kinds
(positional/keyword) of each formal and actual parameters, and matches
them up. Most of the current call binding errors happen during this
phase.

- Once we have matched up formal and actual parameters, we can **_infer
types_** of each actual parameter, and **_check_** that each one is
assignable to the corresponding formal parameter type.

As part of this, we add information to each formal parameter about
whether it is a type form or not. Once [PEP
747](https://peps.python.org/pep-0747/) is finalized, we can hook that
up to this internal type form representation. This replaces the
`ParameterExpectations` type, which did the same thing in a more ad hoc
way.

While we're here, we add a new fluent API for building `Parameter`s,
which makes our signature constructors a bit nicer to read. We also
eliminate a TODO where we were consuming types from the argument list
instead of the bound parameter list when evaluating our special-case
known functions.

Closes #15460

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-03-21 09:38:11 -04:00

3.9 KiB

Unions in calls

Union of return types

def _(flag: bool):
    if flag:
        def f() -> int:
            return 1
    else:
        def f() -> str:
            return "foo"
    reveal_type(f())  # revealed: int | str

Calling with an unknown union

from nonexistent import f  # error: [unresolved-import] "Cannot resolve import `nonexistent`"

def coinflip() -> bool:
    return True

if coinflip():
    def f() -> int:
        return 1

reveal_type(f())  # revealed: Unknown | int

Non-callable elements in a union

Calling a union with a non-callable element should emit a diagnostic.

def _(flag: bool):
    if flag:
        f = 1
    else:
        def f() -> int:
            return 1
    x = f()  # error: [call-non-callable] "Object of type `Literal[1]` is not callable"
    reveal_type(x)  # revealed: Unknown | int

Multiple non-callable elements in a union

Calling a union with multiple non-callable elements should mention all of them in the diagnostic.

def _(flag: bool, flag2: bool):
    if flag:
        f = 1
    elif flag2:
        f = "foo"
    else:
        def f() -> int:
            return 1
    # TODO we should mention all non-callable elements of the union
    # error: [call-non-callable] "Object of type `Literal[1]` is not callable"
    # revealed: Unknown | int
    reveal_type(f())

All non-callable union elements

Calling a union with no callable elements can emit a simpler diagnostic.

def _(flag: bool):
    if flag:
        f = 1
    else:
        f = "foo"

    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.

def f1(a: int) -> int:
    return a

def f2(a: str) -> str:
    return a

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

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

Union of binding errors

def f1(): ...
def f2(): ...
def _(flag: bool):
    if flag:
        f = f1
    else:
        f = f2

    # TODO: we should show all errors from the union, not arbitrarily pick one union element
    # error: [too-many-positional-arguments] "Too many positional arguments to function `f1`: expected 0, got 1"
    x = f(3)
    reveal_type(x)  # revealed: Unknown

One not-callable, one wrong argument

class C: ...

def f1(): ...
def _(flag: bool):
    if flag:
        f = f1
    else:
        f = C()

    # TODO: we should either show all union errors here, or prioritize the not-callable error
    # error: [too-many-positional-arguments] "Too many positional arguments to function `f1`: expected 0, got 1"
    x = f(3)
    reveal_type(x)  # revealed: Unknown

Union including a special-cased function

def _(flag: bool):
    if flag:
        f = str
    else:
        f = repr
    reveal_type(str("string"))  # revealed: Literal["string"]
    reveal_type(repr("string"))  # revealed: Literal["'string'"]
    reveal_type(f("string"))  # revealed: Literal["string", "'string'"]

Cannot use an argument as both a value and a type form

from knot_extensions import is_fully_static

def _(flag: bool):
    if flag:
        f = repr
    else:
        f = is_fully_static
    # error: [conflicting-argument-forms] "Argument is used as both a value and a type form in call"
    reveal_type(f(int))  # revealed: str | Literal[True]