mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 02:38:25 +00:00
[red-knot] Stricter parsing for type[]
type expressions
This commit is contained in:
parent
cb04343b3b
commit
b1b4f8c272
9 changed files with 158 additions and 58 deletions
|
@ -50,6 +50,22 @@ def _(
|
|||
reveal_type(j_) # revealed: Unknown
|
||||
```
|
||||
|
||||
Even more types are rejected as invalid in the context of `type[]` type expressions:
|
||||
|
||||
```py
|
||||
from typing_extensions import Callable, LiteralString, TypeAlias
|
||||
|
||||
# fmt: off
|
||||
|
||||
def f(
|
||||
a: type[LiteralString], # error: [invalid-type-form] "Variable of type `typing.LiteralString` is not allowed in a type expression when nested directly inside `type[]` or `Type[]`"
|
||||
b: type[Callable], # error: [invalid-type-form] "Variable of type `typing.Callable` is not allowed in a type expression when nested directly inside `type[]` or `Type[]`"
|
||||
c: type[TypeAlias], # error: [invalid-type-form] "Variable of type `typing.TypeAlias` is not allowed in a type expression when nested directly inside `type[]` or `Type[]`"
|
||||
): ...
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
## Invalid AST nodes
|
||||
|
||||
```py
|
||||
|
|
|
@ -81,9 +81,8 @@ class Shape:
|
|||
|
||||
@classmethod
|
||||
def bar(cls: type[Self]) -> Self:
|
||||
# TODO: type[Shape]
|
||||
reveal_type(cls) # revealed: @Todo(unsupported type[X] special form)
|
||||
return cls()
|
||||
reveal_type(cls) # revealed: Self'meta
|
||||
return cls() # error: [call-non-callable]
|
||||
|
||||
class Circle(Shape): ...
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None:
|
|||
def takes_in_type(x: type[T]) -> type[T]:
|
||||
return x
|
||||
|
||||
reveal_type(takes_in_type(int)) # revealed: @Todo(unsupported type[X] special form)
|
||||
reveal_type(takes_in_type(int)) # revealed: <class 'int'>
|
||||
```
|
||||
|
||||
This also works when passing in arguments that are subclasses of the parameter type.
|
||||
|
|
|
@ -101,7 +101,8 @@ def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None:
|
|||
def takes_in_type[T](x: type[T]) -> type[T]:
|
||||
return x
|
||||
|
||||
reveal_type(takes_in_type(int)) # revealed: @Todo(unsupported type[X] special form)
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(takes_in_type(int)) # revealed: T'meta
|
||||
```
|
||||
|
||||
This also works when passing in arguments that are subclasses of the parameter type.
|
||||
|
|
|
@ -230,12 +230,10 @@ And it is also an error to use `Protocol` in type expressions:
|
|||
|
||||
def f(
|
||||
x: Protocol, # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions"
|
||||
y: type[Protocol], # TODO: should emit `[invalid-type-form]` here too
|
||||
y: type[Protocol], # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions"
|
||||
):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
# TODO: should be `type[Unknown]`
|
||||
reveal_type(y) # revealed: @Todo(unsupported type[X] special form)
|
||||
reveal_type(y) # revealed: type[Unknown]
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
|
|
@ -2552,6 +2552,18 @@ impl<'db> Type<'db> {
|
|||
.find_name_in_mro_with_policy(db, name, policy)
|
||||
}
|
||||
|
||||
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
|
||||
None => KnownClass::Object
|
||||
.to_class_literal(db)
|
||||
.find_name_in_mro_with_policy(db, name, policy),
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||
bound.find_name_in_mro_with_policy(db, name, policy)
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
|
||||
Type::Union(constraints).find_name_in_mro_with_policy(db, name, policy)
|
||||
}
|
||||
},
|
||||
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::Callable(_)
|
||||
| Type::BoundMethod(_)
|
||||
|
@ -2569,7 +2581,6 @@ impl<'db> Type<'db> {
|
|||
| Type::LiteralString
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::Tuple(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::NominalInstance(_)
|
||||
| Type::ProtocolInstance(_)
|
||||
| Type::PropertyInstance(_) => None,
|
||||
|
@ -4818,10 +4829,11 @@ 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::NominalInstance(builtins.int)`, that is, all objects whose
|
||||
/// `__class__` is `int`.
|
||||
pub fn in_type_expression(
|
||||
fn in_type_expression(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
scope_id: ScopeId<'db>,
|
||||
context: TypeExpressionContext,
|
||||
) -> Result<Type<'db>, InvalidTypeExpressionError<'db>> {
|
||||
match self {
|
||||
// Special cases for `float` and `complex`
|
||||
|
@ -4881,11 +4893,20 @@ impl<'db> Type<'db> {
|
|||
Type::KnownInstance(known_instance) => match known_instance {
|
||||
KnownInstanceType::TypeAliasType(alias) => Ok(alias.value_type(db)),
|
||||
KnownInstanceType::Never | KnownInstanceType::NoReturn => Ok(Type::Never),
|
||||
KnownInstanceType::LiteralString => Ok(Type::LiteralString),
|
||||
KnownInstanceType::Unknown => Ok(Type::unknown()),
|
||||
KnownInstanceType::AlwaysTruthy => Ok(Type::AlwaysTruthy),
|
||||
KnownInstanceType::AlwaysFalsy => Ok(Type::AlwaysFalsy),
|
||||
|
||||
KnownInstanceType::LiteralString if context.in_subclass_of_expression() => {
|
||||
Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec![
|
||||
InvalidTypeExpression::InvalidTypeInContext { ty: *self, context }
|
||||
],
|
||||
fallback_type: Type::unknown(),
|
||||
})
|
||||
}
|
||||
KnownInstanceType::LiteralString => Ok(Type::LiteralString),
|
||||
|
||||
// We treat `typing.Type` exactly the same as `builtins.type`:
|
||||
KnownInstanceType::Type => Ok(KnownClass::Type.to_instance(db)),
|
||||
KnownInstanceType::Tuple => Ok(KnownClass::Tuple.to_instance(db)),
|
||||
|
@ -4903,7 +4924,14 @@ impl<'db> Type<'db> {
|
|||
|
||||
KnownInstanceType::TypeVar(typevar) => Ok(Type::TypeVar(*typevar)),
|
||||
|
||||
// TODO: Use an opt-in rule for a bare `Callable`
|
||||
KnownInstanceType::Callable if context.in_subclass_of_expression() => {
|
||||
Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec![
|
||||
InvalidTypeExpression::InvalidTypeInContext { ty: *self, context }
|
||||
],
|
||||
fallback_type: Type::unknown(),
|
||||
})
|
||||
}
|
||||
KnownInstanceType::Callable => Ok(Type::Callable(CallableType::unknown(db))),
|
||||
|
||||
KnownInstanceType::TypingSelf => {
|
||||
|
@ -4929,6 +4957,17 @@ impl<'db> Type<'db> {
|
|||
TypeVarKind::Legacy,
|
||||
)))
|
||||
}
|
||||
// TODO: invalid in most contexts (in parameters or a return type);
|
||||
// only valid in `ast::AnnAssign` nodes.
|
||||
KnownInstanceType::TypeAlias if context.in_subclass_of_expression() => {
|
||||
Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec![
|
||||
InvalidTypeExpression::InvalidTypeInContext { ty: *self, context }
|
||||
],
|
||||
fallback_type: Type::unknown(),
|
||||
})
|
||||
}
|
||||
|
||||
KnownInstanceType::TypeAlias => Ok(todo_type!("Support for `typing.TypeAlias`")),
|
||||
KnownInstanceType::TypedDict => Ok(todo_type!("Support for `typing.TypedDict`")),
|
||||
|
||||
|
@ -4995,7 +5034,7 @@ impl<'db> Type<'db> {
|
|||
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, scope_id) {
|
||||
match element.in_type_expression(db, scope_id, context) {
|
||||
Ok(type_expr) => builder = builder.add(type_expr),
|
||||
Err(InvalidTypeExpressionError {
|
||||
fallback_type,
|
||||
|
@ -5110,15 +5149,29 @@ impl<'db> Type<'db> {
|
|||
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db),
|
||||
Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db),
|
||||
|
||||
Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) {
|
||||
None => KnownClass::Object.to_class_literal(db),
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.to_meta_type(db),
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
|
||||
// TODO: If we add a proper `OneOf` connector, we should use that here instead
|
||||
// of union. (Using a union here doesn't break anything, but it is imprecise.)
|
||||
constraints.map(db, |constraint| constraint.to_meta_type(db))
|
||||
}
|
||||
},
|
||||
Type::TypeVar(typevar) => {
|
||||
let bound_or_constraints = match typevar.bound_or_constraints(db) {
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(upper_bound)) => Some(
|
||||
TypeVarBoundOrConstraints::UpperBound(upper_bound.to_meta_type(db)),
|
||||
),
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => Some(
|
||||
match constraints.map(db, |constraint| constraint.to_meta_type(db)) {
|
||||
Type::Union(union) => TypeVarBoundOrConstraints::Constraints(union),
|
||||
other_type => TypeVarBoundOrConstraints::UpperBound(other_type),
|
||||
},
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
Type::TypeVar(TypeVarInstance::new(
|
||||
db,
|
||||
Name::new(format!("{}'meta", typevar.name(db))),
|
||||
None,
|
||||
bound_or_constraints,
|
||||
typevar.variance(db),
|
||||
None,
|
||||
typevar.kind(db),
|
||||
))
|
||||
}
|
||||
|
||||
Type::ClassLiteral(class) => class.metaclass(db),
|
||||
Type::GenericAlias(alias) => ClassType::from(*alias).metaclass(db),
|
||||
|
@ -5728,6 +5781,34 @@ impl<'db> From<Type<'db>> for TypeAndQualifiers<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
|
||||
struct TypeExpressionContext: u8 {
|
||||
/// The type expression is directly nested inside `type[]` or `Type[]`.
|
||||
const SUBCLASS_OF = 1 << 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeExpressionContext {
|
||||
const fn in_subclass_of_expression(self) -> bool {
|
||||
self.contains(Self::SUBCLASS_OF)
|
||||
}
|
||||
|
||||
const fn as_str(self) -> &'static str {
|
||||
if self.in_subclass_of_expression() {
|
||||
"in a type expression when nested directly inside `type[]` or `Type[]`"
|
||||
} else {
|
||||
"at the top-level of a type expression"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TypeExpressionContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
@ -5782,6 +5863,11 @@ enum InvalidTypeExpression<'db> {
|
|||
TypeQualifierRequiresOneArgument(KnownInstanceType<'db>),
|
||||
/// Some types are always invalid in type expressions
|
||||
InvalidType(Type<'db>, ScopeId<'db>),
|
||||
/// Some types are always invalid in specific contexts in type expressions
|
||||
InvalidTypeInContext {
|
||||
ty: Type<'db>,
|
||||
context: TypeExpressionContext,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'db> InvalidTypeExpression<'db> {
|
||||
|
@ -5830,6 +5916,11 @@ impl<'db> InvalidTypeExpression<'db> {
|
|||
"Variable of type `{ty}` is not allowed in a type expression",
|
||||
ty = ty.display(self.db)
|
||||
),
|
||||
InvalidTypeExpression::InvalidTypeInContext { ty, context } => write!(
|
||||
f,
|
||||
"Variable of type `{ty}` is not allowed {context}",
|
||||
ty = ty.display(self.db),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5856,7 +5947,7 @@ impl<'db> InvalidTypeExpression<'db> {
|
|||
return;
|
||||
};
|
||||
if module_member_with_same_name
|
||||
.in_type_expression(db, scope)
|
||||
.in_type_expression(db, scope, TypeExpressionContext::empty())
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
|
|
|
@ -114,7 +114,7 @@ use super::string_annotation::{
|
|||
BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation,
|
||||
};
|
||||
use super::subclass_of::SubclassOfInner;
|
||||
use super::{BoundSuperError, BoundSuperType, ClassBase};
|
||||
use super::{BoundSuperError, BoundSuperType, ClassBase, TypeExpressionContext};
|
||||
|
||||
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
|
||||
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
|
||||
|
@ -7669,7 +7669,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
TypeAndQualifiers::new(Type::unknown(), TypeQualifiers::FINAL)
|
||||
}
|
||||
_ => name_expr_ty
|
||||
.in_type_expression(self.db(), self.scope())
|
||||
.in_type_expression(
|
||||
self.db(),
|
||||
self.scope(),
|
||||
TypeExpressionContext::empty(),
|
||||
)
|
||||
.unwrap_or_else(|error| {
|
||||
error.into_fallback_type(
|
||||
&self.context,
|
||||
|
@ -7850,7 +7854,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
ast::Expr::Name(name) => match name.ctx {
|
||||
ast::ExprContext::Load => self
|
||||
.infer_name_expression(name)
|
||||
.in_type_expression(self.db(), self.scope())
|
||||
.in_type_expression(self.db(), self.scope(), TypeExpressionContext::empty())
|
||||
.unwrap_or_else(|error| {
|
||||
error.into_fallback_type(
|
||||
&self.context,
|
||||
|
@ -7867,7 +7871,7 @@ 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(), self.scope())
|
||||
.in_type_expression(self.db(), self.scope(), TypeExpressionContext::empty())
|
||||
.unwrap_or_else(|error| {
|
||||
error.into_fallback_type(
|
||||
&self.context,
|
||||
|
@ -8263,25 +8267,13 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
/// Given the slice of a `type[]` annotation, return the type that the annotation represents
|
||||
fn infer_subclass_of_type_expression(&mut self, slice: &ast::Expr) -> Type<'db> {
|
||||
match slice {
|
||||
ast::Expr::Name(_) | ast::Expr::Attribute(_) => {
|
||||
let name_ty = self.infer_expression(slice);
|
||||
match name_ty {
|
||||
Type::ClassLiteral(class_literal) => {
|
||||
if class_literal.is_known(self.db(), KnownClass::Any) {
|
||||
SubclassOfType::subclass_of_any()
|
||||
} else {
|
||||
SubclassOfType::from(
|
||||
self.db(),
|
||||
class_literal.default_specialization(self.db()),
|
||||
)
|
||||
}
|
||||
}
|
||||
Type::KnownInstance(KnownInstanceType::Unknown) => {
|
||||
SubclassOfType::subclass_of_unknown()
|
||||
}
|
||||
_ => todo_type!("unsupported type[X] special form"),
|
||||
}
|
||||
}
|
||||
ast::Expr::Name(_) | ast::Expr::Attribute(_) => self
|
||||
.infer_expression(slice)
|
||||
.in_type_expression(self.db(), self.scope(), TypeExpressionContext::SUBCLASS_OF)
|
||||
.unwrap_or_else(|error| {
|
||||
error.into_fallback_type(&self.context, slice, self.is_reachable(slice))
|
||||
})
|
||||
.to_meta_type(self.db()),
|
||||
ast::Expr::BinOp(binary) if binary.op == ast::Operator::BitOr => {
|
||||
let union_ty = UnionType::from_elements(
|
||||
self.db(),
|
||||
|
@ -8372,7 +8364,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
generic_context,
|
||||
);
|
||||
specialized_class
|
||||
.in_type_expression(self.db(), self.scope())
|
||||
.in_type_expression(
|
||||
self.db(),
|
||||
self.scope(),
|
||||
TypeExpressionContext::empty(),
|
||||
)
|
||||
.unwrap_or(Type::unknown())
|
||||
}
|
||||
None => {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::db::tests::TestDb;
|
||||
use crate::symbol::{builtins_symbol, known_module_symbol};
|
||||
use crate::types::{
|
||||
BoundMethodType, CallableType, IntersectionBuilder, KnownClass, KnownInstanceType, Parameter,
|
||||
Parameters, Signature, SubclassOfType, TupleType, Type, UnionType,
|
||||
BoundMethodType, CallableType, DynamicType, IntersectionBuilder, KnownClass, KnownInstanceType,
|
||||
Parameter, Parameters, Signature, SubclassOfType, TupleType, Type, UnionType,
|
||||
};
|
||||
use crate::{Db, KnownModule};
|
||||
use hashbrown::HashSet;
|
||||
|
@ -162,7 +162,7 @@ impl Ty {
|
|||
let elements = tys.into_iter().map(|ty| ty.into_type(db));
|
||||
TupleType::from_elements(db, elements)
|
||||
}
|
||||
Ty::SubclassOfAny => SubclassOfType::subclass_of_any(),
|
||||
Ty::SubclassOfAny => SubclassOfType::from(db, DynamicType::Any),
|
||||
Ty::SubclassOfBuiltinClass(s) => SubclassOfType::from(
|
||||
db,
|
||||
builtins_symbol(db, s)
|
||||
|
|
|
@ -46,13 +46,6 @@ impl<'db> SubclassOfType<'db> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Return a [`Type`] instance representing the type `type[Any]`.
|
||||
pub(crate) const fn subclass_of_any() -> Type<'db> {
|
||||
Type::SubclassOf(SubclassOfType {
|
||||
subclass_of: SubclassOfInner::Dynamic(DynamicType::Any),
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the inner [`SubclassOfInner`] value wrapped by this `SubclassOfType`.
|
||||
pub(crate) const fn subclass_of(self) -> SubclassOfInner<'db> {
|
||||
self.subclass_of
|
||||
|
@ -206,6 +199,12 @@ impl<'db> From<ClassType<'db>> for SubclassOfInner<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<DynamicType> for SubclassOfInner<'_> {
|
||||
fn from(value: DynamicType) -> Self {
|
||||
SubclassOfInner::Dynamic(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<SubclassOfInner<'db>> for Type<'db> {
|
||||
fn from(value: SubclassOfInner<'db>) -> Self {
|
||||
match value {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue