From 24707777afd4b4c52e985aa1146095fe41adef5d Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Mon, 17 Mar 2025 11:56:16 +0000 Subject: [PATCH] [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 --- .../resources/mdtest/annotations/callable.md | 4 +- .../resources/mdtest/annotations/invalid.md | 18 +++++++ .../resources/mdtest/annotations/literal.md | 7 +++ .../resources/mdtest/directives/cast.md | 4 +- .../src/types/infer.rs | 47 ++++++++++++++++--- 5 files changed, 72 insertions(+), 8 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md index 8963ac6715..37ea251e37 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/callable.md @@ -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) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md index 87c86ac360..cb1d5e847d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/invalid.md @@ -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 +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md index 33d90fafab..4544446f0e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md @@ -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(): diff --git a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md b/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md index 600a3316cb..17ee202dad 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md @@ -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") diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 0de06d02b2..6ffc04d7ca 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -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 {