diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index c7d9f92b02..97102aa6b7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -248,6 +248,7 @@ static_assert(is_assignable_to(Intersection[Child1, Parent], Parent)) static_assert(is_assignable_to(Intersection[Parent, Unrelated], Parent)) static_assert(is_assignable_to(Intersection[Child1, Unrelated], Child1)) +static_assert(is_assignable_to(Intersection[Child1, Unrelated, Child2], Intersection[Child1, Unrelated])) static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Child1)) static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Parent)) @@ -256,21 +257,40 @@ static_assert(is_assignable_to(Intersection[Child1, Not[Grandchild]], Parent)) static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child1, Child2])) static_assert(is_assignable_to(Intersection[Child1, Child2], Intersection[Child2, Child1])) static_assert(is_assignable_to(Grandchild, Intersection[Child1, Child2])) +static_assert(not is_assignable_to(Intersection[Child1, Child2], Intersection[Parent, Unrelated])) static_assert(not is_assignable_to(Parent, Intersection[Parent, Unrelated])) static_assert(not is_assignable_to(int, Intersection[int, Not[Literal[1]]])) +# The literal `1` is not assignable to `Parent`, so the intersection of int and Parent is definitely an int that is not `1` +static_assert(is_assignable_to(Intersection[int, Parent], Intersection[int, Not[Literal[1]]])) static_assert(not is_assignable_to(int, Not[int])) static_assert(not is_assignable_to(int, Not[Literal[1]])) -static_assert(not is_assignable_to(Intersection[Any, Parent], Unrelated)) -static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Any])) -static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]])) +# Intersection with `Any` dominates the left hand side of intersections +static_assert(is_assignable_to(Intersection[Any, Parent], Parent)) +static_assert(is_assignable_to(Intersection[Any, Child1], Parent)) +static_assert(is_assignable_to(Intersection[Any, Child2, Not[Child1]], Parent)) +static_assert(is_assignable_to(Intersection[Any, Parent], Unrelated)) +static_assert(is_assignable_to(Intersection[Any, Parent], Intersection[Parent, Unrelated])) +static_assert(is_assignable_to(Intersection[Any, Parent, Unrelated], Parent)) +static_assert(is_assignable_to(Intersection[Any, Parent, Unrelated], Intersection[Parent, Unrelated])) -# TODO: The following assertions should not fail (see https://github.com/astral-sh/ruff/issues/14899) -# error: [static-assert-error] -static_assert(is_assignable_to(Intersection[Any, int], int)) -# error: [static-assert-error] -static_assert(is_assignable_to(Intersection[Unrelated, Any], Not[tuple[Unrelated, Any]])) +# Even Any & Not[Parent] is assignable to Parent, since it could be Never +static_assert(is_assignable_to(Intersection[Any, Not[Parent]], Parent)) + +# Intersection with `Any` is effectively ignored on the right hand side for the sake of assignment +static_assert(is_assignable_to(Parent, Intersection[Any, Parent])) +static_assert(is_assignable_to(Parent, Parent | Intersection[Any, Unrelated])) +static_assert(is_assignable_to(Child1, Intersection[Any, Parent])) +static_assert(not is_assignable_to(Literal[1], Intersection[Any, Parent])) +static_assert(not is_assignable_to(Unrelated, Intersection[Any, Parent])) + +# Intersections with Any on both sides combine the above logic - the LHS dominates and Any is ignored on the right hand side +static_assert(is_assignable_to(Intersection[Any, Parent], Intersection[Any, Parent])) +static_assert(is_assignable_to(Intersection[Any, Unrelated], Intersection[Any, Parent])) +static_assert(is_assignable_to(Intersection[Any, Parent, Unrelated], Intersection[Any, Parent, Unrelated])) +static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]])) +static_assert(is_assignable_to(Intersection[Literal[1], Any], Intersection[Unrelated, Not[Any]])) ``` ## General properties diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 9041af59e8..811c35aa6e 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -802,6 +802,28 @@ impl<'db> Type<'db> { .iter() .any(|&elem_ty| ty.is_assignable_to(db, elem_ty)), + // A type S is assignable to an intersection type T if + // S is assignable to all positive elements of T (e.g. `str & int` is assignable to `str & Any`), and + // S is disjoint from all negative elements of T (e.g. `int` is not assignable to Intersection[int, Not[Literal[1]]]). + (ty, Type::Intersection(intersection)) => { + intersection + .positive(db) + .iter() + .all(|&elem_ty| ty.is_assignable_to(db, elem_ty)) + && intersection + .negative(db) + .iter() + .all(|&neg_ty| ty.is_disjoint_from(db, neg_ty)) + } + + // An intersection type S is assignable to a type T if + // Any element of S is assignable to T (e.g. `A & B` is assignable to `A`) + // Negative elements do not have an effect on assignability - if S is assignable to T then S & ~P is also assignable to T. + (Type::Intersection(intersection), ty) => intersection + .positive(db) + .iter() + .any(|&elem_ty| elem_ty.is_assignable_to(db, ty)), + // A tuple type S is assignable to a tuple type T if their lengths are the same, and // each element of S is assignable to the corresponding element of T. (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => { diff --git a/crates/ruff_benchmark/benches/red_knot.rs b/crates/ruff_benchmark/benches/red_knot.rs index 4e45a0dcb6..4c65b2348c 100644 --- a/crates/ruff_benchmark/benches/red_knot.rs +++ b/crates/ruff_benchmark/benches/red_knot.rs @@ -67,28 +67,6 @@ static EXPECTED_DIAGNOSTICS: &[KeyDiagnosticFields] = &[ Cow::Borrowed("Module `collections.abc` has no member `Iterable`"), Severity::Error, ), - // We don't handle intersections in `is_assignable_to` yet - ( - DiagnosticId::lint("invalid-argument-type"), - Some("/src/tomllib/_parser.py"), - Some(20158..20172), - Cow::Borrowed("Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_datetime`; expected type `Match`"), - Severity::Error, - ), - ( - DiagnosticId::lint("invalid-argument-type"), - Some("/src/tomllib/_parser.py"), - Some(20464..20479), - Cow::Borrowed("Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_localtime`; expected type `Match`"), - Severity::Error, - ), - ( - DiagnosticId::lint("invalid-argument-type"), - Some("/src/tomllib/_parser.py"), - Some(20774..20786), - Cow::Borrowed("Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_number`; expected type `Match`"), - Severity::Error, - ), ( DiagnosticId::lint("unused-ignore-comment"), Some("/src/tomllib/_parser.py"),