Repository: astral-sh/ruff
Pull request #16850: Show more precise messages in invalid type expressions
Author: login: MatthewMckee4
Labels: name: ty
description: Multi-file analysis & type inference
Created: 2025-03-19T15:26:56Z | Merged: 2025-03-19T17:00:31Z

<!--
Thank you for contributing to Ruff! To help us out with reviewing, please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

Some error messages are not very specific and could be improved

## Test Plan

Not yet sure where to put some tests, but will just test invalid type expressions



Diff:
diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/annotated.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/annotated.md
index 71b06a24f280b..a9f7b91618380 100644
--- a/crates/red_knot_python_semantic/resources/mdtest/annotations/annotated.md
+++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/annotated.md
@@ -29,7 +29,7 @@ It is invalid to parameterize `Annotated` with less than two arguments.
 ```py
 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):
     reveal_type(x)  # revealed: Unknown
 
@@ -39,11 +39,11 @@ def _(flag: bool):
     else:
         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):
         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):
     reveal_type(x)  # revealed: Unknown | bool
 
diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md
index 435740cc42193..a798bb100fbf1 100644
--- a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md
+++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md
@@ -18,7 +18,7 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
     # TODO: should understand the annotation
     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 h() -> TypeIs[int]: ...
@@ -35,7 +35,26 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.
 
 class Foo:
     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
diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md
index 7065406d60a10..7826fbf92d1bd 100644
--- a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md
+++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md
@@ -1,6 +1,6 @@
 # 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
 false-positive errors if you use one in an annotation:
@@ -19,6 +19,33 @@ class Bar(TypedDict):
     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
 
 You can't inherit from a type qualifier.
diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs
index 2a61461f3895d..23d3212b59078 100644
--- a/crates/red_knot_python_semantic/src/types.rs
+++ b/crates/red_knot_python_semantic/src/types.rs
@@ -3182,6 +3182,7 @@ impl<'db> Type<'db> {
                 };
                 Ok(ty)
             }
+
             Type::SubclassOf(_)
             | Type::BooleanLiteral(_)
             | Type::BytesLiteral(_)
@@ -3200,30 +3201,95 @@ impl<'db> Type<'db> {
                 fallback_type: Type::unknown(),
             }),
 
-            // We treat `typing.Type` exactly the same as `builtins.type`:
-            Type::KnownInstance(KnownInstanceType::Type) => Ok(KnownClass::Type.to_instance(db)),
-            Type::KnownInstance(KnownInstanceType::Tuple) => Ok(KnownClass::Tuple.to_instance(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::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`:
+                KnownInstanceType::Type => Ok(KnownClass::Type.to_instance(db)),
+                KnownInstanceType::Tuple => Ok(KnownClass::Tuple.to_instance(db)),
+
+                // Legacy `typing` aliases
+                KnownInstanceType::List => Ok(KnownClass::List.to_instance(db)),
+                KnownInstanceType::Dict => Ok(KnownClass::Dict.to_instance(db)),
+                KnownInstanceType::Set => Ok(KnownClass::Set.to_instance(db)),
+                KnownInstanceType::FrozenSet => 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),
 
-            // Legacy `typing` aliases
-            Type::KnownInstance(KnownInstanceType::List) => Ok(KnownClass::List.to_instance(db)),
-            Type::KnownInstance(KnownInstanceType::Dict) => Ok(KnownClass::Dict.to_instance(db)),
-            Type::KnownInstance(KnownInstanceType::Set) => Ok(KnownClass::Set.to_instance(db)),
-            Type::KnownInstance(KnownInstanceType::FrozenSet) => {
-                Ok(KnownClass::FrozenSet.to_instance(db))
-            }
-            Type::KnownInstance(KnownInstanceType::ChainMap) => {
-                Ok(KnownClass::ChainMap.to_instance(db))
-            }
-            Type::KnownInstance(KnownInstanceType::Counter) => {
-                Ok(KnownClass::Counter.to_instance(db))
-            }
-            Type::KnownInstance(KnownInstanceType::DefaultDict) => {
-                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))
-            }
+                // 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(),
+                    })
+                }
+
+                KnownInstanceType::ClassVar | KnownInstanceType::Final => {
+                    Err(InvalidTypeExpressionError {
+                        invalid_expressions: smallvec::smallvec![
+                            InvalidTypeExpression::TypeQualifier(*known_instance)
+                        ],
+                        fallback_type: Type::unknown(),
+                    })
+                }
+
+                KnownInstanceType::ReadOnly
+                | KnownInstanceType::NotRequired
+                | KnownInstanceType::Required => Err(InvalidTypeExpressionError {
+                    invalid_expressions: smallvec::smallvec![
+                        InvalidTypeExpression::TypeQualifierRequiresOneArgument(*known_instance)
+                    ],
+                    fallback_type: Type::unknown(),
+                }),
+            },
 
             Type::Union(union) => {
                 let mut builder = UnionBuilder::new(db);
@@ -3249,85 +3315,13 @@ impl<'db> Type<'db> {
                     })
                 }
             }
