Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[red-knot] More comprehensive is_assignable_to tests #15353

Merged
merged 8 commits into from
Jan 8, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
# Assignable-to relation

The `is_assignable_to(S, T)` relation below checks if type `S` is assignable to type `T` (target).
This allows us to check if a type `S` can be used in a context where a type `T` is expected
(function arguments, variable assignments). See the [typing documentation] for a precise definition
of this concept.

```py
from knot_extensions import static_assert, is_assignable_to, Unknown, TypeOf, Intersection, Not
from typing_extensions import Never, Any, Literal, LiteralString
from abc import ABCMeta

class Parent: ...
class Child1(Parent): ...
class Child2(Parent): ...
class Grandchild(Child1, Child2): ...
class Unrelated: ...

# Basic fully static types

static_assert(is_assignable_to(int, int))
static_assert(is_assignable_to(Parent, Parent))
static_assert(is_assignable_to(Child1, Parent))
static_assert(is_assignable_to(Grandchild, Parent))
static_assert(is_assignable_to(Unrelated, Unrelated))

static_assert(not is_assignable_to(str, int))
static_assert(not is_assignable_to(object, int))
static_assert(not is_assignable_to(Parent, Child1))
static_assert(not is_assignable_to(Unrelated, Parent))
static_assert(not is_assignable_to(Child1, Child2))

# Basic gradual types

static_assert(is_assignable_to(Unknown, Literal[1]))
static_assert(is_assignable_to(Any, Literal[1]))
static_assert(is_assignable_to(Literal[1], Unknown))
static_assert(is_assignable_to(Literal[1], Any))

# Everything is assignable to object

static_assert(is_assignable_to(str, object))
static_assert(is_assignable_to(Literal[1], object))
static_assert(is_assignable_to(object, object))
static_assert(is_assignable_to(type, object))
static_assert(is_assignable_to(Any, object))
static_assert(is_assignable_to(Unknown, object))
static_assert(is_assignable_to(type[object], object))
static_assert(is_assignable_to(type[str], object))
static_assert(is_assignable_to(type[Any], object))

# Every type is assignable to Any/Unknown

static_assert(is_assignable_to(str, Any))
static_assert(is_assignable_to(Literal[1], Any))
static_assert(is_assignable_to(object, Any))
static_assert(is_assignable_to(type, Any))
static_assert(is_assignable_to(Any, Any))
static_assert(is_assignable_to(Unknown, Any))
static_assert(is_assignable_to(type[object], Any))
static_assert(is_assignable_to(type[str], Any))
static_assert(is_assignable_to(type[Any], Any))

static_assert(is_assignable_to(str, Unknown))
static_assert(is_assignable_to(Literal[1], Unknown))
static_assert(is_assignable_to(object, Unknown))
static_assert(is_assignable_to(type, Unknown))
static_assert(is_assignable_to(Any, Unknown))
static_assert(is_assignable_to(Unknown, Unknown))
static_assert(is_assignable_to(type[object], Unknown))
static_assert(is_assignable_to(type[str], Unknown))
static_assert(is_assignable_to(type[Any], Unknown))

# Never is assignable to every type

static_assert(is_assignable_to(Never, str))
static_assert(is_assignable_to(Never, Literal[1]))
static_assert(is_assignable_to(Never, object))
static_assert(is_assignable_to(Never, type))
static_assert(is_assignable_to(Never, Any))
static_assert(is_assignable_to(Never, Unknown))
static_assert(is_assignable_to(Never, type[object]))
static_assert(is_assignable_to(Never, type[str]))
static_assert(is_assignable_to(Never, type[Any]))

# Boolean literals

static_assert(is_assignable_to(Literal[True], Literal[True]))
static_assert(is_assignable_to(Literal[True], bool))
static_assert(is_assignable_to(Literal[True], int))

static_assert(not is_assignable_to(Literal[True], Literal[False]))
static_assert(not is_assignable_to(bool, Literal[True]))

# Integer literals

static_assert(is_assignable_to(Literal[1], Literal[1]))
static_assert(is_assignable_to(Literal[1], int))

static_assert(not is_assignable_to(Literal[1], Literal[2]))
static_assert(not is_assignable_to(int, Literal[1]))
static_assert(not is_assignable_to(Literal[1], str))

# String literals, `LiteralString`

static_assert(is_assignable_to(Literal["foo"], Literal["foo"]))
static_assert(is_assignable_to(Literal["foo"], LiteralString))
static_assert(is_assignable_to(Literal["foo"], str))

static_assert(is_assignable_to(LiteralString, str))

static_assert(not is_assignable_to(Literal["foo"], Literal["bar"]))
static_assert(not is_assignable_to(str, Literal["foo"]))
static_assert(not is_assignable_to(str, LiteralString))

# Byte literals

static_assert(is_assignable_to(Literal[b"foo"], bytes))
static_assert(is_assignable_to(Literal[b"foo"], Literal[b"foo"]))

static_assert(not is_assignable_to(Literal[b"foo"], str))
static_assert(not is_assignable_to(Literal[b"foo"], LiteralString))
static_assert(not is_assignable_to(Literal[b"foo"], Literal[b"bar"]))
static_assert(not is_assignable_to(Literal[b"foo"], Literal["foo"]))
static_assert(not is_assignable_to(Literal["foo"], Literal[b"foo"]))

# type[…]

static_assert(is_assignable_to(type[Any], type[Any]))
static_assert(is_assignable_to(type[Any], type[object]))
static_assert(is_assignable_to(type[Any], type[str]))
static_assert(is_assignable_to(type[object], type[Any]))
static_assert(is_assignable_to(type[object], type[object]))
static_assert(is_assignable_to(type[object], type))
static_assert(is_assignable_to(type[str], type[Any]))
static_assert(is_assignable_to(type[str], type[object]))

static_assert(not is_assignable_to(type[object], type[str]))

static_assert(is_assignable_to(type[str], type[str]))
static_assert(is_assignable_to(type[str], type))

static_assert(not is_assignable_to(type, type[str]))

static_assert(is_assignable_to(type, type[Any]))
static_assert(is_assignable_to(type, type[object]))
static_assert(is_assignable_to(type, type))
static_assert(is_assignable_to(TypeOf[str], type[Any]))
static_assert(is_assignable_to(type[str], type[Unknown]))
static_assert(is_assignable_to(type[Unknown], type[str]))
static_assert(is_assignable_to(type[Any], type[Unknown]))
static_assert(is_assignable_to(type[Any], ABCMeta))
static_assert(is_assignable_to(type[Unknown], ABCMeta))

# Tuple types

static_assert(is_assignable_to(tuple[()], tuple[()]))
static_assert(is_assignable_to(tuple[int], tuple[int]))
static_assert(is_assignable_to(tuple[int, str], tuple[int, str]))
static_assert(is_assignable_to(tuple[Child1, Child2], tuple[Parent, Parent]))

static_assert(not is_assignable_to(tuple[()], tuple[int]))
static_assert(not is_assignable_to(tuple[int], tuple[int, str]))
static_assert(not is_assignable_to(tuple[int, str], tuple[int]))
static_assert(not is_assignable_to(tuple[Parent, int], tuple[Child1, int]))

# Union types

static_assert(is_assignable_to(int, int | str))
static_assert(is_assignable_to(str, int | str))
static_assert(is_assignable_to(int | str, int | str))
static_assert(is_assignable_to(str | int, int | str))
static_assert(is_assignable_to(Literal[1], int | str))
static_assert(is_assignable_to(Literal[1], Unknown | str))
static_assert(is_assignable_to(Literal[1] | Literal[2], Literal[1] | Literal[2]))
static_assert(is_assignable_to(Literal[1] | Literal[2], int))
static_assert(is_assignable_to(Literal[1] | None, int | None))
static_assert(is_assignable_to(Child1 | Child2, Parent))

static_assert(not is_assignable_to(int | None, int))
static_assert(not is_assignable_to(int | None, str | None))
static_assert(not is_assignable_to(Literal[1] | None, int))
static_assert(not is_assignable_to(Literal[1] | None, str | None))

# Intersection types

static_assert(is_assignable_to(Intersection[Child1, Child2], Child1))
static_assert(is_assignable_to(Intersection[Child1, Child2], Child2))
static_assert(is_assignable_to(Intersection[Child1, Child2], Parent))
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, Not[Child2]], Child1))
static_assert(is_assignable_to(Intersection[Child1, Not[Child2]], Parent))
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(Parent, Intersection[Parent, Unrelated]))
static_assert(not is_assignable_to(int, Intersection[int, Not[Literal[1]]]))
static_assert(not is_assignable_to(int, Not[int]))
static_assert(not is_assignable_to(int, Not[Literal[1]]))

# 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[Unrelated, Any], Intersection[Unrelated, Any]))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Intersection[Unrelated, Not[Any]]))
# error: [static-assert-error]
static_assert(is_assignable_to(Intersection[Unrelated, Any], Not[tuple[Unrelated, Any]]))
```

