mirror of https://github.com/astral-sh/ruff
[red-knot] `knot_extensions` Python API (#15103)
## Summary Adds a type-check-time Python API that allows us to create and manipulate types and to test various of their properties. For example, this can be used to write a Markdown test to make sure that `A & B` is a subtype of `A` and `B`, but not of an unrelated class `C` (something that requires quite a bit more code to do in Rust): ```py from knot_extensions import Intersection, is_subtype_of, static_assert class A: ... class B: ... type AB = Intersection[A, B] static_assert(is_subtype_of(AB, A)) static_assert(is_subtype_of(AB, B)) class C: ... static_assert(not is_subtype_of(AB, C)) ``` I think this functionality is also helpful for interactive debugging sessions, in order to query various properties of Red Knot's type system. Which is something that otherwise requires a custom Rust unit test, some boilerplate code and constant re-compilation. ## Test Plan - New Markdown tests - Tested the modified typeshed_sync workflow locally
This commit is contained in:
parent
03ff883626
commit
235fdfc57a
|
|
@ -46,6 +46,10 @@ jobs:
|
||||||
cp -r typeshed/stdlib ruff/crates/red_knot_vendored/vendor/typeshed/stdlib
|
cp -r typeshed/stdlib ruff/crates/red_knot_vendored/vendor/typeshed/stdlib
|
||||||
rm -rf ruff/crates/red_knot_vendored/vendor/typeshed/stdlib/@tests
|
rm -rf ruff/crates/red_knot_vendored/vendor/typeshed/stdlib/@tests
|
||||||
git -C typeshed rev-parse HEAD > ruff/crates/red_knot_vendored/vendor/typeshed/source_commit.txt
|
git -C typeshed rev-parse HEAD > ruff/crates/red_knot_vendored/vendor/typeshed/source_commit.txt
|
||||||
|
# Patch the typeshed stubs to include `knot_extensions`
|
||||||
|
ln -s ../../../knot_extensions/knot_extensions.pyi ruff/crates/red_knot_vendored/vendor/typeshed/stdlib/
|
||||||
|
echo "# Patch applied for red_knot:" >> ruff/crates/red_knot_vendored/vendor/typeshed/stdlib/VERSIONS
|
||||||
|
echo "knot_extensions: 3.0-" >> ruff/crates/red_knot_vendored/vendor/typeshed/stdlib/VERSIONS
|
||||||
- name: Commit the changes
|
- name: Commit the changes
|
||||||
id: commit
|
id: commit
|
||||||
if: ${{ steps.sync.outcome == 'success' }}
|
if: ${{ steps.sync.outcome == 'success' }}
|
||||||
|
|
|
||||||
|
|
@ -257,3 +257,67 @@ def f(x: int) -> int:
|
||||||
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
|
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
|
||||||
reveal_type(f(1, x=2)) # revealed: int
|
reveal_type(f(1, x=2)) # revealed: int
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Special functions
|
||||||
|
|
||||||
|
Some functions require special handling in type inference. Here, we make sure that we still emit
|
||||||
|
proper diagnostics in case of missing or superfluous arguments.
|
||||||
|
|
||||||
|
### `reveal_type`
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing_extensions import reveal_type
|
||||||
|
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`"
|
||||||
|
reveal_type() # revealed: Unknown
|
||||||
|
|
||||||
|
# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2"
|
||||||
|
reveal_type(1, 2) # revealed: Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `static_assert`
|
||||||
|
|
||||||
|
```py
|
||||||
|
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"
|
||||||
|
static_assert(True, 2, 3)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `len`
|
||||||
|
|
||||||
|
```py
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `obj` of function `len`"
|
||||||
|
len()
|
||||||
|
|
||||||
|
# error: [too-many-positional-arguments] "Too many positional arguments to function `len`: expected 1, got 2"
|
||||||
|
len([], 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type API predicates
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import is_subtype_of, is_fully_static
|
||||||
|
|
||||||
|
# error: [missing-argument]
|
||||||
|
is_subtype_of()
|
||||||
|
|
||||||
|
# error: [missing-argument]
|
||||||
|
is_subtype_of(int)
|
||||||
|
|
||||||
|
# error: [too-many-positional-arguments]
|
||||||
|
is_subtype_of(int, int, int)
|
||||||
|
|
||||||
|
# error: [too-many-positional-arguments]
|
||||||
|
is_subtype_of(int, int, int, int)
|
||||||
|
|
||||||
|
# error: [missing-argument]
|
||||||
|
is_fully_static()
|
||||||
|
|
||||||
|
# error: [too-many-positional-arguments]
|
||||||
|
is_fully_static(int, int)
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,352 @@
|
||||||
|
# Type API (`knot_extensions`)
|
||||||
|
|
||||||
|
This document describes the internal `knot_extensions` API for creating and manipulating types as
|
||||||
|
well as testing various type system properties.
|
||||||
|
|
||||||
|
## Type extensions
|
||||||
|
|
||||||
|
The Python language itself allows us to perform a variety of operations on types. For example, we
|
||||||
|
can build a union of types like `int | None`, or we can use type constructors such as `list[int]`
|
||||||
|
and `type[int]` to create new types. But some type-level operations that we rely on in Red Knot,
|
||||||
|
like intersections, cannot yet be expressed in Python. The `knot_extensions` module provides the
|
||||||
|
`Intersection` and `Not` type constructors (special forms) which allow us to construct these types
|
||||||
|
directly.
|
||||||
|
|
||||||
|
### Negation
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import Not, static_assert
|
||||||
|
|
||||||
|
def negate(n1: Not[int], n2: Not[Not[int]], n3: Not[Not[Not[int]]]) -> None:
|
||||||
|
reveal_type(n1) # revealed: ~int
|
||||||
|
reveal_type(n2) # revealed: int
|
||||||
|
reveal_type(n3) # revealed: ~int
|
||||||
|
|
||||||
|
def static_truthiness(not_one: Not[Literal[1]]) -> None:
|
||||||
|
static_assert(not_one != 1)
|
||||||
|
static_assert(not (not_one == 1))
|
||||||
|
|
||||||
|
# error: "Special form `knot_extensions.Not` expected exactly one type parameter"
|
||||||
|
n: Not[int, str]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Intersection
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import Intersection, Not, is_subtype_of, static_assert
|
||||||
|
from typing_extensions import Never
|
||||||
|
|
||||||
|
class S: ...
|
||||||
|
class T: ...
|
||||||
|
|
||||||
|
def x(x1: Intersection[S, T], x2: Intersection[S, Not[T]]) -> None:
|
||||||
|
reveal_type(x1) # revealed: S & T
|
||||||
|
reveal_type(x2) # revealed: S & ~T
|
||||||
|
|
||||||
|
def y(y1: Intersection[int, object], y2: Intersection[int, bool], y3: Intersection[int, Never]) -> None:
|
||||||
|
reveal_type(y1) # revealed: int
|
||||||
|
reveal_type(y2) # revealed: bool
|
||||||
|
reveal_type(y3) # revealed: Never
|
||||||
|
|
||||||
|
def z(z1: Intersection[int, Not[Literal[1]], Not[Literal[2]]]) -> None:
|
||||||
|
reveal_type(z1) # revealed: int & ~Literal[1] & ~Literal[2]
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
class B: ...
|
||||||
|
class C: ...
|
||||||
|
|
||||||
|
type ABC = Intersection[A, B, C]
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(ABC, A))
|
||||||
|
static_assert(is_subtype_of(ABC, B))
|
||||||
|
static_assert(is_subtype_of(ABC, C))
|
||||||
|
|
||||||
|
class D: ...
|
||||||
|
|
||||||
|
static_assert(not is_subtype_of(ABC, D))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unknown type
|
||||||
|
|
||||||
|
The `Unknown` type is a special type that we use to represent actually unknown types (no
|
||||||
|
annotation), as opposed to `Any` which represents an explicitly unknown type.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import Unknown, static_assert, is_assignable_to, is_fully_static
|
||||||
|
|
||||||
|
static_assert(is_assignable_to(Unknown, int))
|
||||||
|
static_assert(is_assignable_to(int, Unknown))
|
||||||
|
|
||||||
|
static_assert(not is_fully_static(Unknown))
|
||||||
|
|
||||||
|
def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None:
|
||||||
|
reveal_type(x) # revealed: Unknown
|
||||||
|
reveal_type(y) # revealed: tuple[str, Unknown]
|
||||||
|
reveal_type(z) # revealed: Unknown | Literal[1]
|
||||||
|
|
||||||
|
# Unknown can be subclassed, just like Any
|
||||||
|
class C(Unknown): ...
|
||||||
|
|
||||||
|
# revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||||
|
reveal_type(C.__mro__)
|
||||||
|
|
||||||
|
# error: "Special form `knot_extensions.Unknown` expected no type parameter"
|
||||||
|
u: Unknown[str]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Static assertions
|
||||||
|
|
||||||
|
### Basics
|
||||||
|
|
||||||
|
The `knot_extensions` module provides a `static_assert` function that can be used to enforce
|
||||||
|
properties at type-check time. The function takes an arbitrary expression and raises a type error if
|
||||||
|
the expression is not of statically known truthiness.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import static_assert
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
import sys
|
||||||
|
|
||||||
|
static_assert(True)
|
||||||
|
static_assert(False) # error: "Static assertion error: argument evaluates to `False`"
|
||||||
|
|
||||||
|
static_assert(False or True)
|
||||||
|
static_assert(True and True)
|
||||||
|
static_assert(False or False) # error: "Static assertion error: argument evaluates to `False`"
|
||||||
|
static_assert(False and True) # error: "Static assertion error: argument evaluates to `False`"
|
||||||
|
|
||||||
|
static_assert(1 + 1 == 2)
|
||||||
|
static_assert(1 + 1 == 3) # error: "Static assertion error: argument evaluates to `False`"
|
||||||
|
|
||||||
|
static_assert("a" in "abc")
|
||||||
|
static_assert("d" in "abc") # error: "Static assertion error: argument evaluates to `False`"
|
||||||
|
|
||||||
|
n = None
|
||||||
|
static_assert(n is None)
|
||||||
|
|
||||||
|
static_assert(TYPE_CHECKING)
|
||||||
|
|
||||||
|
static_assert(sys.version_info >= (3, 6))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Narrowing constraints
|
||||||
|
|
||||||
|
Static assertions can be used to enforce narrowing constraints:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import static_assert
|
||||||
|
|
||||||
|
def f(x: int) -> None:
|
||||||
|
if x != 0:
|
||||||
|
static_assert(x != 0)
|
||||||
|
else:
|
||||||
|
# `int` can be subclassed, so we cannot assert that `x == 0` here:
|
||||||
|
# error: "Static assertion error: argument of type `bool` has an ambiguous static truthiness"
|
||||||
|
static_assert(x == 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Truthy expressions
|
||||||
|
|
||||||
|
See also: <https://docs.python.org/3/library/stdtypes.html#truth-value-testing>
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import static_assert
|
||||||
|
|
||||||
|
static_assert(True)
|
||||||
|
static_assert(False) # error: "Static assertion error: argument evaluates to `False`"
|
||||||
|
|
||||||
|
static_assert(None) # error: "Static assertion error: argument of type `None` is statically known to be falsy"
|
||||||
|
|
||||||
|
static_assert(1)
|
||||||
|
static_assert(0) # error: "Static assertion error: argument of type `Literal[0]` is statically known to be falsy"
|
||||||
|
|
||||||
|
static_assert((0,))
|
||||||
|
static_assert(()) # error: "Static assertion error: argument of type `tuple[()]` is statically known to be falsy"
|
||||||
|
|
||||||
|
static_assert("a")
|
||||||
|
static_assert("") # error: "Static assertion error: argument of type `Literal[""]` is statically known to be falsy"
|
||||||
|
|
||||||
|
static_assert(b"a")
|
||||||
|
static_assert(b"") # error: "Static assertion error: argument of type `Literal[b""]` is statically known to be falsy"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error messages
|
||||||
|
|
||||||
|
We provide various tailored error messages for wrong argument types to `static_assert`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import static_assert
|
||||||
|
|
||||||
|
static_assert(2 * 3 == 6)
|
||||||
|
|
||||||
|
# error: "Static assertion error: argument evaluates to `False`"
|
||||||
|
static_assert(2 * 3 == 7)
|
||||||
|
|
||||||
|
# error: "Static assertion error: argument of type `bool` has an ambiguous static truthiness"
|
||||||
|
static_assert(int(2.0 * 3.0) == 6)
|
||||||
|
|
||||||
|
class InvalidBoolDunder:
|
||||||
|
def __bool__(self) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# error: "Static assertion error: argument of type `InvalidBoolDunder` has an ambiguous static truthiness"
|
||||||
|
static_assert(InvalidBoolDunder())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom error messages
|
||||||
|
|
||||||
|
Alternatively, users can provide custom error messages:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import static_assert
|
||||||
|
|
||||||
|
# error: "Static assertion error: I really want this to be true"
|
||||||
|
static_assert(1 + 1 == 3, "I really want this to be true")
|
||||||
|
|
||||||
|
error_message = "A custom message "
|
||||||
|
error_message += "constructed from multiple string literals"
|
||||||
|
# error: "Static assertion error: A custom message constructed from multiple string literals"
|
||||||
|
static_assert(False, error_message)
|
||||||
|
|
||||||
|
# There are limitations to what we can still infer as a string literal. In those cases,
|
||||||
|
# we simply fall back to the default message.
|
||||||
|
shouted_message = "A custom message".upper()
|
||||||
|
# error: "Static assertion error: argument evaluates to `False`"
|
||||||
|
static_assert(False, shouted_message)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type predicates
|
||||||
|
|
||||||
|
The `knot_extensions` module also provides predicates to test various properties of types. These are
|
||||||
|
implemented as functions that return `Literal[True]` or `Literal[False]` depending on the result of
|
||||||
|
the test.
|
||||||
|
|
||||||
|
### Equivalence
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import is_equivalent_to, static_assert
|
||||||
|
from typing_extensions import Never, Union
|
||||||
|
|
||||||
|
static_assert(is_equivalent_to(type, type[object]))
|
||||||
|
static_assert(is_equivalent_to(tuple[int, Never], Never))
|
||||||
|
static_assert(is_equivalent_to(int | str, Union[int, str]))
|
||||||
|
|
||||||
|
static_assert(not is_equivalent_to(int, str))
|
||||||
|
static_assert(not is_equivalent_to(int | str, int | str | bytes))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subtyping
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import is_subtype_of, static_assert
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(bool, int))
|
||||||
|
static_assert(not is_subtype_of(str, int))
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(bool, int | str))
|
||||||
|
static_assert(is_subtype_of(str, int | str))
|
||||||
|
static_assert(not is_subtype_of(bytes, int | str))
|
||||||
|
|
||||||
|
class Base: ...
|
||||||
|
class Derived(Base): ...
|
||||||
|
class Unrelated: ...
|
||||||
|
|
||||||
|
static_assert(is_subtype_of(Derived, Base))
|
||||||
|
static_assert(not is_subtype_of(Base, Derived))
|
||||||
|
static_assert(is_subtype_of(Base, Base))
|
||||||
|
|
||||||
|
static_assert(not is_subtype_of(Unrelated, Base))
|
||||||
|
static_assert(not is_subtype_of(Base, Unrelated))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Assignability
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import is_assignable_to, static_assert
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
static_assert(is_assignable_to(int, Any))
|
||||||
|
static_assert(is_assignable_to(Any, str))
|
||||||
|
static_assert(not is_assignable_to(int, str))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disjointness
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import is_disjoint_from, static_assert
|
||||||
|
|
||||||
|
static_assert(is_disjoint_from(None, int))
|
||||||
|
static_assert(not is_disjoint_from(Literal[2] | str, int))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fully static types
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import is_fully_static, static_assert
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
static_assert(is_fully_static(int | str))
|
||||||
|
static_assert(is_fully_static(type[int]))
|
||||||
|
|
||||||
|
static_assert(not is_fully_static(int | Any))
|
||||||
|
static_assert(not is_fully_static(type[Any]))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Singleton types
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import is_singleton, static_assert
|
||||||
|
|
||||||
|
static_assert(is_singleton(None))
|
||||||
|
static_assert(is_singleton(Literal[True]))
|
||||||
|
|
||||||
|
static_assert(not is_singleton(int))
|
||||||
|
static_assert(not is_singleton(Literal["a"]))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Single-valued types
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import is_single_valued, static_assert
|
||||||
|
|
||||||
|
static_assert(is_single_valued(None))
|
||||||
|
static_assert(is_single_valued(Literal[True]))
|
||||||
|
static_assert(is_single_valued(Literal["a"]))
|
||||||
|
|
||||||
|
static_assert(not is_single_valued(int))
|
||||||
|
static_assert(not is_single_valued(Literal["a"] | Literal["b"]))
|
||||||
|
```
|
||||||
|
|
||||||
|
## `TypeOf`
|
||||||
|
|
||||||
|
We use `TypeOf` to get the inferred type of an expression. This is useful when we want to refer to
|
||||||
|
it in a type expression. For example, if we want to make sure that the class literal type `str` is a
|
||||||
|
subtype of `type[str]`, we can not use `is_subtype_of(str, type[str])`, as that would test if the
|
||||||
|
type `str` itself is a subtype of `type[str]`. Instead, we can use `TypeOf[str]` to get the type of
|
||||||
|
the expression `str`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import TypeOf, is_subtype_of, static_assert
|
||||||
|
|
||||||
|
# This is incorrect and therefore fails with ...
|
||||||
|
# error: "Static assertion error: argument evaluates to `False`"
|
||||||
|
static_assert(is_subtype_of(str, type[str]))
|
||||||
|
|
||||||
|
# Correct, returns True:
|
||||||
|
static_assert(is_subtype_of(TypeOf[str], type[str]))
|
||||||
|
|
||||||
|
class Base: ...
|
||||||
|
class Derived(Base): ...
|
||||||
|
|
||||||
|
# `TypeOf` can be used in annotations:
|
||||||
|
def type_of_annotation() -> None:
|
||||||
|
t1: TypeOf[Base] = Base
|
||||||
|
t2: TypeOf[Base] = Derived # error: [invalid-assignment]
|
||||||
|
|
||||||
|
# Note how this is different from `type[…]` which includes subclasses:
|
||||||
|
s1: type[Base] = Base
|
||||||
|
s2: type[Base] = Derived # no error here
|
||||||
|
|
||||||
|
# error: "Special form `knot_extensions.TypeOf` expected exactly one type parameter"
|
||||||
|
t: TypeOf[int, str, bytes]
|
||||||
|
```
|
||||||
|
|
@ -109,6 +109,7 @@ pub enum KnownModule {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
Abc, // currently only used in tests
|
Abc, // currently only used in tests
|
||||||
Collections,
|
Collections,
|
||||||
|
KnotExtensions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KnownModule {
|
impl KnownModule {
|
||||||
|
|
@ -122,6 +123,7 @@ impl KnownModule {
|
||||||
Self::Sys => "sys",
|
Self::Sys => "sys",
|
||||||
Self::Abc => "abc",
|
Self::Abc => "abc",
|
||||||
Self::Collections => "collections",
|
Self::Collections => "collections",
|
||||||
|
Self::KnotExtensions => "knot_extensions",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,6 +149,7 @@ impl KnownModule {
|
||||||
"sys" => Some(Self::Sys),
|
"sys" => Some(Self::Sys),
|
||||||
"abc" => Some(Self::Abc),
|
"abc" => Some(Self::Abc),
|
||||||
"collections" => Some(Self::Collections),
|
"collections" => Some(Self::Collections),
|
||||||
|
"knot_extensions" => Some(Self::KnotExtensions),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,4 +157,8 @@ impl KnownModule {
|
||||||
pub const fn is_typing(self) -> bool {
|
pub const fn is_typing(self) -> bool {
|
||||||
matches!(self, Self::Typing)
|
matches!(self, Self::Typing)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn is_knot_extensions(self) -> bool {
|
||||||
|
matches!(self, Self::KnotExtensions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,11 @@ impl<'db> Definition<'db> {
|
||||||
Some(KnownModule::Typing | KnownModule::TypingExtensions)
|
Some(KnownModule::Typing | KnownModule::TypingExtensions)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_knot_extensions_definition(self, db: &'db dyn Db) -> bool {
|
||||||
|
file_to_module(db, self.file(db))
|
||||||
|
.is_some_and(|module| module.is_known(KnownModule::KnotExtensions))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ use crate::semantic_index::{
|
||||||
use crate::stdlib::{builtins_symbol, known_module_symbol, typing_extensions_symbol};
|
use crate::stdlib::{builtins_symbol, known_module_symbol, typing_extensions_symbol};
|
||||||
use crate::suppression::check_suppressions;
|
use crate::suppression::check_suppressions;
|
||||||
use crate::symbol::{Boundness, Symbol};
|
use crate::symbol::{Boundness, Symbol};
|
||||||
use crate::types::call::{bind_call, CallArguments, CallBinding, CallDunderResult, CallOutcome};
|
use crate::types::call::{
|
||||||
|
bind_call, CallArguments, CallBinding, CallDunderResult, CallOutcome, StaticAssertionErrorKind,
|
||||||
|
};
|
||||||
use crate::types::class_base::ClassBase;
|
use crate::types::class_base::ClassBase;
|
||||||
use crate::types::diagnostic::INVALID_TYPE_FORM;
|
use crate::types::diagnostic::INVALID_TYPE_FORM;
|
||||||
use crate::types::mro::{Mro, MroError, MroIterator};
|
use crate::types::mro::{Mro, MroError, MroIterator};
|
||||||
|
|
@ -657,6 +659,13 @@ impl<'db> Type<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_string_literal(self) -> Option<StringLiteralType<'db>> {
|
||||||
|
match self {
|
||||||
|
Type::StringLiteral(string_literal) => Some(string_literal),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn expect_int_literal(self) -> i64 {
|
pub fn expect_int_literal(self) -> i64 {
|
||||||
self.into_int_literal()
|
self.into_int_literal()
|
||||||
|
|
@ -1824,12 +1833,88 @@ impl<'db> Type<'db> {
|
||||||
let mut binding = bind_call(db, arguments, function_type.signature(db), Some(self));
|
let mut binding = bind_call(db, arguments, function_type.signature(db), Some(self));
|
||||||
match function_type.known(db) {
|
match function_type.known(db) {
|
||||||
Some(KnownFunction::RevealType) => {
|
Some(KnownFunction::RevealType) => {
|
||||||
let revealed_ty = binding.first_parameter().unwrap_or(Type::Unknown);
|
let revealed_ty = binding.one_parameter_ty().unwrap_or(Type::Unknown);
|
||||||
CallOutcome::revealed(binding, revealed_ty)
|
CallOutcome::revealed(binding, revealed_ty)
|
||||||
}
|
}
|
||||||
|
Some(KnownFunction::StaticAssert) => {
|
||||||
|
if let Some((parameter_ty, message)) = binding.two_parameter_tys() {
|
||||||
|
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_tys()
|
||||||
|
.unwrap_or((Type::Unknown, Type::Unknown));
|
||||||
|
binding
|
||||||
|
.set_return_ty(Type::BooleanLiteral(ty_a.is_equivalent_to(db, ty_b)));
|
||||||
|
CallOutcome::callable(binding)
|
||||||
|
}
|
||||||
|
Some(KnownFunction::IsSubtypeOf) => {
|
||||||
|
let (ty_a, ty_b) = binding
|
||||||
|
.two_parameter_tys()
|
||||||
|
.unwrap_or((Type::Unknown, Type::Unknown));
|
||||||
|
binding.set_return_ty(Type::BooleanLiteral(ty_a.is_subtype_of(db, ty_b)));
|
||||||
|
CallOutcome::callable(binding)
|
||||||
|
}
|
||||||
|
Some(KnownFunction::IsAssignableTo) => {
|
||||||
|
let (ty_a, ty_b) = binding
|
||||||
|
.two_parameter_tys()
|
||||||
|
.unwrap_or((Type::Unknown, Type::Unknown));
|
||||||
|
binding
|
||||||
|
.set_return_ty(Type::BooleanLiteral(ty_a.is_assignable_to(db, ty_b)));
|
||||||
|
CallOutcome::callable(binding)
|
||||||
|
}
|
||||||
|
Some(KnownFunction::IsDisjointFrom) => {
|
||||||
|
let (ty_a, ty_b) = binding
|
||||||
|
.two_parameter_tys()
|
||||||
|
.unwrap_or((Type::Unknown, Type::Unknown));
|
||||||
|
binding
|
||||||
|
.set_return_ty(Type::BooleanLiteral(ty_a.is_disjoint_from(db, ty_b)));
|
||||||
|
CallOutcome::callable(binding)
|
||||||
|
}
|
||||||
|
Some(KnownFunction::IsFullyStatic) => {
|
||||||
|
let ty = binding.one_parameter_ty().unwrap_or(Type::Unknown);
|
||||||
|
binding.set_return_ty(Type::BooleanLiteral(ty.is_fully_static(db)));
|
||||||
|
CallOutcome::callable(binding)
|
||||||
|
}
|
||||||
|
Some(KnownFunction::IsSingleton) => {
|
||||||
|
let ty = binding.one_parameter_ty().unwrap_or(Type::Unknown);
|
||||||
|
binding.set_return_ty(Type::BooleanLiteral(ty.is_singleton(db)));
|
||||||
|
CallOutcome::callable(binding)
|
||||||
|
}
|
||||||
|
Some(KnownFunction::IsSingleValued) => {
|
||||||
|
let ty = binding.one_parameter_ty().unwrap_or(Type::Unknown);
|
||||||
|
binding.set_return_ty(Type::BooleanLiteral(ty.is_single_valued(db)));
|
||||||
|
CallOutcome::callable(binding)
|
||||||
|
}
|
||||||
|
|
||||||
Some(KnownFunction::Len) => {
|
Some(KnownFunction::Len) => {
|
||||||
if let Some(first_arg) = binding.first_parameter() {
|
if let Some(first_arg) = binding.one_parameter_ty() {
|
||||||
if let Some(len_ty) = first_arg.len(db) {
|
if let Some(len_ty) = first_arg.len(db) {
|
||||||
binding.set_return_ty(len_ty);
|
binding.set_return_ty(len_ty);
|
||||||
}
|
}
|
||||||
|
|
@ -2107,6 +2192,7 @@ impl<'db> Type<'db> {
|
||||||
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::BareLiteral],
|
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::BareLiteral],
|
||||||
fallback_type: Type::Unknown,
|
fallback_type: Type::Unknown,
|
||||||
}),
|
}),
|
||||||
|
Type::KnownInstance(KnownInstanceType::Unknown) => Ok(Type::Unknown),
|
||||||
Type::Todo(_) => Ok(*self),
|
Type::Todo(_) => Ok(*self),
|
||||||
_ => Ok(todo_type!(
|
_ => Ok(todo_type!(
|
||||||
"Unsupported or invalid type in a type expression"
|
"Unsupported or invalid type in a type expression"
|
||||||
|
|
@ -2613,6 +2699,14 @@ pub enum KnownInstanceType<'db> {
|
||||||
TypeVar(TypeVarInstance<'db>),
|
TypeVar(TypeVarInstance<'db>),
|
||||||
/// A single instance of `typing.TypeAliasType` (PEP 695 type alias)
|
/// A single instance of `typing.TypeAliasType` (PEP 695 type alias)
|
||||||
TypeAliasType(TypeAliasType<'db>),
|
TypeAliasType(TypeAliasType<'db>),
|
||||||
|
/// The symbol `knot_extensions.Unknown`
|
||||||
|
Unknown,
|
||||||
|
/// The symbol `knot_extensions.Not`
|
||||||
|
Not,
|
||||||
|
/// The symbol `knot_extensions.Intersection`
|
||||||
|
Intersection,
|
||||||
|
/// The symbol `knot_extensions.TypeOf`
|
||||||
|
TypeOf,
|
||||||
|
|
||||||
// Various special forms, special aliases and type qualifiers that we don't yet understand
|
// Various special forms, special aliases and type qualifiers that we don't yet understand
|
||||||
// (all currently inferred as TODO in most contexts):
|
// (all currently inferred as TODO in most contexts):
|
||||||
|
|
@ -2667,6 +2761,10 @@ impl<'db> KnownInstanceType<'db> {
|
||||||
Self::ChainMap => "ChainMap",
|
Self::ChainMap => "ChainMap",
|
||||||
Self::OrderedDict => "OrderedDict",
|
Self::OrderedDict => "OrderedDict",
|
||||||
Self::ReadOnly => "ReadOnly",
|
Self::ReadOnly => "ReadOnly",
|
||||||
|
Self::Unknown => "Unknown",
|
||||||
|
Self::Not => "Not",
|
||||||
|
Self::Intersection => "Intersection",
|
||||||
|
Self::TypeOf => "TypeOf",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2705,7 +2803,11 @@ impl<'db> KnownInstanceType<'db> {
|
||||||
| Self::ChainMap
|
| Self::ChainMap
|
||||||
| Self::OrderedDict
|
| Self::OrderedDict
|
||||||
| Self::ReadOnly
|
| Self::ReadOnly
|
||||||
| Self::TypeAliasType(_) => Truthiness::AlwaysTrue,
|
| Self::TypeAliasType(_)
|
||||||
|
| Self::Unknown
|
||||||
|
| Self::Not
|
||||||
|
| Self::Intersection
|
||||||
|
| Self::TypeOf => Truthiness::AlwaysTrue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2745,6 +2847,10 @@ impl<'db> KnownInstanceType<'db> {
|
||||||
Self::ReadOnly => "typing.ReadOnly",
|
Self::ReadOnly => "typing.ReadOnly",
|
||||||
Self::TypeVar(typevar) => typevar.name(db),
|
Self::TypeVar(typevar) => typevar.name(db),
|
||||||
Self::TypeAliasType(_) => "typing.TypeAliasType",
|
Self::TypeAliasType(_) => "typing.TypeAliasType",
|
||||||
|
Self::Unknown => "knot_extensions.Unknown",
|
||||||
|
Self::Not => "knot_extensions.Not",
|
||||||
|
Self::Intersection => "knot_extensions.Intersection",
|
||||||
|
Self::TypeOf => "knot_extensions.TypeOf",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2784,6 +2890,10 @@ impl<'db> KnownInstanceType<'db> {
|
||||||
Self::OrderedDict => KnownClass::StdlibAlias,
|
Self::OrderedDict => KnownClass::StdlibAlias,
|
||||||
Self::TypeVar(_) => KnownClass::TypeVar,
|
Self::TypeVar(_) => KnownClass::TypeVar,
|
||||||
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
|
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
|
||||||
|
Self::TypeOf => KnownClass::SpecialForm,
|
||||||
|
Self::Not => KnownClass::SpecialForm,
|
||||||
|
Self::Intersection => KnownClass::SpecialForm,
|
||||||
|
Self::Unknown => KnownClass::Object,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2834,6 +2944,10 @@ impl<'db> KnownInstanceType<'db> {
|
||||||
"Concatenate" => Self::Concatenate,
|
"Concatenate" => Self::Concatenate,
|
||||||
"NotRequired" => Self::NotRequired,
|
"NotRequired" => Self::NotRequired,
|
||||||
"LiteralString" => Self::LiteralString,
|
"LiteralString" => Self::LiteralString,
|
||||||
|
"Unknown" => Self::Unknown,
|
||||||
|
"Not" => Self::Not,
|
||||||
|
"Intersection" => Self::Intersection,
|
||||||
|
"TypeOf" => Self::TypeOf,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -2883,6 +2997,9 @@ impl<'db> KnownInstanceType<'db> {
|
||||||
| Self::TypeVar(_) => {
|
| Self::TypeVar(_) => {
|
||||||
matches!(module, KnownModule::Typing | KnownModule::TypingExtensions)
|
matches!(module, KnownModule::Typing | KnownModule::TypingExtensions)
|
||||||
}
|
}
|
||||||
|
Self::Unknown | Self::Not | Self::Intersection | Self::TypeOf => {
|
||||||
|
module.is_knot_extensions()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3121,13 +3238,41 @@ pub enum KnownFunction {
|
||||||
|
|
||||||
/// [`typing(_extensions).no_type_check`](https://typing.readthedocs.io/en/latest/spec/directives.html#no-type-check)
|
/// [`typing(_extensions).no_type_check`](https://typing.readthedocs.io/en/latest/spec/directives.html#no-type-check)
|
||||||
NoTypeCheck,
|
NoTypeCheck,
|
||||||
|
|
||||||
|
/// `knot_extensions.static_assert`
|
||||||
|
StaticAssert,
|
||||||
|
/// `knot_extensions.is_equivalent_to`
|
||||||
|
IsEquivalentTo,
|
||||||
|
/// `knot_extensions.is_subtype_of`
|
||||||
|
IsSubtypeOf,
|
||||||
|
/// `knot_extensions.is_assignable_to`
|
||||||
|
IsAssignableTo,
|
||||||
|
/// `knot_extensions.is_disjoint_from`
|
||||||
|
IsDisjointFrom,
|
||||||
|
/// `knot_extensions.is_fully_static`
|
||||||
|
IsFullyStatic,
|
||||||
|
/// `knot_extensions.is_singleton`
|
||||||
|
IsSingleton,
|
||||||
|
/// `knot_extensions.is_single_valued`
|
||||||
|
IsSingleValued,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KnownFunction {
|
impl KnownFunction {
|
||||||
pub fn constraint_function(self) -> Option<KnownConstraintFunction> {
|
pub fn constraint_function(self) -> Option<KnownConstraintFunction> {
|
||||||
match self {
|
match self {
|
||||||
Self::ConstraintFunction(f) => Some(f),
|
Self::ConstraintFunction(f) => Some(f),
|
||||||
Self::RevealType | Self::Len | Self::Final | Self::NoTypeCheck => None,
|
Self::RevealType
|
||||||
|
| Self::Len
|
||||||
|
| Self::Final
|
||||||
|
| Self::NoTypeCheck
|
||||||
|
| Self::StaticAssert
|
||||||
|
| Self::IsEquivalentTo
|
||||||
|
| Self::IsSubtypeOf
|
||||||
|
| Self::IsAssignableTo
|
||||||
|
| Self::IsDisjointFrom
|
||||||
|
| Self::IsFullyStatic
|
||||||
|
| Self::IsSingleton
|
||||||
|
| Self::IsSingleValued => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3149,9 +3294,50 @@ impl KnownFunction {
|
||||||
"no_type_check" if definition.is_typing_definition(db) => {
|
"no_type_check" if definition.is_typing_definition(db) => {
|
||||||
Some(KnownFunction::NoTypeCheck)
|
Some(KnownFunction::NoTypeCheck)
|
||||||
}
|
}
|
||||||
|
"static_assert" if definition.is_knot_extensions_definition(db) => {
|
||||||
|
Some(KnownFunction::StaticAssert)
|
||||||
|
}
|
||||||
|
"is_subtype_of" if definition.is_knot_extensions_definition(db) => {
|
||||||
|
Some(KnownFunction::IsSubtypeOf)
|
||||||
|
}
|
||||||
|
"is_disjoint_from" if definition.is_knot_extensions_definition(db) => {
|
||||||
|
Some(KnownFunction::IsDisjointFrom)
|
||||||
|
}
|
||||||
|
"is_equivalent_to" if definition.is_knot_extensions_definition(db) => {
|
||||||
|
Some(KnownFunction::IsEquivalentTo)
|
||||||
|
}
|
||||||
|
"is_assignable_to" if definition.is_knot_extensions_definition(db) => {
|
||||||
|
Some(KnownFunction::IsAssignableTo)
|
||||||
|
}
|
||||||
|
"is_fully_static" if definition.is_knot_extensions_definition(db) => {
|
||||||
|
Some(KnownFunction::IsFullyStatic)
|
||||||
|
}
|
||||||
|
"is_singleton" if definition.is_knot_extensions_definition(db) => {
|
||||||
|
Some(KnownFunction::IsSingleton)
|
||||||
|
}
|
||||||
|
"is_single_valued" if definition.is_knot_extensions_definition(db) => {
|
||||||
|
Some(KnownFunction::IsSingleValued)
|
||||||
|
}
|
||||||
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether or not a particular function takes type expression as arguments, i.e. should
|
||||||
|
/// the argument of a call like `f(int)` be interpreted as the type int (true) or as the
|
||||||
|
/// type of the expression `int`, i.e. `Literal[int]` (false).
|
||||||
|
const fn takes_type_expression_arguments(self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
KnownFunction::IsEquivalentTo
|
||||||
|
| KnownFunction::IsSubtypeOf
|
||||||
|
| KnownFunction::IsAssignableTo
|
||||||
|
| KnownFunction::IsDisjointFrom
|
||||||
|
| KnownFunction::IsFullyStatic
|
||||||
|
| KnownFunction::IsSingleton
|
||||||
|
| KnownFunction::IsSingleValued
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[salsa::interned]
|
#[salsa::interned]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use super::context::InferContext;
|
use super::context::InferContext;
|
||||||
use super::diagnostic::CALL_NON_CALLABLE;
|
use super::diagnostic::CALL_NON_CALLABLE;
|
||||||
use super::{Severity, Signature, Type, TypeArrayDisplay, UnionBuilder};
|
use super::{Severity, Signature, Type, TypeArrayDisplay, UnionBuilder};
|
||||||
|
use crate::types::diagnostic::STATIC_ASSERT_ERROR;
|
||||||
use crate::Db;
|
use crate::Db;
|
||||||
use ruff_db::diagnostic::DiagnosticId;
|
use ruff_db::diagnostic::DiagnosticId;
|
||||||
use ruff_python_ast as ast;
|
use ruff_python_ast as ast;
|
||||||
|
|
@ -11,6 +12,14 @@ mod bind;
|
||||||
pub(super) use arguments::{Argument, CallArguments};
|
pub(super) use arguments::{Argument, CallArguments};
|
||||||
pub(super) use bind::{bind_call, CallBinding};
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub(super) enum CallOutcome<'db> {
|
pub(super) enum CallOutcome<'db> {
|
||||||
Callable {
|
Callable {
|
||||||
|
|
@ -31,6 +40,10 @@ pub(super) enum CallOutcome<'db> {
|
||||||
called_ty: Type<'db>,
|
called_ty: Type<'db>,
|
||||||
call_outcome: Box<CallOutcome<'db>>,
|
call_outcome: Box<CallOutcome<'db>>,
|
||||||
},
|
},
|
||||||
|
StaticAssertionError {
|
||||||
|
binding: CallBinding<'db>,
|
||||||
|
error_kind: StaticAssertionErrorKind<'db>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'db> CallOutcome<'db> {
|
impl<'db> CallOutcome<'db> {
|
||||||
|
|
@ -89,6 +102,7 @@ impl<'db> CallOutcome<'db> {
|
||||||
})
|
})
|
||||||
.map(UnionBuilder::build),
|
.map(UnionBuilder::build),
|
||||||
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_ty(db),
|
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_ty(db),
|
||||||
|
Self::StaticAssertionError { .. } => Some(Type::none(db)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,6 +195,7 @@ impl<'db> CallOutcome<'db> {
|
||||||
binding,
|
binding,
|
||||||
revealed_ty,
|
revealed_ty,
|
||||||
} => {
|
} => {
|
||||||
|
binding.report_diagnostics(context, node);
|
||||||
context.report_diagnostic(
|
context.report_diagnostic(
|
||||||
node,
|
node,
|
||||||
DiagnosticId::RevealedType,
|
DiagnosticId::RevealedType,
|
||||||
|
|
@ -249,6 +264,51 @@ impl<'db> CallOutcome<'db> {
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CallOutcome::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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,8 +154,18 @@ impl<'db> CallBinding<'db> {
|
||||||
&self.parameter_tys
|
&self.parameter_tys
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn first_parameter(&self) -> Option<Type<'db>> {
|
pub(crate) fn one_parameter_ty(&self) -> Option<Type<'db>> {
|
||||||
self.parameter_tys().first().copied()
|
match self.parameter_tys() {
|
||||||
|
[ty] => Some(*ty),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn two_parameter_tys(&self) -> Option<(Type<'db>, Type<'db>)> {
|
||||||
|
match self.parameter_tys() {
|
||||||
|
[first, second] => Some((*first, *second)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn callable_name(&self, db: &'db dyn Db) -> Option<&ast::name::Name> {
|
fn callable_name(&self, db: &'db dyn Db) -> Option<&ast::name::Name> {
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,11 @@ impl<'db> ClassBase<'db> {
|
||||||
| KnownInstanceType::Required
|
| KnownInstanceType::Required
|
||||||
| KnownInstanceType::TypeAlias
|
| KnownInstanceType::TypeAlias
|
||||||
| KnownInstanceType::ReadOnly
|
| KnownInstanceType::ReadOnly
|
||||||
| KnownInstanceType::Optional => None,
|
| KnownInstanceType::Optional
|
||||||
|
| KnownInstanceType::Not
|
||||||
|
| KnownInstanceType::Intersection
|
||||||
|
| KnownInstanceType::TypeOf => None,
|
||||||
|
KnownInstanceType::Unknown => Some(Self::Unknown),
|
||||||
KnownInstanceType::Any => Some(Self::Any),
|
KnownInstanceType::Any => Some(Self::Any),
|
||||||
// TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO
|
// TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO
|
||||||
KnownInstanceType::Dict => {
|
KnownInstanceType::Dict => {
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||||
registry.register_lint(&UNRESOLVED_REFERENCE);
|
registry.register_lint(&UNRESOLVED_REFERENCE);
|
||||||
registry.register_lint(&UNSUPPORTED_OPERATOR);
|
registry.register_lint(&UNSUPPORTED_OPERATOR);
|
||||||
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
|
registry.register_lint(&ZERO_STEPSIZE_IN_SLICE);
|
||||||
|
registry.register_lint(&STATIC_ASSERT_ERROR);
|
||||||
|
|
||||||
// String annotations
|
// String annotations
|
||||||
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
|
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
|
||||||
|
|
@ -678,6 +679,25 @@ declare_lint! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare_lint! {
|
||||||
|
/// ## What it does
|
||||||
|
/// Makes sure that the argument of `static_assert` is statically known to be true.
|
||||||
|
///
|
||||||
|
/// ## Examples
|
||||||
|
/// ```python
|
||||||
|
/// from knot_extensions import static_assert
|
||||||
|
///
|
||||||
|
/// static_assert(1 + 1 == 3) # error: evaluates to `False`
|
||||||
|
///
|
||||||
|
/// static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known truthiness
|
||||||
|
/// ```
|
||||||
|
pub(crate) static STATIC_ASSERT_ERROR = {
|
||||||
|
summary: "Failed static assertion",
|
||||||
|
status: LintStatus::preview("1.0.0"),
|
||||||
|
default_level: Level::Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||||
pub struct TypeCheckDiagnostic {
|
pub struct TypeCheckDiagnostic {
|
||||||
pub(crate) id: DiagnosticId,
|
pub(crate) id: DiagnosticId,
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
//! definitions once the rest of the types in the scope have been inferred.
|
//! definitions once the rest of the types in the scope have been inferred.
|
||||||
use std::num::NonZeroU32;
|
use std::num::NonZeroU32;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::{Either, Itertools};
|
||||||
use ruff_db::files::File;
|
use ruff_db::files::File;
|
||||||
use ruff_db::parsed::parsed_module;
|
use ruff_db::parsed::parsed_module;
|
||||||
use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext};
|
use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext};
|
||||||
|
|
@ -919,7 +919,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
self.infer_type_parameters(type_params);
|
self.infer_type_parameters(type_params);
|
||||||
|
|
||||||
if let Some(arguments) = class.arguments.as_deref() {
|
if let Some(arguments) = class.arguments.as_deref() {
|
||||||
self.infer_arguments(arguments);
|
self.infer_arguments(arguments, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2523,7 +2523,17 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
self.infer_expression(expression)
|
self.infer_expression(expression)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn infer_arguments(&mut self, arguments: &ast::Arguments) -> CallArguments<'db> {
|
fn infer_arguments(
|
||||||
|
&mut self,
|
||||||
|
arguments: &ast::Arguments,
|
||||||
|
infer_as_type_expressions: bool,
|
||||||
|
) -> CallArguments<'db> {
|
||||||
|
let infer_argument_type = if infer_as_type_expressions {
|
||||||
|
Self::infer_type_expression
|
||||||
|
} else {
|
||||||
|
Self::infer_expression
|
||||||
|
};
|
||||||
|
|
||||||
arguments
|
arguments
|
||||||
.arguments_source_order()
|
.arguments_source_order()
|
||||||
.map(|arg_or_keyword| {
|
.map(|arg_or_keyword| {
|
||||||
|
|
@ -2534,19 +2544,19 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
range: _,
|
range: _,
|
||||||
ctx: _,
|
ctx: _,
|
||||||
}) => {
|
}) => {
|
||||||
let ty = self.infer_expression(value);
|
let ty = infer_argument_type(self, value);
|
||||||
self.store_expression_type(arg, ty);
|
self.store_expression_type(arg, ty);
|
||||||
Argument::Variadic(ty)
|
Argument::Variadic(ty)
|
||||||
}
|
}
|
||||||
// TODO diagnostic if after a keyword argument
|
// TODO diagnostic if after a keyword argument
|
||||||
_ => Argument::Positional(self.infer_expression(arg)),
|
_ => Argument::Positional(infer_argument_type(self, arg)),
|
||||||
},
|
},
|
||||||
ast::ArgOrKeyword::Keyword(ast::Keyword {
|
ast::ArgOrKeyword::Keyword(ast::Keyword {
|
||||||
arg,
|
arg,
|
||||||
value,
|
value,
|
||||||
range: _,
|
range: _,
|
||||||
}) => {
|
}) => {
|
||||||
let ty = self.infer_expression(value);
|
let ty = infer_argument_type(self, value);
|
||||||
if let Some(arg) = arg {
|
if let Some(arg) = arg {
|
||||||
Argument::Keyword {
|
Argument::Keyword {
|
||||||
name: arg.id.clone(),
|
name: arg.id.clone(),
|
||||||
|
|
@ -3070,8 +3080,14 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
arguments,
|
arguments,
|
||||||
} = call_expression;
|
} = call_expression;
|
||||||
|
|
||||||
let call_arguments = self.infer_arguments(arguments);
|
|
||||||
let function_type = self.infer_expression(func);
|
let function_type = self.infer_expression(func);
|
||||||
|
|
||||||
|
let infer_arguments_as_type_expressions = function_type
|
||||||
|
.into_function_literal()
|
||||||
|
.and_then(|f| f.known(self.db()))
|
||||||
|
.is_some_and(KnownFunction::takes_type_expression_arguments);
|
||||||
|
|
||||||
|
let call_arguments = self.infer_arguments(arguments, infer_arguments_as_type_expressions);
|
||||||
function_type
|
function_type
|
||||||
.call(self.db(), &call_arguments)
|
.call(self.db(), &call_arguments)
|
||||||
.unwrap_with_diagnostic(&self.context, call_expression.into())
|
.unwrap_with_diagnostic(&self.context, call_expression.into())
|
||||||
|
|
@ -5156,6 +5172,55 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
todo_type!("Callable types")
|
todo_type!("Callable types")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Type API special forms
|
||||||
|
KnownInstanceType::Not => match arguments_slice {
|
||||||
|
ast::Expr::Tuple(_) => {
|
||||||
|
self.context.report_lint(
|
||||||
|
&INVALID_TYPE_FORM,
|
||||||
|
subscript.into(),
|
||||||
|
format_args!(
|
||||||
|
"Special form `{}` expected exactly one type parameter",
|
||||||
|
known_instance.repr(self.db())
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Type::Unknown
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let argument_type = self.infer_type_expression(arguments_slice);
|
||||||
|
argument_type.negate(self.db())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
KnownInstanceType::Intersection => {
|
||||||
|
let elements = match arguments_slice {
|
||||||
|
ast::Expr::Tuple(tuple) => Either::Left(tuple.iter()),
|
||||||
|
element => Either::Right(std::iter::once(element)),
|
||||||
|
};
|
||||||
|
|
||||||
|
elements
|
||||||
|
.fold(IntersectionBuilder::new(self.db()), |builder, element| {
|
||||||
|
builder.add_positive(self.infer_type_expression(element))
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
KnownInstanceType::TypeOf => match arguments_slice {
|
||||||
|
ast::Expr::Tuple(_) => {
|
||||||
|
self.context.report_lint(
|
||||||
|
&INVALID_TYPE_FORM,
|
||||||
|
subscript.into(),
|
||||||
|
format_args!(
|
||||||
|
"Special form `{}` expected exactly one type parameter",
|
||||||
|
known_instance.repr(self.db())
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Type::Unknown
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// NB: This calls `infer_expression` instead of `infer_type_expression`.
|
||||||
|
let argument_type = self.infer_expression(arguments_slice);
|
||||||
|
argument_type
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// TODO: Generics
|
// TODO: Generics
|
||||||
KnownInstanceType::ChainMap => {
|
KnownInstanceType::ChainMap => {
|
||||||
self.infer_type_expression(arguments_slice);
|
self.infer_type_expression(arguments_slice);
|
||||||
|
|
@ -5241,7 +5306,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
);
|
);
|
||||||
Type::Unknown
|
Type::Unknown
|
||||||
}
|
}
|
||||||
KnownInstanceType::TypingSelf | KnownInstanceType::TypeAlias => {
|
KnownInstanceType::TypingSelf
|
||||||
|
| KnownInstanceType::TypeAlias
|
||||||
|
| KnownInstanceType::Unknown => {
|
||||||
self.context.report_lint(
|
self.context.report_lint(
|
||||||
&INVALID_TYPE_FORM,
|
&INVALID_TYPE_FORM,
|
||||||
subscript.into(),
|
subscript.into(),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
The `knot_extensions.pyi` file in this directory will be symlinked into
|
||||||
|
the `vendor/typeshed/stdlib` directory every time we sync our `typeshed`
|
||||||
|
stubs (see `.github/workflows/sync_typeshed.yaml`).
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from typing import _SpecialForm, Any, LiteralString
|
||||||
|
|
||||||
|
# Special operations
|
||||||
|
def static_assert(condition: object, msg: LiteralString | None = None) -> None: ...
|
||||||
|
|
||||||
|
# Types
|
||||||
|
Unknown = object()
|
||||||
|
|
||||||
|
# Special forms
|
||||||
|
Not: _SpecialForm
|
||||||
|
Intersection: _SpecialForm
|
||||||
|
TypeOf: _SpecialForm
|
||||||
|
|
||||||
|
# Predicates on types
|
||||||
|
#
|
||||||
|
# Ideally, these would be annotated using `TypeForm`, but that has not been
|
||||||
|
# standardized yet (https://peps.python.org/pep-0747).
|
||||||
|
def is_equivalent_to(type_a: Any, type_b: Any) -> bool: ...
|
||||||
|
def is_subtype_of(type_derived: Any, typ_ebase: Any) -> bool: ...
|
||||||
|
def is_assignable_to(type_target: Any, type_source: Any) -> bool: ...
|
||||||
|
def is_disjoint_from(type_a: Any, type_b: Any) -> bool: ...
|
||||||
|
def is_fully_static(type: Any) -> bool: ...
|
||||||
|
def is_singleton(type: Any) -> bool: ...
|
||||||
|
def is_single_valued(type: Any) -> bool: ...
|
||||||
|
|
@ -341,3 +341,5 @@ zipfile._path: 3.12-
|
||||||
zipimport: 3.0-
|
zipimport: 3.0-
|
||||||
zlib: 3.0-
|
zlib: 3.0-
|
||||||
zoneinfo: 3.9-
|
zoneinfo: 3.9-
|
||||||
|
# Patch applied for red_knot:
|
||||||
|
knot_extensions: 3.0-
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../../../knot_extensions/knot_extensions.pyi
|
||||||
Loading…
Reference in New Issue