[red-knot] Stricter parsing for type[] type expressions

This commit is contained in:
Alex Waygood 2025-04-23 18:40:12 +01:00
parent cb04343b3b
commit b1b4f8c272
9 changed files with 158 additions and 58 deletions

View file

@ -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

View file

@ -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): ...

View file

@ -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.

View file

@ -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.

View file

@ -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
```

View file

@ -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;

View file

@ -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 => {

View file

@ -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)

View file

@ -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 {