[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
83 changes: 3 additions & 80 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,9 @@ impl<'db> Type<'db> {
return true;
}
match (self, target) {
// Never can be assigned to any type.
(Type::Never, _) => true,

// The dynamic type is assignable-to and assignable-from any type.
(Type::Unknown | Type::Any | Type::Todo(_), _) => true,
(_, Type::Unknown | Type::Any | Type::Todo(_)) => true,
Expand Down Expand Up @@ -3988,7 +3991,6 @@ pub(crate) mod tests {
},
Tuple(Vec<Ty>),
SubclassOfAny,
SubclassOfUnknown,
SubclassOfBuiltinClass(&'static str),
SubclassOfAbcClass(&'static str),
StdlibModule(KnownModule),
Expand Down Expand Up @@ -4039,7 +4041,6 @@ pub(crate) mod tests {
TupleType::from_elements(db, elements)
}
Ty::SubclassOfAny => SubclassOfType::subclass_of_any(),
Ty::SubclassOfUnknown => SubclassOfType::subclass_of_unknown(),
Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from(
db,
builtins_symbol(db, s)
Expand Down Expand Up @@ -4078,84 +4079,6 @@ pub(crate) mod tests {
assert_eq!(ty.into_type(&db), Type::Never);
}

#[test_case(Ty::BuiltinInstance("str"), Ty::BuiltinInstance("object"))]
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("object"))]
#[test_case(Ty::Unknown, Ty::IntLiteral(1))]
#[test_case(Ty::Any, Ty::IntLiteral(1))]
#[test_case(Ty::Never, Ty::IntLiteral(1))]
#[test_case(Ty::IntLiteral(1), Ty::Unknown)]
#[test_case(Ty::IntLiteral(1), Ty::Any)]
#[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("int"))]
#[test_case(Ty::StringLiteral("foo"), Ty::BuiltinInstance("str"))]
#[test_case(Ty::StringLiteral("foo"), Ty::LiteralString)]
#[test_case(Ty::LiteralString, Ty::BuiltinInstance("str"))]
#[test_case(Ty::BytesLiteral("foo"), Ty::BuiltinInstance("bytes"))]
#[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str")]))]
#[test_case(Ty::IntLiteral(1), Ty::Union(vec![Ty::Unknown, Ty::BuiltinInstance("str")]))]
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]))]
#[test_case(
Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]),
Ty::BuiltinInstance("int")
)]
#[test_case(
Ty::Union(vec![Ty::IntLiteral(1), Ty::None]),
Ty::Union(vec![Ty::BuiltinInstance("int"), Ty::None])
)]
#[test_case(Ty::Tuple(vec![Ty::Todo]), Ty::Tuple(vec![Ty::IntLiteral(2)]))]
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::Todo]))]
#[test_case(Ty::SubclassOfAny, Ty::SubclassOfAny)]
#[test_case(Ty::SubclassOfAny, Ty::SubclassOfBuiltinClass("object"))]
#[test_case(Ty::SubclassOfAny, Ty::SubclassOfBuiltinClass("str"))]
#[test_case(Ty::SubclassOfAny, Ty::BuiltinInstance("type"))]
#[test_case(Ty::SubclassOfBuiltinClass("object"), Ty::SubclassOfAny)]
#[test_case(
Ty::SubclassOfBuiltinClass("object"),
Ty::SubclassOfBuiltinClass("object")
)]
#[test_case(Ty::SubclassOfBuiltinClass("object"), Ty::BuiltinInstance("type"))]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::SubclassOfAny)]
#[test_case(
Ty::SubclassOfBuiltinClass("str"),
Ty::SubclassOfBuiltinClass("object")
)]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::SubclassOfBuiltinClass("str"))]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::BuiltinInstance("type"))]
#[test_case(Ty::BuiltinInstance("type"), Ty::SubclassOfAny)]
#[test_case(Ty::BuiltinInstance("type"), Ty::SubclassOfBuiltinClass("object"))]
#[test_case(Ty::BuiltinInstance("type"), Ty::BuiltinInstance("type"))]
#[test_case(Ty::BuiltinClassLiteral("str"), Ty::SubclassOfAny)]
#[test_case(Ty::SubclassOfBuiltinClass("str"), Ty::SubclassOfUnknown)]
#[test_case(Ty::SubclassOfUnknown, Ty::SubclassOfBuiltinClass("str"))]
#[test_case(Ty::SubclassOfAny, Ty::AbcInstance("ABCMeta"))]
#[test_case(Ty::SubclassOfUnknown, Ty::AbcInstance("ABCMeta"))]
#[test_case(Ty::SubclassOfAny, Ty::BuiltinInstance("object"))]
fn is_assignable_to(from: Ty, to: Ty) {
let db = setup_db();
assert!(from.into_type(&db).is_assignable_to(&db, to.into_type(&db)));
}

