[red-knot] Ban most Type::Instance types in type expressions (#16872)

## Summary

Catch some Instances, but raise type error for the rest of them
Fixes #16851 

## Test Plan

Extend invalid.md in annotations

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Matthew Mckee 2025-03-20 22:19:56 +00:00 committed by GitHub
parent 296d67a496
commit 63e78b41cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 144 additions and 69 deletions

View file

@ -9,6 +9,8 @@ import typing
from knot_extensions import AlwaysTruthy, AlwaysFalsy
from typing_extensions import Literal, Never
class A: ...
def _(
a: type[int],
b: AlwaysTruthy,
@ -18,30 +20,34 @@ def _(
f: Literal[b"foo"],
g: tuple[int, str],
h: Never,
i: int,
j: A,
):
def foo(): ...
def invalid(
i: a, # error: [invalid-type-form] "Variable of type `type[int]` is not allowed in a type expression"
j: b, # error: [invalid-type-form]
k: c, # error: [invalid-type-form]
l: d, # error: [invalid-type-form]
m: e, # error: [invalid-type-form]
n: f, # error: [invalid-type-form]
o: g, # error: [invalid-type-form]
p: h, # error: [invalid-type-form]
q: typing, # error: [invalid-type-form]
r: foo, # error: [invalid-type-form]
a_: a, # error: [invalid-type-form] "Variable of type `type[int]` is not allowed in a type expression"
b_: b, # error: [invalid-type-form]
c_: c, # error: [invalid-type-form]
d_: d, # error: [invalid-type-form]
e_: e, # error: [invalid-type-form]
f_: f, # error: [invalid-type-form]
g_: g, # error: [invalid-type-form]
h_: h, # error: [invalid-type-form]
i_: typing, # error: [invalid-type-form]
j_: foo, # error: [invalid-type-form]
k_: i, # error: [invalid-type-form] "Variable of type `int` is not allowed in a type expression"
l_: j, # error: [invalid-type-form] "Variable of type `A` is not allowed in a type expression"
):
reveal_type(i) # revealed: Unknown
reveal_type(j) # revealed: Unknown
reveal_type(k) # revealed: Unknown
reveal_type(l) # revealed: Unknown
reveal_type(m) # revealed: Unknown
reveal_type(n) # revealed: Unknown
reveal_type(o) # revealed: Unknown
reveal_type(p) # revealed: Unknown
reveal_type(q) # revealed: Unknown
reveal_type(r) # revealed: Unknown
reveal_type(a_) # revealed: Unknown
reveal_type(b_) # revealed: Unknown
reveal_type(c_) # revealed: Unknown
reveal_type(d_) # revealed: Unknown
reveal_type(e_) # revealed: Unknown
reveal_type(f_) # revealed: Unknown
reveal_type(g_) # revealed: Unknown
reveal_type(h_) # revealed: Unknown
reveal_type(i_) # revealed: Unknown
reveal_type(j_) # revealed: Unknown
```
## Invalid AST nodes

View file

@ -0,0 +1,20 @@
# NewType
Currently, red-knot doesn't support `typing.NewType` in type annotations.
## Valid forms
```py
from typing_extensions import NewType
from types import GenericAlias
A = NewType("A", int)
B = GenericAlias(A, ())
def _(
a: A,
b: B,
):
reveal_type(a) # revealed: @Todo(Support for `typing.NewType` instances in type expressions)
reveal_type(b) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions)
```

View file

@ -43,18 +43,22 @@ class Foo:
One thing that is supported is error messages for using special forms in type expressions.
```py
from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate
from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec
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"
e: ParamSpec,
) -> None:
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown
def foo(a_: e) -> None:
reveal_type(a_) # revealed: @Todo(Support for `typing.ParamSpec` instances in type expressions)
```
## Inheritance

View file

@ -113,7 +113,7 @@ class Legacy(Generic[T]):
legacy: Legacy[int] = Legacy()
# TODO: revealed: str
reveal_type(legacy.m(1, "string")) # revealed: @Todo(Invalid or unsupported `Instance` in `Type::to_type_expression`)
reveal_type(legacy.m(1, "string")) # revealed: @Todo(Support for `typing.TypeVar` instances in type expressions)
```
With PEP 695 syntax, it is clearer that the method uses a separate typevar:

View file

@ -3318,9 +3318,29 @@ impl<'db> Type<'db> {
Type::Dynamic(_) => Ok(*self),
Type::Instance(_) => Ok(todo_type!(
"Invalid or unsupported `Instance` in `Type::to_type_expression`"
Type::Instance(InstanceType { class }) => match class.known(db) {
Some(KnownClass::TypeVar) => Ok(todo_type!(
"Support for `typing.TypeVar` instances in type expressions"
)),
Some(KnownClass::ParamSpec) => Ok(todo_type!(
"Support for `typing.ParamSpec` instances in type expressions"
)),
Some(KnownClass::TypeVarTuple) => Ok(todo_type!(
"Support for `typing.TypeVarTuple` instances in type expressions"
)),
Some(KnownClass::NewType) => Ok(todo_type!(
"Support for `typing.NewType` instances in type expressions"
)),
Some(KnownClass::GenericAlias) => Ok(todo_type!(
"Support for `typing.GenericAlias` instances in type expressions"
)),
_ => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(
*self
)],
fallback_type: Type::unknown(),
}),
},
Type::Intersection(_) => Ok(todo_type!("Type::Intersection.in_type_expression")),
}

View file

