[red-knot] Emit an error if a bare Annotated or Literal is used in a type expression (#14973)

This commit is contained in:
Alex Waygood 2024-12-15 02:00:52 +00:00 committed by GitHub
parent fa46ba2306
commit 1389cb8e59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 135 additions and 23 deletions

View file

@ -29,10 +29,24 @@ It is invalid to parameterize `Annotated` with less than two arguments.
```py ```py
from typing_extensions import Annotated 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): def _(x: Annotated):
reveal_type(x) # revealed: Unknown 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] # error: [invalid-type-form]
def _(x: Annotated[()]): def _(x: Annotated[()]):
reveal_type(x) # revealed: Unknown reveal_type(x) # revealed: Unknown

View file

@ -91,3 +91,13 @@ a1: Literal[26]
def f(): def f():
reveal_type(a1) # revealed: Literal[26] 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
```

View file

@ -28,7 +28,7 @@ use crate::stdlib::{
use crate::symbol::{Boundness, Symbol}; use crate::symbol::{Boundness, Symbol};
use crate::types::call::{CallDunderResult, CallOutcome}; use crate::types::call::{CallDunderResult, CallOutcome};
use crate::types::class_base::ClassBase; 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::mro::{Mro, MroError, MroIterator};
use crate::types::narrow::narrowing_constraint; use crate::types::narrow::narrowing_constraint;
use crate::{Db, FxOrderSet, Module, Program, PythonVersion}; 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 /// `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 /// expression, it names the type `Type::Instance(builtins.int)`, that is, all objects whose
/// `__class__` is `int`. /// `__class__` is `int`.
#[must_use] pub fn in_type_expression(
pub fn in_type_expression(&self, db: &'db dyn Db) -> Type<'db> { &self,
db: &'db dyn Db,
) -> Result<Type<'db>, InvalidTypeExpressionError<'db>> {
match self { match self {
// In a type expression, a bare `type` is interpreted as "instance of `type`", which is // In a type expression, a bare `type` is interpreted as "instance of `type`", which is
// equivalent to `type[object]`. // 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`: // We treat `typing.Type` exactly the same as `builtins.type`:
Type::KnownInstance(KnownInstanceType::Type) => KnownClass::Type.to_instance(db), Type::KnownInstance(KnownInstanceType::Type) => Ok(KnownClass::Type.to_instance(db)),
Type::KnownInstance(KnownInstanceType::Tuple) => KnownClass::Tuple.to_instance(db), Type::KnownInstance(KnownInstanceType::Tuple) => Ok(KnownClass::Tuple.to_instance(db)),
Type::Union(union) => union.map(db, |element| element.in_type_expression(db)), Type::Union(union) => {
Type::Unknown => Type::Unknown, let mut builder = UnionBuilder::new(db);
// TODO map this to a new `Type::TypeVar` variant let mut invalid_expressions = smallvec::SmallVec::default();
Type::KnownInstance(KnownInstanceType::TypeVar(_)) => *self, for element in union.elements(db) {
Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => alias.value_ty(db), match element.in_type_expression(db) {
Type::KnownInstance(KnownInstanceType::Never | KnownInstanceType::NoReturn) => { Ok(type_expr) => builder = builder.add(type_expr),
Type::Never Err(InvalidTypeExpressionError {
fallback_type,
invalid_expressions: new_invalid_expressions,
}) => {
invalid_expressions.extend(new_invalid_expressions);
builder = builder.add(fallback_type);
} }
Type::KnownInstance(KnownInstanceType::LiteralString) => Type::LiteralString, }
Type::KnownInstance(KnownInstanceType::Any) => Type::Any, }
if invalid_expressions.is_empty() {
Ok(builder.build())
} else {
Err(InvalidTypeExpressionError {
fallback_type: builder.build(),
invalid_expressions,
})
}
}
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 // TODO: Should emit a diagnostic
Type::KnownInstance(KnownInstanceType::Annotated) => Type::Unknown, Type::KnownInstance(KnownInstanceType::Annotated) => Err(InvalidTypeExpressionError {
Type::Todo(_) => *self, invalid_expressions: smallvec::smallvec![InvalidTypeExpression::BareAnnotated],
_ => todo_type!("Unsupported or invalid type in a type expression"), 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<Type<'db>> 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 /// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow
/// for easier syntax when interacting with very common classes. /// for easier syntax when interacting with very common classes.
/// ///

View file

@ -4465,9 +4465,12 @@ impl<'db> TypeInferenceBuilder<'db> {
// https://typing.readthedocs.io/en/latest/spec/annotations.html#grammar-token-expression-grammar-type_expression // https://typing.readthedocs.io/en/latest/spec/annotations.html#grammar-token-expression-grammar-type_expression
match expression { match expression {
ast::Expr::Name(name) => match name.ctx { ast::Expr::Name(name) => match name.ctx {
ast::ExprContext::Load => { ast::ExprContext::Load => self
self.infer_name_expression(name).in_type_expression(self.db) .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::Invalid => Type::Unknown,
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(), 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::Expr::Attribute(attribute_expression) => match attribute_expression.ctx {
ast::ExprContext::Load => self ast::ExprContext::Load => self
.infer_attribute_expression(attribute_expression) .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::Invalid => Type::Unknown,
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(), ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(),
}, },