+
             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!(
                 "Invalid or unsupported `Instance` in `Type::to_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
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
 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
     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
-    ProtocolInTypeExpression,
-    /// The `ClassVar` type qualifier was used in a type expression
-    ClassVarInTypeExpression,
-    /// The `Final` type qualifier was used in a type expression
-    FinalInTypeExpression,
+    Protocol,
+    /// Type qualifiers are always invalid in *type expressions*,
+    /// but these ones are okay with 0 arguments in *annotation expressions*
+    TypeQualifier(KnownInstanceType<'db>),
+    /// 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
     InvalidType(Type<'db>),
 }
@@ -3619,26 +3615,33 @@ impl<'db> InvalidTypeExpression<'db> {
         impl std::fmt::Display for Display<'_> {
             fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                 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!(
                         f,
                         "`{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!(
                         f,
                         "`{ty}` requires at least one argument when used in a type expression",
                         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"
                     ),
-                    InvalidTypeExpression::ClassVarInTypeExpression => f.write_str(
-                        "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
+                    InvalidTypeExpression::TypeQualifier(qualifier) => write!(
+                        f,
+                        "Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions)",
+                        q = qualifier.repr(self.db)
                     ),
-                    InvalidTypeExpression::FinalInTypeExpression => f.write_str(
-                        "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
+                    InvalidTypeExpression::TypeQualifierRequiresOneArgument(qualifier) => write!(
+                        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!(
                         f,


Reviews:
user: login: AlexWaygood
body: Looks great, thank you!
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T16:33:11Z
user: login: carljm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T16:35:34Z
user: login: MatthewMckee4
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T16:36:10Z
user: login: AlexWaygood
body: Thanks, this is great!! I pushed a commit to make the `match` a bit more readable, since it was getting a bit big and the `KnownInstance` branches were interspersed with some of the other branches
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-19T16:57:22Z

Review comments:
diff_hunk: @@ -35,7 +35,26 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.
 
 class Foo:
     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 type qualifiers in type expressions.
path: crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md
user: login: AlexWaygood
body: ```suggestion
One thing that is supported is error messages for using special forms in type expressions.
```
created_at: 2025-03-19T16:27:03Z
author_association: MEMBER
diff_hunk: @@ -35,7 +35,26 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.
 
 class Foo:
     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 type qualifiers in type expressions.
+
+```py
+from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate
path: crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md
user: login: AlexWaygood
body: ```suggestion
from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate
```
created_at: 2025-03-19T16:27:47Z
author_association: MEMBER
diff_hunk: @@ -3593,18 +3600,18 @@ impl<'db> InvalidTypeExpressionError<'db> {
 /// Enumeration of various types that are invalid in type-expression contexts
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
 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
     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
     ProtocolInTypeExpression,
-    /// The `ClassVar` type qualifier was used in a type expression
-    ClassVarInTypeExpression,
-    /// The `Final` type qualifier was used in a type expression
-    FinalInTypeExpression,
+    /// Type qualifiers are always invalid in type expressions
path: crates/red_knot_python_semantic/src/types.rs
user: login: AlexWaygood
body: ```suggestion
    /// Type qualifiers are always invalid in *type expressions*,
    /// but these ones are okay with 0 arguments in *annotation expressions*
```
created_at: 2025-03-19T16:31:06Z
author_association: MEMBER
diff_hunk: @@ -3593,18 +3600,18 @@ impl<'db> InvalidTypeExpressionError<'db> {
 /// Enumeration of various types that are invalid in type-expression contexts
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
 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
     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
     ProtocolInTypeExpression,
-    /// The `ClassVar` type qualifier was used in a type expression
-    ClassVarInTypeExpression,
-    /// The `Final` type qualifier was used in a type expression
-    FinalInTypeExpression,
+    /// Type qualifiers are always invalid in type expressions
+    TypeQualifier(Type<'db>),
+    /// Type qualifiers require exactly one argument and are invalid in type expressions
path: crates/red_knot_python_semantic/src/types.rs
user: login: AlexWaygood
body: ```suggestion
    /// Type qualifiers that are invalid in type expressions,
    /// and which would require exactly one argument even if they appeared in an annotation expression
```
created_at: 2025-03-19T16:32:11Z
author_association: MEMBER
diff_hunk: @@ -3593,18 +3600,18 @@ impl<'db> InvalidTypeExpressionError<'db> {
 /// Enumeration of various types that are invalid in type-expression contexts
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
 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
     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
     ProtocolInTypeExpression,
path: crates/red_knot_python_semantic/src/types.rs
user: login: carljm
body: Since this PR is removing `ClassVarInTypeExpression` and `FinalInTypeExpression`, can we also rename `ProtocolInTypeExpression` to just `Protocol`?
created_at: 2025-03-19T16:35:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3593,18 +3600,18 @@ impl<'db> InvalidTypeExpressionError<'db> {
 /// Enumeration of various types that are invalid in type-expression contexts
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
 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
     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
     ProtocolInTypeExpression,
path: crates/red_knot_python_semantic/src/types.rs
user: login: MatthewMckee4
body: Yeah
created_at: 2025-03-19T16:36:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: MatthewMckee4
created_at: 2025-03-19T15:29:58Z
body: Im not fully sure where to put tests for the currently unsupported special forms and type qualifiers
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-19T15:34:33Z
body: <!-- generated-comment mypy_primer -->
## `mypy_primer` results
No ecosystem changes detected ✅

author_association: CONTRIBUTOR
user: login: AlexWaygood
created_at: 2025-03-19T15:46:07Z
body: > Im not fully sure where to put tests for the currently unsupported special forms and type qualifiers

maybe these files?
- https://github.com/astral-sh/ruff/blob/main/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_type_qualifiers.md
- https://github.com/astral-sh/ruff/blob/main/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md
author_association: MEMBER
user: login: MatthewMckee4
created_at: 2025-03-19T15:47:42Z
body: Yeah, i thought that, but we does providing error messages not imply that we are supporting them partially?
author_association: CONTRIBUTOR
user: login: AlexWaygood
created_at: 2025-03-19T15:49:40Z
body: True. We'll need comprehensive test suites covering each special form eventually, so you can create new files for each now if you like! But equally, I don't think it matters too much right now; I'd be inclined to just adjust the prose a little bit in those files saying that we don't fully support them, but we do detect some invalid uses when they appear in type expressions.
author_association: MEMBER
user: login: MatthewMckee4
created_at: 2025-03-19T15:50:42Z
body: Okay, will do that then, thanks
author_association: CONTRIBUTOR
user: login: MatthewMckee4
created_at: 2025-03-19T16:08:09Z
body: Tests failing because
```python
def _(
    a: Final
) -> None: ...
```
raises no error

Ill change it to Final | int.

@AlexWaygood why does this happen exactly?
author_association: CONTRIBUTOR
user: login: AlexWaygood
created_at: 2025-03-19T16:14:35Z
body: > @AlexWaygood why does this happen exactly?

That's because we currently only validate that parameter annotations are valid annotation expressions. Bare `Final` _is_ a valid _annotation expression_, even though it's not a valid _type expression_.

You can take a look at the typing spec page here for an explanation of the difference between the two concepts: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions. `Final` is a valid annotation expression but not a valid type expression; `Final | int` is neither a valid annotation expression nor a valid type expression.

Now, is `Final` a valid parameter annotation? No! 😄 But the reason it's invalid isn't relevant to the changes you're making here, and we shouldn't attempt to detect the invalidity as part of the `Type::in_type_expression()` method. At some point, we'll need to add some extra validation specifically for parameter annotations that checks exactly whether the annotation expression is one of the expressions that are disallowed in the specific context of parameter annotations. But we haven't implemented that additional check yet :-)
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-03-19T16:18:16Z
body: <!-- generated-comment ecosystem -->
## `ruff-ecosystem` results
### Linter (stable)
✅ ecosystem check detected no linter changes.

### Linter (preview)
✅ ecosystem check detected no linter changes.



author_association: CONTRIBUTOR
user: login: MatthewMckee4
created_at: 2025-03-19T16:19:12Z
body: Okay understood. What function infers the type of parameter annotations?
author_association: CONTRIBUTOR
user: login: carljm
created_at: 2025-03-19T16:33:01Z
body: > What function infers the type of parameter annotations?

`TypeInferenceBuilder::infer_parameter` and `TypeInferenceBuilder::infer_parameter_with_default` both call into `TypeInferenceBuilder::infer_optional_annotation_expression` to infer the annotated type.
author_association: CONTRIBUTOR