Show more precise messages in invalid type expressions (#16850)

## Summary

Some error messages were not very specific; this PR improves them

## Test Plan

New mdtests added; existing mdtests tweaked
This commit is contained in:
Matthew Mckee 2025-03-19 17:00:30 +00:00 committed by GitHub
parent 98fdc0ebae
commit 4ed93b4311
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 171 additions and 122 deletions

View file

@ -29,7 +29,7 @@ It is invalid to parameterize `Annotated` with less than two arguments.
```py ```py
from typing_extensions import Annotated from typing_extensions import Annotated
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression" # error: [invalid-type-form] "`typing.Annotated` requires at least two arguments when used in a type expression"
def _(x: Annotated): def _(x: Annotated):
reveal_type(x) # revealed: Unknown reveal_type(x) # revealed: Unknown
@ -39,11 +39,11 @@ def _(flag: bool):
else: else:
X = bool X = bool
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression" # error: [invalid-type-form] "`typing.Annotated` requires at least two arguments when used in a type expression"
def f(y: X): def f(y: X):
reveal_type(y) # revealed: Unknown | bool reveal_type(y) # revealed: Unknown | bool
# error: [invalid-type-form] "`Annotated` requires at least two arguments when used in an annotation or type expression" # error: [invalid-type-form] "`typing.Annotated` requires at least two arguments when used in a type expression"
def _(x: Annotated | bool): def _(x: Annotated | bool):
reveal_type(x) # revealed: Unknown | bool reveal_type(x) # revealed: Unknown | bool

View file

@ -18,7 +18,7 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
# TODO: should understand the annotation # TODO: should understand the annotation
reveal_type(args) # revealed: tuple reveal_type(args) # revealed: tuple
reveal_type(Alias) # revealed: @Todo(Invalid or unsupported `KnownInstanceType` in `Type::to_type_expression`) reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
def g() -> TypeGuard[int]: ... def g() -> TypeGuard[int]: ...
def h() -> TypeIs[int]: ... def h() -> TypeIs[int]: ...
@ -35,7 +35,26 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.
class Foo: class Foo:
def method(self, x: Self): def method(self, x: Self):
reveal_type(x) # revealed: @Todo(Invalid or unsupported `KnownInstanceType` in `Type::to_type_expression`) reveal_type(x) # revealed: @Todo(Support for `typing.Self`)
```
## Type expressions
One thing that is supported is error messages for using special forms in type expressions.
```py
from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate
def _(
a: Unpack, # error: [invalid-type-form] "`typing.Unpack` requires exactly one argument when used in a type expression"
b: TypeGuard, # error: [invalid-type-form] "`typing.TypeGuard` requires exactly one argument when used in a type expression"
c: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression"
d: Concatenate, # error: [invalid-type-form] "`typing.Concatenate` requires at least two arguments when used in a type expression"
) -> None:
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
``` ```
## Inheritance ## Inheritance

View file

@ -1,6 +1,6 @@
# Unsupported type qualifiers # Unsupported type qualifiers
## Not yet supported ## Not yet fully supported
Several type qualifiers are unsupported by red-knot currently. However, we also don't emit Several type qualifiers are unsupported by red-knot currently. However, we also don't emit
false-positive errors if you use one in an annotation: false-positive errors if you use one in an annotation:
@ -19,6 +19,33 @@ class Bar(TypedDict):
z: ReadOnly[bytes] z: ReadOnly[bytes]
``` ```
## Type expressions
One thing that is supported is error messages for using type qualifiers in type expressions.
```py
from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly
def _(
a: (
Final # error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
| int
),
b: (
ClassVar # error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
| int
),
c: Required, # error: [invalid-type-form] "Type qualifier `typing.Required` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
d: NotRequired, # error: [invalid-type-form] "Type qualifier `typing.NotRequired` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
e: ReadOnly, # error: [invalid-type-form] "Type qualifier `typing.ReadOnly` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
) -> None:
reveal_type(a) # revealed: Unknown | int
reveal_type(b) # revealed: Unknown | int
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
reveal_type(e) # revealed: Unknown
```
## Inheritance ## Inheritance
You can't inherit from a type qualifier. You can't inherit from a type qualifier.

View file

@ -3182,6 +3182,7 @@ impl<'db> Type<'db> {
}; };
Ok(ty) Ok(ty)
} }
Type::SubclassOf(_) Type::SubclassOf(_)
| Type::BooleanLiteral(_) | Type::BooleanLiteral(_)
| Type::BytesLiteral(_) | Type::BytesLiteral(_)
@ -3200,31 +3201,96 @@ impl<'db> Type<'db> {
fallback_type: Type::unknown(), fallback_type: Type::unknown(),
}), }),
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::Any => Ok(Type::any()),
KnownInstanceType::Unknown => Ok(Type::unknown()),
KnownInstanceType::AlwaysTruthy => Ok(Type::AlwaysTruthy),
KnownInstanceType::AlwaysFalsy => Ok(Type::AlwaysFalsy),
// We treat `typing.Type` exactly the same as `builtins.type`: // We treat `typing.Type` exactly the same as `builtins.type`:
Type::KnownInstance(KnownInstanceType::Type) => Ok(KnownClass::Type.to_instance(db)), KnownInstanceType::Type => Ok(KnownClass::Type.to_instance(db)),
Type::KnownInstance(KnownInstanceType::Tuple) => Ok(KnownClass::Tuple.to_instance(db)), KnownInstanceType::Tuple => Ok(KnownClass::Tuple.to_instance(db)),
// Legacy `typing` aliases // Legacy `typing` aliases
Type::KnownInstance(KnownInstanceType::List) => Ok(KnownClass::List.to_instance(db)), KnownInstanceType::List => Ok(KnownClass::List.to_instance(db)),
Type::KnownInstance(KnownInstanceType::Dict) => Ok(KnownClass::Dict.to_instance(db)), KnownInstanceType::Dict => Ok(KnownClass::Dict.to_instance(db)),
Type::KnownInstance(KnownInstanceType::Set) => Ok(KnownClass::Set.to_instance(db)), KnownInstanceType::Set => Ok(KnownClass::Set.to_instance(db)),
Type::KnownInstance(KnownInstanceType::FrozenSet) => { KnownInstanceType::FrozenSet => Ok(KnownClass::FrozenSet.to_instance(db)),
Ok(KnownClass::FrozenSet.to_instance(db)) KnownInstanceType::ChainMap => Ok(KnownClass::ChainMap.to_instance(db)),
KnownInstanceType::Counter => Ok(KnownClass::Counter.to_instance(db)),
KnownInstanceType::DefaultDict => Ok(KnownClass::DefaultDict.to_instance(db)),
KnownInstanceType::Deque => Ok(KnownClass::Deque.to_instance(db)),
KnownInstanceType::OrderedDict => Ok(KnownClass::OrderedDict.to_instance(db)),
// TODO map this to a new `Type::TypeVar` variant
KnownInstanceType::TypeVar(_) => Ok(*self),
// TODO: Use an opt-in rule for a bare `Callable`
KnownInstanceType::Callable => Ok(Type::Callable(CallableType::General(
GeneralCallableType::unknown(db),
))),
KnownInstanceType::TypingSelf => Ok(todo_type!("Support for `typing.Self`")),
KnownInstanceType::TypeAlias => Ok(todo_type!("Support for `typing.TypeAlias`")),
KnownInstanceType::Protocol => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Protocol],
fallback_type: Type::unknown(),
}),
KnownInstanceType::Literal
| KnownInstanceType::Union
| KnownInstanceType::Intersection => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![
InvalidTypeExpression::RequiresArguments(*self)
],
fallback_type: Type::unknown(),
}),
KnownInstanceType::Optional
| KnownInstanceType::Not
| KnownInstanceType::TypeOf
| KnownInstanceType::TypeIs
| KnownInstanceType::TypeGuard
| KnownInstanceType::Unpack
| KnownInstanceType::CallableTypeFromFunction => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![
InvalidTypeExpression::RequiresOneArgument(*self)
],
fallback_type: Type::unknown(),
}),
KnownInstanceType::Annotated | KnownInstanceType::Concatenate => {
Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![
InvalidTypeExpression::RequiresTwoArguments(*self)
],
fallback_type: Type::unknown(),
})
} }
Type::KnownInstance(KnownInstanceType::ChainMap) => {
Ok(KnownClass::ChainMap.to_instance(db)) KnownInstanceType::ClassVar | KnownInstanceType::Final => {
} Err(InvalidTypeExpressionError {
Type::KnownInstance(KnownInstanceType::Counter) => { invalid_expressions: smallvec::smallvec![
Ok(KnownClass::Counter.to_instance(db)) InvalidTypeExpression::TypeQualifier(*known_instance)
} ],
Type::KnownInstance(KnownInstanceType::DefaultDict) => { fallback_type: Type::unknown(),
Ok(KnownClass::DefaultDict.to_instance(db)) })
}
Type::KnownInstance(KnownInstanceType::Deque) => Ok(KnownClass::Deque.to_instance(db)),
Type::KnownInstance(KnownInstanceType::OrderedDict) => {
Ok(KnownClass::OrderedDict.to_instance(db))
} }
KnownInstanceType::ReadOnly
| KnownInstanceType::NotRequired
| KnownInstanceType::Required => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![
InvalidTypeExpression::TypeQualifierRequiresOneArgument(*known_instance)
],
fallback_type: Type::unknown(),
}),
},
Type::Union(union) => { Type::Union(union) => {
let mut builder = UnionBuilder::new(db); let mut builder = UnionBuilder::new(db);
let mut invalid_expressions = smallvec::SmallVec::default(); let mut invalid_expressions = smallvec::SmallVec::default();
@ -3249,85 +3315,13 @@ impl<'db> Type<'db> {
}) })
} }
} }
Type::Dynamic(_) => Ok(*self), Type::Dynamic(_) => Ok(*self),
// TODO map this to a new `Type::TypeVar` variant
Type::KnownInstance(KnownInstanceType::TypeVar(_)) => Ok(*self),
Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => {
Ok(alias.value_type(db))
}
Type::KnownInstance(KnownInstanceType::Never | KnownInstanceType::NoReturn) => {
Ok(Type::Never)
}
Type::KnownInstance(KnownInstanceType::LiteralString) => Ok(Type::LiteralString),
Type::KnownInstance(KnownInstanceType::Any) => Ok(Type::any()),
Type::KnownInstance(KnownInstanceType::Annotated) => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::BareAnnotated],
fallback_type: Type::unknown(),
}),
Type::KnownInstance(KnownInstanceType::ClassVar) => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![
InvalidTypeExpression::ClassVarInTypeExpression
],
fallback_type: Type::unknown(),
}),
Type::KnownInstance(KnownInstanceType::Final) => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![
InvalidTypeExpression::FinalInTypeExpression
],
fallback_type: Type::unknown(),
}),
Type::KnownInstance(KnownInstanceType::Unknown) => Ok(Type::unknown()),
Type::KnownInstance(KnownInstanceType::AlwaysTruthy) => Ok(Type::AlwaysTruthy),
Type::KnownInstance(KnownInstanceType::AlwaysFalsy) => Ok(Type::AlwaysFalsy),
Type::KnownInstance(KnownInstanceType::Callable) => {
// TODO: Use an opt-in rule for a bare `Callable`
Ok(Type::Callable(CallableType::General(
GeneralCallableType::unknown(db),
)))
}
Type::KnownInstance(
KnownInstanceType::Literal
| KnownInstanceType::Union
| KnownInstanceType::Intersection,
) => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::RequiresArguments(
*self
)],
fallback_type: Type::unknown(),
}),
Type::KnownInstance(
KnownInstanceType::Optional
| KnownInstanceType::Not
| KnownInstanceType::TypeOf
| KnownInstanceType::CallableTypeFromFunction,
) => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![
InvalidTypeExpression::RequiresOneArgument(*self)
],
fallback_type: Type::unknown(),
}),
Type::KnownInstance(KnownInstanceType::Protocol) => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![
InvalidTypeExpression::ProtocolInTypeExpression
],
fallback_type: Type::unknown(),
}),
Type::KnownInstance(
KnownInstanceType::TypingSelf
| KnownInstanceType::ReadOnly
| KnownInstanceType::TypeAlias
| KnownInstanceType::NotRequired
| KnownInstanceType::Concatenate
| KnownInstanceType::TypeIs
| KnownInstanceType::TypeGuard
| KnownInstanceType::Unpack
| KnownInstanceType::Required,
) => Ok(todo_type!(
"Invalid or unsupported `KnownInstanceType` in `Type::to_type_expression`"
)),
Type::Instance(_) => Ok(todo_type!( Type::Instance(_) => Ok(todo_type!(
"Invalid or unsupported `Instance` in `Type::to_type_expression`" "Invalid or unsupported `Instance` in `Type::to_type_expression`"
)), )),
Type::Intersection(_) => Ok(todo_type!("Type::Intersection.in_type_expression")), Type::Intersection(_) => Ok(todo_type!("Type::Intersection.in_type_expression")),
} }
} }
@ -3593,18 +3587,20 @@ impl<'db> InvalidTypeExpressionError<'db> {
/// Enumeration of various types that are invalid in type-expression contexts /// Enumeration of various types that are invalid in type-expression contexts
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum InvalidTypeExpression<'db> { enum InvalidTypeExpression<'db> {
/// `x: Annotated` is invalid as an annotation
BareAnnotated,
/// Some types always require at least one argument when used in a type expression
RequiresArguments(Type<'db>),
/// Some types always require exactly one argument when used in a type expression /// Some types always require exactly one argument when used in a type expression
RequiresOneArgument(Type<'db>), RequiresOneArgument(Type<'db>),
/// Some types always require at least one argument when used in a type expression
RequiresArguments(Type<'db>),
/// Some types always require at least two arguments when used in a type expression
RequiresTwoArguments(Type<'db>),
/// The `Protocol` type is invalid in type expressions /// The `Protocol` type is invalid in type expressions
ProtocolInTypeExpression, Protocol,
/// The `ClassVar` type qualifier was used in a type expression /// Type qualifiers are always invalid in *type expressions*,
ClassVarInTypeExpression, /// but these ones are okay with 0 arguments in *annotation expressions*
/// The `Final` type qualifier was used in a type expression TypeQualifier(KnownInstanceType<'db>),
FinalInTypeExpression, /// Type qualifiers that are invalid in type expressions,
/// and which would require exactly one argument even if they appeared in an annotation expression
TypeQualifierRequiresOneArgument(KnownInstanceType<'db>),
/// Some types are always invalid in type expressions /// Some types are always invalid in type expressions
InvalidType(Type<'db>), InvalidType(Type<'db>),
} }
@ -3619,26 +3615,33 @@ impl<'db> InvalidTypeExpression<'db> {
impl std::fmt::Display for Display<'_> { impl std::fmt::Display for Display<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.error { match self.error {
InvalidTypeExpression::BareAnnotated => f.write_str(
"`Annotated` requires at least two arguments when used in an annotation or type expression"
),
InvalidTypeExpression::RequiresOneArgument(ty) => write!( InvalidTypeExpression::RequiresOneArgument(ty) => write!(
f, f,
"`{ty}` requires exactly one argument when used in a type expression", "`{ty}` requires exactly one argument when used in a type expression",
ty = ty.display(self.db)), ty = ty.display(self.db)
),
InvalidTypeExpression::RequiresArguments(ty) => write!( InvalidTypeExpression::RequiresArguments(ty) => write!(
f, f,
"`{ty}` requires at least one argument when used in a type expression", "`{ty}` requires at least one argument when used in a type expression",
ty = ty.display(self.db) ty = ty.display(self.db)
), ),
InvalidTypeExpression::ProtocolInTypeExpression => f.write_str( InvalidTypeExpression::RequiresTwoArguments(ty) => write!(
f,
"`{ty}` requires at least two arguments when used in a type expression",
ty = ty.display(self.db)
),
InvalidTypeExpression::Protocol => f.write_str(
"`typing.Protocol` is not allowed in type expressions" "`typing.Protocol` is not allowed in type expressions"
), ),
InvalidTypeExpression::ClassVarInTypeExpression => f.write_str( InvalidTypeExpression::TypeQualifier(qualifier) => write!(
"Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)" f,
"Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions)",
q = qualifier.repr(self.db)
), ),
InvalidTypeExpression::FinalInTypeExpression => f.write_str( InvalidTypeExpression::TypeQualifierRequiresOneArgument(qualifier) => write!(
"Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)" f,
"Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)",
q = qualifier.repr(self.db)
), ),
InvalidTypeExpression::InvalidType(ty) => write!( InvalidTypeExpression::InvalidType(ty) => write!(
f, f,