diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index d1785034e1ca4..20925a523e370 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -46,6 +46,10 @@ jobs: cp -r typeshed/stdlib ruff/crates/red_knot_vendored/vendor/typeshed/stdlib 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 + # 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 id: commit if: ${{ steps.sync.outcome == 'success' }} 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 435c9bbe21e33..b68aaf6675de9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/function.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/function.md @@ -257,3 +257,67 @@ def f(x: int) -> int: # error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`" 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) +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_api.md b/crates/red_knot_python_semantic/resources/mdtest/type_api.md new file mode 100644 index 0000000000000..c4e7aa1a1685a --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/type_api.md @@ -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: + +```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] +``` diff --git a/crates/red_knot_python_semantic/src/module_resolver/module.rs b/crates/red_knot_python_semantic/src/module_resolver/module.rs index 140153674db92..7040ce8edc3cf 100644 --- a/crates/red_knot_python_semantic/src/module_resolver/module.rs +++ b/crates/red_knot_python_semantic/src/module_resolver/module.rs @@ -109,6 +109,7 @@ pub enum KnownModule { #[allow(dead_code)] Abc, // currently only used in tests Collections, + KnotExtensions, } impl KnownModule { @@ -122,6 +123,7 @@ impl KnownModule { Self::Sys => "sys", Self::Abc => "abc", Self::Collections => "collections", + Self::KnotExtensions => "knot_extensions", } } @@ -147,6 +149,7 @@ impl KnownModule { "sys" => Some(Self::Sys), "abc" => Some(Self::Abc), "collections" => Some(Self::Collections), + "knot_extensions" => Some(Self::KnotExtensions), _ => None, } } @@ -154,4 +157,8 @@ impl KnownModule { pub const fn is_typing(self) -> bool { matches!(self, Self::Typing) } + + pub const fn is_knot_extensions(self) -> bool { + matches!(self, Self::KnotExtensions) + } } diff --git a/crates/red_knot_python_semantic/src/semantic_index/definition.rs b/crates/red_knot_python_semantic/src/semantic_index/definition.rs index fc75d252dafef..d8d60d04873bd 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/definition.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/definition.rs @@ -74,6 +74,11 @@ impl<'db> Definition<'db> { 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)] diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 4597563b278e6..84b1e2ef025cb 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -31,7 +31,9 @@ use crate::semantic_index::{ use crate::stdlib::{builtins_symbol, known_module_symbol, typing_extensions_symbol}; use crate::suppression::check_suppressions; 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::diagnostic::INVALID_TYPE_FORM; use crate::types::mro::{Mro, MroError, MroIterator}; @@ -657,6 +659,13 @@ impl<'db> Type<'db> { } } + pub fn into_string_literal(self) -> Option> { + match self { + Type::StringLiteral(string_literal) => Some(string_literal), + _ => None, + } + } + #[track_caller] pub fn expect_int_literal(self) -> i64 { 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)); match function_type.known(db) { 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) } + 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) => { - 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) { binding.set_return_ty(len_ty); } @@ -2107,6 +2192,7 @@ impl<'db> Type<'db> { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::BareLiteral], fallback_type: Type::Unknown, }), + Type::KnownInstance(KnownInstanceType::Unknown) => Ok(Type::Unknown), Type::Todo(_) => Ok(*self), _ => Ok(todo_type!( "Unsupported or invalid type in a type expression" @@ -2613,6 +2699,14 @@ pub enum KnownInstanceType<'db> { TypeVar(TypeVarInstance<'db>), /// A single instance of `typing.TypeAliasType` (PEP 695 type alias) 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 // (all currently inferred as TODO in most contexts): @@ -2667,6 +2761,10 @@ impl<'db> KnownInstanceType<'db> { Self::ChainMap => "ChainMap", Self::OrderedDict => "OrderedDict", 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::OrderedDict | 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::TypeVar(typevar) => typevar.name(db), 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::TypeVar(_) => KnownClass::TypeVar, 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, "NotRequired" => Self::NotRequired, "LiteralString" => Self::LiteralString, + "Unknown" => Self::Unknown, + "Not" => Self::Not, + "Intersection" => Self::Intersection, + "TypeOf" => Self::TypeOf, _ => return None, }; @@ -2883,6 +2997,9 @@ impl<'db> KnownInstanceType<'db> { | Self::TypeVar(_) => { 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) 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 { pub fn constraint_function(self) -> Option { match self { 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) => { 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, } } + + /// 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] diff --git a/crates/red_knot_python_semantic/src/types/call.rs b/crates/red_knot_python_semantic/src/types/call.rs index d803b6103d016..951fdb7858442 100644 --- a/crates/red_knot_python_semantic/src/types/call.rs +++ b/crates/red_knot_python_semantic/src/types/call.rs @@ -1,6 +1,7 @@ use super::context::InferContext; use super::diagnostic::CALL_NON_CALLABLE; use super::{Severity, Signature, Type, TypeArrayDisplay, UnionBuilder}; +use crate::types::diagnostic::STATIC_ASSERT_ERROR; use crate::Db; use ruff_db::diagnostic::DiagnosticId; use ruff_python_ast as ast; @@ -11,6 +12,14 @@ 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), +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(super) enum CallOutcome<'db> { Callable { @@ -31,6 +40,10 @@ pub(super) enum CallOutcome<'db> { called_ty: Type<'db>, call_outcome: Box>, }, + StaticAssertionError { + binding: CallBinding<'db>, + error_kind: StaticAssertionErrorKind<'db>, + }, } impl<'db> CallOutcome<'db> { @@ -89,6 +102,7 @@ impl<'db> CallOutcome<'db> { }) .map(UnionBuilder::build), Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_ty(db), + Self::StaticAssertionError { .. } => Some(Type::none(db)), } } @@ -181,6 +195,7 @@ impl<'db> CallOutcome<'db> { binding, revealed_ty, } => { + binding.report_diagnostics(context, node); context.report_diagnostic( node, 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) + } } } } 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 0fcb40140efcc..7e699d1f5d094 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -154,8 +154,18 @@ impl<'db> CallBinding<'db> { &self.parameter_tys } - pub(crate) fn first_parameter(&self) -> Option> { - self.parameter_tys().first().copied() + pub(crate) fn one_parameter_ty(&self) -> Option> { + 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> { diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index f08e4de444ad1..7a441d3f9e263 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -100,7 +100,11 @@ impl<'db> ClassBase<'db> { | KnownInstanceType::Required | KnownInstanceType::TypeAlias | KnownInstanceType::ReadOnly - | KnownInstanceType::Optional => None, + | KnownInstanceType::Optional + | KnownInstanceType::Not + | KnownInstanceType::Intersection + | KnownInstanceType::TypeOf => None, + KnownInstanceType::Unknown => Some(Self::Unknown), KnownInstanceType::Any => Some(Self::Any), // TODO: Classes inheriting from `typing.Type` et al. also have `Generic` in their MRO KnownInstanceType::Dict => { diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index c2468e7847055..7fe097ce4eadd 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -56,6 +56,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) { registry.register_lint(&UNRESOLVED_REFERENCE); registry.register_lint(&UNSUPPORTED_OPERATOR); registry.register_lint(&ZERO_STEPSIZE_IN_SLICE); + registry.register_lint(&STATIC_ASSERT_ERROR); // String annotations 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)] pub struct TypeCheckDiagnostic { pub(crate) id: DiagnosticId, diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 3eb4f72c1de9b..e17041e84f3e9 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -28,7 +28,7 @@ //! definitions once the rest of the types in the scope have been inferred. use std::num::NonZeroU32; -use itertools::Itertools; +use itertools::{Either, Itertools}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext}; @@ -919,7 +919,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_type_parameters(type_params); 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) } - 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_source_order() .map(|arg_or_keyword| { @@ -2534,19 +2544,19 @@ impl<'db> TypeInferenceBuilder<'db> { range: _, ctx: _, }) => { - let ty = self.infer_expression(value); + let ty = infer_argument_type(self, value); self.store_expression_type(arg, ty); Argument::Variadic(ty) } // 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 { arg, value, range: _, }) => { - let ty = self.infer_expression(value); + let ty = infer_argument_type(self, value); if let Some(arg) = arg { Argument::Keyword { name: arg.id.clone(), @@ -3070,8 +3080,14 @@ impl<'db> TypeInferenceBuilder<'db> { arguments, } = call_expression; - let call_arguments = self.infer_arguments(arguments); 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 .call(self.db(), &call_arguments) .unwrap_with_diagnostic(&self.context, call_expression.into()) @@ -4448,7 +4464,7 @@ impl<'db> TypeInferenceBuilder<'db> { return dunder_getitem_method .call(self.db(), &CallArguments::positional([value_ty, slice_ty])) - .return_ty_result(&self.context, value_node.into()) + .return_ty_result( &self.context, value_node.into()) .unwrap_or_else(|err| { self.context.report_lint( &CALL_NON_CALLABLE, @@ -5156,6 +5172,55 @@ impl<'db> TypeInferenceBuilder<'db> { 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 KnownInstanceType::ChainMap => { self.infer_type_expression(arguments_slice); @@ -5241,7 +5306,9 @@ impl<'db> TypeInferenceBuilder<'db> { ); Type::Unknown } - KnownInstanceType::TypingSelf | KnownInstanceType::TypeAlias => { + KnownInstanceType::TypingSelf + | KnownInstanceType::TypeAlias + | KnownInstanceType::Unknown => { self.context.report_lint( &INVALID_TYPE_FORM, subscript.into(), diff --git a/crates/red_knot_vendored/knot_extensions/README.md b/crates/red_knot_vendored/knot_extensions/README.md new file mode 100644 index 0000000000000..d26f2802b53d3 --- /dev/null +++ b/crates/red_knot_vendored/knot_extensions/README.md @@ -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`). diff --git a/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi b/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi new file mode 100644 index 0000000000000..581b2a3d29717 --- /dev/null +++ b/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi @@ -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: ... diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/VERSIONS b/crates/red_knot_vendored/vendor/typeshed/stdlib/VERSIONS index 3c6898dc1a777..6cb47d4bfeb13 100644 --- a/crates/red_knot_vendored/vendor/typeshed/stdlib/VERSIONS +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/VERSIONS @@ -341,3 +341,5 @@ zipfile._path: 3.12- zipimport: 3.0- zlib: 3.0- zoneinfo: 3.9- +# Patch applied for red_knot: +knot_extensions: 3.0- diff --git a/crates/red_knot_vendored/vendor/typeshed/stdlib/knot_extensions.pyi b/crates/red_knot_vendored/vendor/typeshed/stdlib/knot_extensions.pyi new file mode 120000 index 0000000000000..8e6b527c81002 --- /dev/null +++ b/crates/red_knot_vendored/vendor/typeshed/stdlib/knot_extensions.pyi @@ -0,0 +1 @@ +../../../knot_extensions/knot_extensions.pyi \ No newline at end of file