[red-knot] Emit error if int/float/complex/bytes/boolean literals appear in type expressions outside typing.Literal[] (#16765)

## Summary
Fixes https://github.com/astral-sh/ruff/issues/16532

## Test Plan

New mdtest assertions added

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Matthew Mckee 2025-03-17 11:56:16 +00:00 committed by GitHub
parent 93ca4a96e0
commit 24707777af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 72 additions and 8 deletions

View file

@ -47,8 +47,10 @@ def _(c: Callable[42, str]):
Or, when one of the parameter type is invalid in the list:
```py
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
# error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
def _(c: Callable[[int, 42, str, False], None]):
# revealed: (int, @Todo(number literal in type expression), str, @Todo(boolean literal in type expression), /) -> None
# revealed: (int, Unknown, str, Unknown, /) -> None
reveal_type(c)
```

View file

@ -43,3 +43,21 @@ def _(
reveal_type(q) # revealed: Unknown
reveal_type(r) # revealed: Unknown
```
## Invalid AST nodes
```py
def _(
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions"
d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
# error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
e: int | b"foo",
):
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: int | Unknown
```

View file

@ -127,6 +127,13 @@ Literal: _SpecialForm
```py
from other import Literal
# TODO: can we add a subdiagnostic here saying something like:
#
# `other.Literal` and `typing.Literal` have similar names, but are different symbols and don't have the same semantics
#
# ?
#
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
a1: Literal[26]
def f():

View file

@ -16,8 +16,10 @@ reveal_type(cast(int | str, 1)) # revealed: int | str
# error: [invalid-type-form]
reveal_type(cast(Literal, True)) # revealed: Unknown
# error: [invalid-type-form]
reveal_type(cast(1, True)) # revealed: Unknown
# TODO: These should be errors
cast(1)
cast(str)
cast(str, b"ar", "foo")

View file

@ -6085,6 +6085,13 @@ impl<'db> TypeInferenceBuilder<'db> {
/// Infer the type of a type expression without storing the result.
fn infer_type_expression_no_store(&mut self, expression: &ast::Expr) -> Type<'db> {
// https://typing.readthedocs.io/en/latest/spec/annotations.html#grammar-token-expression-grammar-type_expression
let report_invalid_type_expression = |message: std::fmt::Arguments| {
self.context
.report_lint(&INVALID_TYPE_FORM, expression, message);
Type::unknown()
};
match expression {
ast::Expr::Name(name) => match name.ctx {
ast::ExprContext::Load => self
@ -6115,12 +6122,40 @@ impl<'db> TypeInferenceBuilder<'db> {
todo_type!("ellipsis literal in type expression")
}
// Other literals do not have meaningful values in the annotation expression context.
// However, we will we want to handle these differently when working with special forms,
// since (e.g.) `123` is not valid in an annotation expression but `Literal[123]` is.
ast::Expr::BytesLiteral(_literal) => todo_type!("bytes literal in type expression"),
ast::Expr::NumberLiteral(_literal) => todo_type!("number literal in type expression"),
ast::Expr::BooleanLiteral(_literal) => todo_type!("boolean literal in type expression"),
// TODO: add a subdiagnostic linking to type-expression grammar
// and stating that it is only valid in `typing.Literal[]` or `typing.Annotated[]`
ast::Expr::BytesLiteral(_) => report_invalid_type_expression(format_args!(
"Bytes literals are not allowed in this context in a type expression"
)),
// TODO: add a subdiagnostic linking to type-expression grammar
// and stating that it is only valid in `typing.Literal[]` or `typing.Annotated[]`
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(_),
..
}) => report_invalid_type_expression(format_args!(
"Int literals are not allowed in this context in a type expression"
)),
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Float(_),
..
}) => report_invalid_type_expression(format_args!(
"Float literals are not allowed in type expressions"
)),
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Complex { .. },
..
}) => report_invalid_type_expression(format_args!(
"Complex literals are not allowed in type expressions"
)),
// TODO: add a subdiagnostic linking to type-expression grammar
// and stating that it is only valid in `typing.Literal[]` or `typing.Annotated[]`
ast::Expr::BooleanLiteral(_) => report_invalid_type_expression(format_args!(
"Boolean literals are not allowed in this context in a type expression"
)),
ast::Expr::Subscript(subscript) => {
let ast::ExprSubscript {