[red-knot] Infer tuple types from annotations (#13943)

## Summary

This PR adds support for heterogenous `tuple` annotations to red-knot.

It does the following:
- Extends `infer_type_expression` so that it understands tuple
annotations
- Changes `infer_type_expression` so that `ExprStarred` nodes in type
annotations are inferred as `Todo` rather than `Unknown` (they're valid
in PEP-646 tuple annotations)
- Extends `Type::is_subtype_of` to understand when one heterogenous
tuple type can be understood to be a subtype of another (without this
change, the PR would have introduced new false-positive errors to some
existing mdtests).
This commit is contained in:
Alex Waygood 2024-10-29 10:30:03 +00:00 committed by GitHub
parent 56c796acee
commit 7dd0c7f4bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 166 additions and 11 deletions

View file

@ -23,6 +23,56 @@ x: int
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
```
## Tuple annotations are understood
```py path=module.py
from typing_extensions import Unpack
a: tuple[()] = ()
b: tuple[int] = (42,)
c: tuple[str, int] = ("42", 42)
d: tuple[tuple[str, str], tuple[int, int]] = (("foo", "foo"), (42, 42))
e: tuple[str, ...] = ()
f: tuple[str, *tuple[int, ...], bytes] = ("42", b"42")
g: tuple[str, Unpack[tuple[int, ...]], bytes] = ("42", b"42")
h: tuple[list[int], list[int]] = ([], [])
i: tuple[str | int, str | int] = (42, 42)
j: tuple[str | int] = (42,)
```
```py path=script.py
from module import a, b, c, d, e, f, g, h, i, j
reveal_type(a) # revealed: tuple[()]
reveal_type(b) # revealed: tuple[int]
reveal_type(c) # revealed: tuple[str, int]
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
# TODO: homogenous tuples, PEP-646 tuples
reveal_type(e) # revealed: @Todo
reveal_type(f) # revealed: @Todo
reveal_type(g) # revealed: @Todo
# TODO: support more kinds of type expressions in annotations
reveal_type(h) # revealed: @Todo
reveal_type(i) # revealed: tuple[str | int, str | int]
reveal_type(j) # revealed: tuple[str | int]
```
## Incorrect tuple assignments are complained about
```py
# error: [invalid-assignment] "Object of type `tuple[Literal[1], Literal[2]]` is not assignable to `tuple[()]`"
a: tuple[()] = (1, 2)
# error: [invalid-assignment] "Object of type `tuple[Literal["foo"]]` is not assignable to `tuple[int]`"
b: tuple[int] = ("foo",)
# error: [invalid-assignment] "Object of type `tuple[list, Literal["foo"]]` is not assignable to `tuple[str | int, str]`"
c: tuple[str | int, str] = ([], "foo")
```
## PEP-604 annotations are supported
```py

View file

@ -16,10 +16,11 @@ class MyBox[T]:
def __init__(self, data: T):
self.data = data
# TODO not error (should be subscriptable)
box: MyBox[int] = MyBox(5) # error: [non-subscriptable]
# TODO error differently (str and int don't unify)
wrong_innards: MyBox[int] = MyBox("five") # error: [non-subscriptable]
box: MyBox[int] = MyBox(5)
# TODO should emit a diagnostic here (str is not assignable to int)
wrong_innards: MyBox[int] = MyBox("five")
# TODO reveal int
reveal_type(box.data) # revealed: @Todo

View file

@ -469,6 +469,16 @@ impl<'db> Type<'db> {
{
true
}
(Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => {
let self_elements = self_tuple.elements(db);
let target_elements = target_tuple.elements(db);
self_elements.len() == target_elements.len()
&& self_elements.iter().zip(target_elements).all(
|(self_element, target_element)| {
self_element.is_subtype_of(db, *target_element)
},
)
}
(Type::ClassLiteral(..), Type::Instance(class))
if class.is_known(db, KnownClass::Type) =>
{
@ -504,6 +514,16 @@ impl<'db> Type<'db> {
.elements(db)
.iter()
.any(|&elem_ty| ty.is_assignable_to(db, elem_ty)),
(Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => {
let self_elements = self_tuple.elements(db);
let target_elements = target_tuple.elements(db);
self_elements.len() == target_elements.len()
&& self_elements.iter().zip(target_elements).all(
|(self_element, target_element)| {
self_element.is_assignable_to(db, *target_element)
},
)
}
// TODO other types containing gradual forms (e.g. generics containing Any/Unknown)
_ => self.is_subtype_of(db, target),
}
@ -1887,6 +1907,7 @@ mod tests {
Unknown,
None,
Any,
Todo,
IntLiteral(i64),
BooleanLiteral(bool),
StringLiteral(&'static str),
@ -1905,6 +1926,7 @@ mod tests {
Ty::Unknown => Type::Unknown,
Ty::None => Type::None,
Ty::Any => Type::Any,
Ty::Todo => Type::Todo,
Ty::IntLiteral(n) => Type::IntLiteral(n),
Ty::StringLiteral(s) => Type::StringLiteral(StringLiteralType::new(db, s)),
Ty::BooleanLiteral(b) => Type::BooleanLiteral(b),
@ -1947,6 +1969,8 @@ mod tests {
#[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::Tuple(vec![Ty::Todo]), Ty::Tuple(vec![Ty::IntLiteral(2)]))]
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(2)]), Ty::Tuple(vec![Ty::Todo]))]
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)));
@ -1981,6 +2005,11 @@ mod tests {
#[test_case(Ty::Union(vec![Ty::BuiltinInstance("str"), Ty::BuiltinInstance("int")]), Ty::BuiltinInstance("object"))]
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2), Ty::IntLiteral(3)]))]
#[test_case(Ty::BuiltinInstance("TypeError"), Ty::BuiltinInstance("Exception"))]
#[test_case(Ty::Tuple(vec![]), Ty::Tuple(vec![]))]
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(42)]), Ty::Tuple(vec![Ty::BuiltinInstance("int")]))]
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(42), Ty::StringLiteral("foo")]), Ty::Tuple(vec![Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str")]))]
#[test_case(Ty::Tuple(vec![Ty::BuiltinInstance("int"), Ty::StringLiteral("foo")]), Ty::Tuple(vec![Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str")]))]
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(42), Ty::BuiltinInstance("str")]), Ty::Tuple(vec![Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str")]))]
fn is_subtype_of(from: Ty, to: Ty) {
let db = setup_db();
assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db)));
@ -1997,6 +2026,10 @@ mod tests {
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]), Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(3)]))]
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str"))]
#[test_case(Ty::BuiltinInstance("int"), Ty::IntLiteral(1))]
#[test_case(Ty::Tuple(vec![]), Ty::Tuple(vec![Ty::IntLiteral(1)]))]
#[test_case(Ty::Tuple(vec![Ty::IntLiteral(42)]), Ty::Tuple(vec![Ty::BuiltinInstance("str")]))]
#[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]))]
fn is_not_subtype_of(from: Ty, to: Ty) {
let db = setup_db();
assert!(!from.into_type(&db).is_subtype_of(&db, to.into_type(&db)));

View file

@ -3568,10 +3568,26 @@ impl<'db> TypeInferenceBuilder<'db> {
ast::Expr::NumberLiteral(_literal) => Type::Todo,
ast::Expr::BooleanLiteral(_literal) => Type::Todo,
// TODO: this may be a place we need to revisit with special forms.
ast::Expr::Subscript(subscript) => {
self.infer_subscript_expression(subscript);
Type::Todo
let ast::ExprSubscript {
value,
slice,
ctx: _,
range: _,
} = subscript;
let value_ty = self.infer_expression(value);
if value_ty
.into_class_literal_type()
.is_some_and(|class| class.is_known(self.db, KnownClass::Tuple))
{
self.infer_tuple_type_expression(slice)
} else {
self.infer_type_expression(slice);
// TODO: many other kinds of subscripts
Type::Todo
}
}
ast::Expr::BinOp(binary) => {
@ -3591,6 +3607,12 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
// TODO PEP 646
ast::Expr::Starred(starred) => {
self.infer_starred_expression(starred);
Type::Todo
}
// Forms which are invalid in the context of annotation expressions: we infer their
// nested expressions as normal expressions, but the type of the top-level expression is
// always `Type::Unknown` in these cases.
@ -3667,10 +3689,6 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_attribute_expression(attribute);
Type::Unknown
}
ast::Expr::Starred(starred) => {
self.infer_starred_expression(starred);
Type::Unknown
}
ast::Expr::List(list) => {
self.infer_list_expression(list);
Type::Unknown
@ -3693,6 +3711,59 @@ impl<'db> TypeInferenceBuilder<'db> {
ty
}
/// Given the slice of a `tuple[]` annotation, return the type that the annotation represents
fn infer_tuple_type_expression(&mut self, tuple_slice: &ast::Expr) -> Type<'db> {
/// In most cases, if a subelement of the tuple is inferred as `Todo`,
/// we should only infer `Todo` for that specific subelement.
/// Certain specific AST nodes can however change the meaning of the entire tuple,
/// however: for example, `tuple[int, ...]` or `tuple[int, *tuple[str, ...]]` are a
/// homogeneous tuple and a partly homogeneous tuple (respectively) due to the `...`
/// and the starred expression (respectively), Neither is supported by us right now,
/// so we should infer `Todo` for the *entire* tuple if we encounter one of those elements.
/// Even a subscript subelement could alter the type of the entire tuple
/// if the subscript is `Unpack[]` (which again, we don't yet support).
fn element_could_alter_type_of_whole_tuple(element: &ast::Expr, element_ty: Type) -> bool {
element_ty.is_todo()
&& matches!(
element,
ast::Expr::EllipsisLiteral(_) | ast::Expr::Starred(_) | ast::Expr::Subscript(_)
)
}
// TODO:
// - homogeneous tuples
// - PEP 646
match tuple_slice {
ast::Expr::Tuple(elements) => {
let mut element_types = Vec::with_capacity(elements.len());
// Whether to infer `Todo` for the whole tuple
// (see docstring for `element_could_alter_type_of_whole_tuple`)
let mut return_todo = false;
for element in elements {
let element_ty = self.infer_type_expression(element);
return_todo |= element_could_alter_type_of_whole_tuple(element, element_ty);
element_types.push(element_ty);
}
if return_todo {
Type::Todo
} else {
Type::Tuple(TupleType::new(self.db, element_types.into_boxed_slice()))
}
}
single_element => {
let single_element_ty = self.infer_type_expression(single_element);
if element_could_alter_type_of_whole_tuple(single_element, single_element_ty) {
Type::Todo
} else {
Type::Tuple(TupleType::new(self.db, Box::from([single_element_ty])))
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]