mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:45:24 +00:00
[red-knot] Emit an error if a bare Annotated
or Literal
is used in a type expression (#14973)
This commit is contained in:
parent
fa46ba2306
commit
1389cb8e59
4 changed files with 135 additions and 23 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
```
|
|
@ -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.
|
||||||
///
|
///
|
||||||
|
|
|
@ -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!(),
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue