ruff/crates/red_knot_python_semantic/resources/mdtest/function/parameters.md
Brent Westbrook 9c47b6dbb0
[red-knot] Detect version-related syntax errors (#16379)
## Summary
This PR extends version-related syntax error detection to red-knot. The
main changes here are:

1. Passing `ParseOptions` specifying a `PythonVersion` to parser calls
2. Adding a `python_version` method to the `Db` trait to make this
possible
3. Converting `UnsupportedSyntaxError`s to `Diagnostic`s
4. Updating existing mdtests  to avoid unrelated syntax errors

My initial draft of (1) and (2) in #16090 instead tried passing a
`PythonVersion` down to every parser call, but @MichaReiser suggested
the `Db` approach instead
[here](https://github.com/astral-sh/ruff/pull/16090#discussion_r1969198407),
and I think it turned out much nicer.

All of the new `python_version` methods look like this:

```rust
fn python_version(&self) -> ruff_python_ast::PythonVersion {
    Program::get(self).python_version(self)
}
```

with the exception of the `TestDb` in `ruff_db`, which hard-codes
`PythonVersion::latest()`.

## Test Plan

Existing mdtests, plus a new mdtest to see at least one of the new
diagnostics.
2025-04-17 14:00:30 -04:00

3.2 KiB

Function parameter types

Within a function scope, the declared type of each parameter is its annotated type (or Unknown if not annotated). The initial inferred type is the union of the declared type with the type of the default value expression (if any). If both are fully static types, this union should simplify to the annotated type (since the default value type must be assignable to the annotated type, and for fully static types this means subtype-of, which simplifies in unions). But if the annotated type is Unknown or another non-fully-static type, the default value type may still be relevant as lower bound.

The variadic parameter is a variadic tuple of its annotated type; the variadic-keywords parameter is a dictionary from strings to its annotated type.

Parameter kinds

from typing import Literal

def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5, h: Literal[6] = 6, **kwargs: str):
    reveal_type(a)  # revealed: Unknown
    reveal_type(b)  # revealed: int
    reveal_type(c)  # revealed: Unknown | Literal[1]
    reveal_type(d)  # revealed: int
    reveal_type(e)  # revealed: Unknown | Literal[3]
    reveal_type(f)  # revealed: Literal[4]
    reveal_type(g)  # revealed: Unknown | Literal[5]
    reveal_type(h)  # revealed: Literal[6]

    # TODO: should be `tuple[object, ...]` (needs generics)
    reveal_type(args)  # revealed: tuple

    # TODO: should be `dict[str, str]` (needs generics)
    reveal_type(kwargs)  # revealed: dict

Unannotated variadic parameters

...are inferred as tuple of Unknown or dict from string to Unknown.

def g(*args, **kwargs):
    # TODO: should be `tuple[Unknown, ...]` (needs generics)
    reveal_type(args)  # revealed: tuple

    # TODO: should be `dict[str, Unknown]` (needs generics)
    reveal_type(kwargs)  # revealed: dict

Annotation is present but not a fully static type

The default value type should be a lower bound on the inferred type.

from typing import Any

def f(x: Any = 1):
    reveal_type(x)  # revealed: Any | Literal[1]

Default value type must be assignable to annotated type

The default value type must be assignable to the annotated type. If not, we emit a diagnostic, and fall back to inferring the annotated type, ignoring the default value type.

# error: [invalid-parameter-default]
def f(x: int = "foo"):
    reveal_type(x)  # revealed: int

# The check is assignable-to, not subtype-of, so this is fine:
from typing import Any

def g(x: Any = "foo"):
    reveal_type(x)  # revealed: Any | Literal["foo"]

Stub functions

[environment]
python-version = "3.12"

In Protocol

from typing import Protocol

class Foo(Protocol):
    def x(self, y: bool = ...): ...
    def y[T](self, y: T = ...) -> T: ...

class GenericFoo[T](Protocol):
    def x(self, y: bool = ...) -> T: ...

In abstract method

from abc import abstractmethod

class Bar:
    @abstractmethod
    def x(self, y: bool = ...): ...
    @abstractmethod
    def y[T](self, y: T = ...) -> T: ...

In function overload

from typing import overload

@overload
def x(y: None = ...) -> None: ...
@overload
def x(y: int) -> str: ...
def x(y: int | None = None) -> str | None: ...