[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 knot_extensions import AlwaysTruthy, AlwaysFalsy
from typing_extensions import Literal, Never from typing_extensions import Literal, Never
class A: ...
def _( def _(
a: type[int], a: type[int],
b: AlwaysTruthy, b: AlwaysTruthy,
@ -18,30 +20,34 @@ def _(
f: Literal[b"foo"], f: Literal[b"foo"],
g: tuple[int, str], g: tuple[int, str],
h: Never, h: Never,
i: int,
j: A,
): ):
def foo(): ... def foo(): ...
def invalid( def invalid(
i: a, # error: [invalid-type-form] "Variable of type `type[int]` is not allowed in a type expression" a_: a, # error: [invalid-type-form] "Variable of type `type[int]` is not allowed in a type expression"
j: b, # error: [invalid-type-form] b_: b, # error: [invalid-type-form]
k: c, # error: [invalid-type-form] c_: c, # error: [invalid-type-form]
l: d, # error: [invalid-type-form] d_: d, # error: [invalid-type-form]
m: e, # error: [invalid-type-form] e_: e, # error: [invalid-type-form]
n: f, # error: [invalid-type-form] f_: f, # error: [invalid-type-form]
o: g, # error: [invalid-type-form] g_: g, # error: [invalid-type-form]
p: h, # error: [invalid-type-form] h_: h, # error: [invalid-type-form]
q: typing, # error: [invalid-type-form] i_: typing, # error: [invalid-type-form]
r: foo, # 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(a_) # revealed: Unknown
reveal_type(j) # revealed: Unknown reveal_type(b_) # revealed: Unknown
reveal_type(k) # revealed: Unknown reveal_type(c_) # revealed: Unknown
reveal_type(l) # revealed: Unknown reveal_type(d_) # revealed: Unknown
reveal_type(m) # revealed: Unknown reveal_type(e_) # revealed: Unknown
reveal_type(n) # revealed: Unknown reveal_type(f_) # revealed: Unknown
reveal_type(o) # revealed: Unknown reveal_type(g_) # revealed: Unknown
reveal_type(p) # revealed: Unknown reveal_type(h_) # revealed: Unknown
reveal_type(q) # revealed: Unknown reveal_type(i_) # revealed: Unknown
reveal_type(r) # revealed: Unknown reveal_type(j_) # revealed: Unknown
``` ```
## Invalid AST nodes ## 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. One thing that is supported is error messages for using special forms in type expressions.
```py ```py
from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec
def _( def _(
a: Unpack, # error: [invalid-type-form] "`typing.Unpack` requires exactly one argument when used in a type expression" 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" 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" 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" d: Concatenate, # error: [invalid-type-form] "`typing.Concatenate` requires at least two arguments when used in a type expression"
e: ParamSpec,
) -> None: ) -> None:
reveal_type(a) # revealed: Unknown reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown reveal_type(b) # revealed: Unknown
reveal_type(c) # revealed: Unknown reveal_type(c) # revealed: Unknown
reveal_type(d) # 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 ## Inheritance

View file

@ -113,7 +113,7 @@ class Legacy(Generic[T]):
legacy: Legacy[int] = Legacy() legacy: Legacy[int] = Legacy()
# TODO: revealed: str # 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: 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::Dynamic(_) => Ok(*self),
Type::Instance(_) => Ok(todo_type!( Type::Instance(InstanceType { class }) => match class.known(db) {
"Invalid or unsupported `Instance` in `Type::to_type_expression`" 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")), Type::Intersection(_) => Ok(todo_type!("Type::Intersection.in_type_expression")),
} }

View file

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