@ -829,8 +829,11 @@ pub enum KnownClass {
StdlibAlias,
SpecialForm,
TypeVar,
ParamSpec,
TypeVarTuple,
TypeAliasType,
NoDefaultType,
NewType,
// TODO: This can probably be removed when we have support for protocols
SupportsIndex,
// Collections
@ -873,6 +876,8 @@ impl<'db> KnownClass {
| Self::VersionInfo
| Self::TypeAliasType
| Self::TypeVar
| Self::ParamSpec
| Self::TypeVarTuple
| Self::WrapperDescriptorType
| Self::MethodWrapperType => Truthiness::AlwaysTrue,
@ -886,6 +891,7 @@ impl<'db> KnownClass {
| Self::Str
| Self::List
| Self::GenericAlias
| Self::NewType
| Self::StdlibAlias
| Self::SupportsIndex
| Self::Set
@ -939,8 +945,11 @@ impl<'db> KnownClass {
Self::NoneType => "NoneType",
Self::SpecialForm => "_SpecialForm",
Self::TypeVar => "TypeVar",
Self::ParamSpec => "ParamSpec",
Self::TypeVarTuple => "TypeVarTuple",
Self::TypeAliasType => "TypeAliasType",
Self::NoDefaultType => "_NoDefaultType",
Self::NewType => "NewType",
Self::SupportsIndex => "SupportsIndex",
Self::ChainMap => "ChainMap",
Self::Counter => "Counter",
@ -1109,7 +1118,9 @@ impl<'db> KnownClass {
Self::SpecialForm | Self::TypeVar | Self::StdlibAlias | Self::SupportsIndex => {
KnownModule::Typing
}
Self::TypeAliasType => KnownModule::TypingExtensions,
Self::TypeAliasType | Self::TypeVarTuple | Self::ParamSpec | Self::NewType => {
KnownModule::TypingExtensions
}
Self::NoDefaultType => {
let python_version = Program::get(db).python_version(db);
@ -1142,46 +1153,49 @@ impl<'db> KnownClass {
/// Return true if all instances of this `KnownClass` compare equal.
pub(super) const fn is_single_valued(self) -> bool {
match self {
KnownClass::NoneType
| KnownClass::NoDefaultType
| KnownClass::VersionInfo
| KnownClass::EllipsisType
| KnownClass::TypeAliasType => true,
Self::NoneType
| Self::NoDefaultType
| Self::VersionInfo
| Self::EllipsisType
| Self::TypeAliasType => true,
KnownClass::Bool
| KnownClass::Object
| KnownClass::Bytes
| KnownClass::Type
| KnownClass::Int
| KnownClass::Float
| KnownClass::Complex
| KnownClass::Str
| KnownClass::List
| KnownClass::Tuple
| KnownClass::Set
| KnownClass::FrozenSet
| KnownClass::Dict
| KnownClass::Slice
| KnownClass::Range
| KnownClass::Property
| KnownClass::BaseException
| KnownClass::BaseExceptionGroup
| KnownClass::Classmethod
| KnownClass::GenericAlias
| KnownClass::ModuleType
| KnownClass::FunctionType
| KnownClass::MethodType
| KnownClass::MethodWrapperType
| KnownClass::WrapperDescriptorType
| KnownClass::SpecialForm
| KnownClass::ChainMap
| KnownClass::Counter
| KnownClass::DefaultDict
| KnownClass::Deque
| KnownClass::OrderedDict
| KnownClass::SupportsIndex
| KnownClass::StdlibAlias
| KnownClass::TypeVar => false,
Self::Bool
| Self::Object
| Self::Bytes
| Self::Type
| Self::Int
| Self::Float
| Self::Complex
| Self::Str
| Self::List
| Self::Tuple
| Self::Set
| Self::FrozenSet
| Self::Dict
| Self::Slice
| Self::Range
| Self::Property
| Self::BaseException
| Self::BaseExceptionGroup
| Self::Classmethod
| Self::GenericAlias
| Self::ModuleType
| Self::FunctionType
| Self::MethodType
| Self::MethodWrapperType
| Self::WrapperDescriptorType
| Self::SpecialForm
| Self::ChainMap
| Self::Counter
| Self::DefaultDict
| Self::Deque
| Self::OrderedDict
| Self::SupportsIndex
| Self::StdlibAlias
| Self::TypeVar
| Self::ParamSpec
| Self::TypeVarTuple
| Self::NewType => false,
}
}
@ -1230,7 +1244,10 @@ impl<'db> KnownClass {
| Self::BaseException
| Self::BaseExceptionGroup
| Self::Classmethod
| Self::TypeVar => false,
| Self::TypeVar
| Self::ParamSpec
| Self::TypeVarTuple
| Self::NewType => false,
}
}
@ -1268,8 +1285,11 @@ impl<'db> KnownClass {
"MethodType" => Self::MethodType,
"MethodWrapperType" => Self::MethodWrapperType,
"WrapperDescriptorType" => Self::WrapperDescriptorType,
"NewType" => Self::NewType,
"TypeAliasType" => Self::TypeAliasType,
"TypeVar" => Self::TypeVar,
"ParamSpec" => Self::ParamSpec,
"TypeVarTuple" => Self::TypeVarTuple,
"ChainMap" => Self::ChainMap,
"Counter" => Self::Counter,
"defaultdict" => Self::DefaultDict,
@ -1331,9 +1351,14 @@ impl<'db> KnownClass {
| Self::MethodWrapperType
| Self::WrapperDescriptorType => module == self.canonical_module(db),
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType | Self::SupportsIndex => {
matches!(module, KnownModule::Typing | KnownModule::TypingExtensions)
}
Self::SpecialForm
| Self::TypeVar
| Self::TypeAliasType
| Self::NoDefaultType
| Self::SupportsIndex
| Self::ParamSpec
| Self::TypeVarTuple
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
}
}
}