#[test_case(Ty::BuiltinInstance("object"), Ty::BuiltinInstance("int"))]
#[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("str"))]
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str"))]
#[test_case(Ty::BuiltinInstance("int"), Ty::IntLiteral(1))]
#[test_case(
Ty::Union(vec![Ty::IntLiteral(1), Ty::None]),
Ty::BuiltinInstance("int")
)]
#[test_case(
Ty::Union(vec![Ty::IntLiteral(1), Ty::None]),
Ty::Union(vec![Ty::BuiltinInstance("str"), Ty::None])
)]
#[test_case(
Ty::SubclassOfBuiltinClass("object"),
Ty::SubclassOfBuiltinClass("str")
)]
#[test_case(Ty::BuiltinInstance("type"), Ty::SubclassOfBuiltinClass("str"))]
fn is_not_assignable_to(from: Ty, to: Ty) {
let db = setup_db();
assert!(!from.into_type(&db).is_assignable_to(&db, to.into_type(&db)));
}

#[test_case(Ty::BuiltinInstance("str"), Ty::BuiltinInstance("object"))]
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("object"))]
#[test_case(Ty::BuiltinInstance("bool"), Ty::BuiltinInstance("object"))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ macro_rules! type_property_test {
}

mod stable {
use super::KnownClass;
use crate::types::{KnownClass, Type};

// `T` is equivalent to itself.
type_property_test!(
Expand Down Expand Up @@ -299,6 +299,12 @@ mod stable {
all_fully_static_types_subtype_of_object, db,
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, KnownClass::Object.to_instance(db))
);

// Never should be assignable to every type
type_property_test!(
never_assignable_to_every_type, db,
forall types t. Type::Never.is_assignable_to(db, t)
);
}

/// This module contains property tests that currently lead to many false positives.
Expand Down
Loading