diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/annotated.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/annotated.md index 6601eda871..71b06a24f2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/annotated.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/annotated.md @@ -29,10 +29,24 @@ It is invalid to parameterize `Annotated` with less than two arguments. ```py from typing_extensions import Annotated -# TODO: This should be an error +# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression" def _(x: Annotated): reveal_type(x) # revealed: Unknown +def _(flag: bool): + if flag: + X = Annotated + else: + X = bool + + # error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression" + def f(y: X): + reveal_type(y) # revealed: Unknown | bool + +# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression" +def _(x: Annotated | bool): + reveal_type(x) # revealed: Unknown | bool + # error: [invalid-type-form] def _(x: Annotated[()]): reveal_type(x) # revealed: Unknown diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/literal.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md similarity index 91% rename from crates/red_knot_python_semantic/resources/mdtest/literal/literal.md rename to crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md index 0f9acaf3b5..ca7fff5731 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/literal/literal.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal.md @@ -91,3 +91,13 @@ a1: Literal[26] def f(): reveal_type(a1) # revealed: Literal[26] ``` + +## Invalid + +```py +from typing import Literal + +# error: [invalid-type-form] "`Literal` requires at least one argument when used in a type expression" +def _(x: Literal): + reveal_type(x) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 440e8a99df..b714f3b0f9 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -28,7 +28,7 @@ use crate::stdlib::{ use crate::symbol::{Boundness, Symbol}; use crate::types::call::{CallDunderResult, CallOutcome}; use crate::types::class_base::ClassBase; -use crate::types::diagnostic::TypeCheckDiagnosticsBuilder; +use crate::types::diagnostic::{TypeCheckDiagnosticsBuilder, INVALID_TYPE_FORM}; use crate::types::mro::{Mro, MroError, MroIterator}; use crate::types::narrow::narrowing_constraint; use crate::{Db, FxOrderSet, Module, Program, PythonVersion}; @@ -1881,29 +1881,63 @@ impl<'db> Type<'db> { /// `Type::ClassLiteral(builtins.int)`, that is, it is the `int` class itself. As a type /// expression, it names the type `Type::Instance(builtins.int)`, that is, all objects whose /// `__class__` is `int`. - #[must_use] - pub fn in_type_expression(&self, db: &'db dyn Db) -> Type<'db> { + pub fn in_type_expression( + &self, + db: &'db dyn Db, + ) -> Result, InvalidTypeExpressionError<'db>> { match self { // In a type expression, a bare `type` is interpreted as "instance of `type`", which is // equivalent to `type[object]`. - Type::ClassLiteral(_) | Type::SubclassOf(_) => self.to_instance(db), + Type::ClassLiteral(_) | Type::SubclassOf(_) => Ok(self.to_instance(db)), // We treat `typing.Type` exactly the same as `builtins.type`: - Type::KnownInstance(KnownInstanceType::Type) => KnownClass::Type.to_instance(db), - Type::KnownInstance(KnownInstanceType::Tuple) => KnownClass::Tuple.to_instance(db), - Type::Union(union) => union.map(db, |element| element.in_type_expression(db)), - Type::Unknown => Type::Unknown, - // TODO map this to a new `Type::TypeVar` variant - Type::KnownInstance(KnownInstanceType::TypeVar(_)) => *self, - Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => alias.value_ty(db), - Type::KnownInstance(KnownInstanceType::Never | KnownInstanceType::NoReturn) => { - Type::Never + Type::KnownInstance(KnownInstanceType::Type) => Ok(KnownClass::Type.to_instance(db)), + Type::KnownInstance(KnownInstanceType::Tuple) => Ok(KnownClass::Tuple.to_instance(db)), + Type::Union(union) => { + let mut builder = UnionBuilder::new(db); + let mut invalid_expressions = smallvec::SmallVec::default(); + for element in union.elements(db) { + match element.in_type_expression(db) { + Ok(type_expr) => builder = builder.add(type_expr), + Err(InvalidTypeExpressionError { + fallback_type, + invalid_expressions: new_invalid_expressions, + }) => { + invalid_expressions.extend(new_invalid_expressions); + builder = builder.add(fallback_type); + } + } + } + if invalid_expressions.is_empty() { + Ok(builder.build()) + } else { + Err(InvalidTypeExpressionError { + fallback_type: builder.build(), + invalid_expressions, + }) + } } - Type::KnownInstance(KnownInstanceType::LiteralString) => Type::LiteralString, - Type::KnownInstance(KnownInstanceType::Any) => Type::Any, + Type::Unknown => Ok(Type::Unknown), + // TODO map this to a new `Type::TypeVar` variant + Type::KnownInstance(KnownInstanceType::TypeVar(_)) => Ok(*self), + Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => Ok(alias.value_ty(db)), + Type::KnownInstance(KnownInstanceType::Never | KnownInstanceType::NoReturn) => { + Ok(Type::Never) + } + Type::KnownInstance(KnownInstanceType::LiteralString) => Ok(Type::LiteralString), + Type::KnownInstance(KnownInstanceType::Any) => Ok(Type::Any), // TODO: Should emit a diagnostic - Type::KnownInstance(KnownInstanceType::Annotated) => Type::Unknown, - Type::Todo(_) => *self, - _ => todo_type!("Unsupported or invalid type in a type expression"), + Type::KnownInstance(KnownInstanceType::Annotated) => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![InvalidTypeExpression::BareAnnotated], + fallback_type: Type::Unknown, + }), + Type::KnownInstance(KnownInstanceType::Literal) => Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec![InvalidTypeExpression::BareLiteral], + fallback_type: Type::Unknown, + }), + Type::Todo(_) => Ok(*self), + _ => Ok(todo_type!( + "Unsupported or invalid type in a type expression" + )), } } @@ -2032,6 +2066,54 @@ impl<'db> From> for Symbol<'db> { } } +/// Error struct providing information on type(s) that were deemed to be invalid +/// in a type expression context, and the type we should therefore fallback to +/// for the problematic type expression. +#[derive(Debug, PartialEq, Eq)] +pub struct InvalidTypeExpressionError<'db> { + fallback_type: Type<'db>, + invalid_expressions: smallvec::SmallVec<[InvalidTypeExpression; 1]>, +} + +impl<'db> InvalidTypeExpressionError<'db> { + fn into_fallback_type( + self, + diagnostics: &mut TypeCheckDiagnosticsBuilder, + node: &ast::Expr, + ) -> Type<'db> { + let InvalidTypeExpressionError { + fallback_type, + invalid_expressions, + } = self; + for error in invalid_expressions { + diagnostics.add_lint( + &INVALID_TYPE_FORM, + node.into(), + format_args!("{}", error.reason()), + ); + } + fallback_type + } +} + +/// Enumeration of various types that are invalid in type-expression contexts +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum InvalidTypeExpression { + /// `x: Annotated` is invalid as an annotation + BareAnnotated, + /// `x: Literal` is invalid as an annotation + BareLiteral, +} + +impl InvalidTypeExpression { + const fn reason(self) -> &'static str { + match self { + Self::BareAnnotated => "`Annotated` requires at least two arguments when used in an annotation or type expression", + Self::BareLiteral => "`Literal` requires at least one argument when used in a type expression", + } + } +} + /// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow /// for easier syntax when interacting with very common classes. /// diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 505d04dcf6..5c868163e3 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -4465,9 +4465,12 @@ impl<'db> TypeInferenceBuilder<'db> { // https://typing.readthedocs.io/en/latest/spec/annotations.html#grammar-token-expression-grammar-type_expression match expression { ast::Expr::Name(name) => match name.ctx { - ast::ExprContext::Load => { - self.infer_name_expression(name).in_type_expression(self.db) - } + ast::ExprContext::Load => self + .infer_name_expression(name) + .in_type_expression(self.db) + .unwrap_or_else(|error| { + error.into_fallback_type(&mut self.diagnostics, expression) + }), ast::ExprContext::Invalid => Type::Unknown, ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(), }, @@ -4475,7 +4478,10 @@ impl<'db> TypeInferenceBuilder<'db> { ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx { ast::ExprContext::Load => self .infer_attribute_expression(attribute_expression) - .in_type_expression(self.db), + .in_type_expression(self.db) + .unwrap_or_else(|error| { + error.into_fallback_type(&mut self.diagnostics, expression) + }), ast::ExprContext::Invalid => Type::Unknown, ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(), },