Skip to content

Commit bbcbaac

Browse files
committed
better error messages for implicit generic types
1 parent 28be9a4 commit bbcbaac

File tree

5 files changed

+165
-12
lines changed

5 files changed

+165
-12
lines changed

crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ reveal_type(C[int, int]) # revealed: <type alias 'C[Unknown]'>
6868
And non-generic types cannot be specialized:
6969

7070
```py
71+
from typing import TypeVar, Protocol, TypedDict
72+
7173
type B = ...
7274

7375
# error: [non-subscriptable] "Cannot subscript non-generic type alias"
@@ -100,6 +102,57 @@ type DoubleSpecialization[T] = list[T][T]
100102

101103
def _(d: DoubleSpecialization[int]):
102104
reveal_type(d) # revealed: Unknown
105+
106+
type Tuple = tuple[int, str]
107+
108+
# error: [non-subscriptable] "Cannot subscript non-generic type alias: `tuple[int, str]` is already specialized"
109+
def _(doubly_specialized: Tuple[int]):
110+
reveal_type(doubly_specialized) # revealed: Unknown
111+
112+
T = TypeVar("T")
113+
114+
class LegacyProto(Protocol[T]):
115+
pass
116+
117+
type LegacyProtoInt = LegacyProto[int]
118+
119+
# error: [non-subscriptable] "Cannot subscript non-generic type alias: `LegacyProto[int]` is already specialized"
120+
def _(x: LegacyProtoInt[int]):
121+
reveal_type(x) # revealed: Unknown
122+
123+
class Proto[T](Protocol):
124+
pass
125+
126+
type ProtoInt = Proto[int]
127+
128+
# error: [non-subscriptable] "Cannot subscript non-generic type alias: `Proto[int]` is already specialized"
129+
def _(x: ProtoInt[int]):
130+
reveal_type(x) # revealed: Unknown
131+
132+
# TODO: TypedDict is just a function object at runtime, we should emit an error
133+
class LegacyDict(TypedDict[T]):
134+
x: T
135+
136+
type LegacyDictInt = LegacyDict[int]
137+
138+
# error: [non-subscriptable] "Cannot subscript non-generic type alias"
139+
def _(x: LegacyDictInt[int]):
140+
reveal_type(x) # revealed: Unknown
141+
142+
class Dict[T](TypedDict):
143+
x: T
144+
145+
type DictInt = Dict[int]
146+
147+
# error: [non-subscriptable] "Cannot subscript non-generic type alias: `Dict` is already specialized"
148+
def _(x: DictInt[int]):
149+
reveal_type(x) # revealed: Unknown
150+
151+
type Union = list[str] | list[int]
152+
153+
# error: [non-subscriptable] "Cannot subscript non-generic type alias: `list[str] | list[int]` is already specialized"
154+
def _(x: Union[int]):
155+
reveal_type(x) # revealed: Unknown
103156
```
104157

105158
If the type variable has an upper bound, the specialized type must satisfy that bound:

crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,8 @@ python-version = "3.12"
659659
```
660660

661661
```py
662+
from typing import Protocol, TypeVar, TypedDict
663+
662664
ListOfInts = list[int]
663665

664666
# error: [non-subscriptable] "Cannot subscript non-generic type: `<class 'list[int]'>` is already specialized"
@@ -677,6 +679,66 @@ List = list[int][int]
677679

678680
def _(doubly_specialized: List):
679681
reveal_type(doubly_specialized) # revealed: Unknown
682+
683+
Tuple = tuple[int, str]
684+
685+
# error: [non-subscriptable] "Cannot subscript non-generic type: `<class 'tuple[int, str]'>` is already specialized"
686+
def _(doubly_specialized: Tuple[int]):
687+
reveal_type(doubly_specialized) # revealed: Unknown
688+
689+
T = TypeVar("T")
690+
691+
class LegacyProto(Protocol[T]):
692+
pass
693+
694+
LegacyProtoInt = LegacyProto[int]
695+
696+
# error: [non-subscriptable] "Cannot subscript non-generic type: `<class 'LegacyProto[int]'>` is already specialized"
697+
def _(doubly_specialized: LegacyProtoInt[int]):
698+
reveal_type(doubly_specialized) # revealed: Unknown
699+
700+
class Proto[T](Protocol):
701+
pass
702+
703+
ProtoInt = Proto[int]
704+
705+
# error: [non-subscriptable] "Cannot subscript non-generic type: `<class 'Proto[int]'>` is already specialized"
706+
def _(doubly_specialized: ProtoInt[int]):
707+
reveal_type(doubly_specialized) # revealed: Unknown
708+
709+
# TODO: TypedDict is just a function object at runtime, we should emit an error
710+
class LegacyDict(TypedDict[T]):
711+
x: T
712+
713+
# TODO: should be a `non-subscriptable` error
714+
LegacyDictInt = LegacyDict[int]
715+
716+
# TODO: should be a `non-subscriptable` error
717+
def _(doubly_specialized: LegacyDictInt[int]):
718+
# TODO: should be `Unknown`
719+
reveal_type(doubly_specialized) # revealed: @Todo(Inference of subscript on special form)
720+
721+
class Dict[T](TypedDict):
722+
x: T
723+
724+
DictInt = Dict[int]
725+
726+
# error: [non-subscriptable] "Cannot subscript non-generic type: `<class 'Dict[int]'>` is already specialized"
727+
def _(doubly_specialized: DictInt[int]):
728+
reveal_type(doubly_specialized) # revealed: Unknown
729+
730+
Union = list[str] | list[int]
731+
732+
# error: [non-subscriptable] "Cannot subscript non-generic type: `<types.UnionType special form 'list[str] | list[int]'>` is already specialized"
733+
def _(doubly_specialized: Union[int]):
734+
reveal_type(doubly_specialized) # revealed: Unknown
735+
736+
type MyListAlias[T] = list[T]
737+
MyListOfInts = MyListAlias[int]
738+
739+
# error: [non-subscriptable] "Cannot subscript non-generic type alias: Double specialization is not allowed"
740+
def _(doubly_specialized: MyListOfInts[int]):
741+
reveal_type(doubly_specialized) # revealed: Unknown
680742
```
681743

682744
Specializing a generic implicit type alias with an incorrect number of type arguments also results

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11819,8 +11819,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
1181911819
.report_lint(&NON_SUBSCRIPTABLE, &*subscript.value)
1182011820
{
1182111821
let mut diagnostic =
11822-
builder.into_diagnostic(format_args!("Cannot subscript non-generic type",));
11823-
if value_ty.is_generic_alias() {
11822+
builder.into_diagnostic("Cannot subscript non-generic type");
11823+
if match value_ty {
11824+
Type::GenericAlias(_) => true,
11825+
Type::KnownInstance(KnownInstanceType::UnionType(union)) => union
11826+
.value_expression_types(db)
11827+
.is_ok_and(|mut tys| tys.any(|ty| ty.is_generic_alias())),
11828+
_ => false,
11829+
} {
1182411830
diagnostic.set_primary_message(format_args!(
1182511831
"`{}` is already specialized",
1182611832
value_ty.display(db)
@@ -12070,9 +12076,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
1207012076
debug_assert!(alias.specialization(db).is_none());
1207112077
if let Some(builder) = self.context.report_lint(&NON_SUBSCRIPTABLE, subscript) {
1207212078
let value_type = alias.raw_value_type(db);
12073-
let mut diagnostic = builder
12074-
.into_diagnostic(format_args!("Cannot subscript non-generic type alias",));
12075-
if value_type.is_generic_nominal_instance() {
12079+
let mut diagnostic =
12080+
builder.into_diagnostic("Cannot subscript non-generic type alias");
12081+
if value_type.is_definition_generic(db) {
1207612082
diagnostic.set_primary_message(format_args!(
1207712083
"`{}` is already specialized",
1207812084
value_type.display(db)

crates/ty_python_semantic/src/types/infer/builder/type_expression.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -949,10 +949,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
949949
self.context.report_lint(&NON_SUBSCRIPTABLE, subscript)
950950
{
951951
let value_type = type_alias.raw_value_type(self.db());
952-
let mut diagnostic = builder.into_diagnostic(format_args!(
953-
"Cannot subscript non-generic type alias",
954-
));
955-
if value_type.is_generic_nominal_instance() {
952+
let mut diagnostic = builder
953+
.into_diagnostic("Cannot subscript non-generic type alias");
954+
if value_type.is_definition_generic(self.db()) {
956955
diagnostic.set_primary_message(format_args!(
957956
"`{}` is already specialized",
958957
value_type.display(self.db()),

crates/ty_python_semantic/src/types/instance.rs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::types::generics::{InferableTypeVars, walk_specialization};
1313
use crate::types::protocol_class::{ProtocolClass, walk_protocol_interface};
1414
use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type};
1515
use crate::types::{
16-
ApplyTypeMappingVisitor, ClassBase, ClassLiteral, FindLegacyTypeVarsVisitor,
16+
ApplyTypeMappingVisitor, ClassBase, ClassLiteral, DynamicType, FindLegacyTypeVarsVisitor,
1717
HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, NormalizedVisitor, TypeContext,
1818
TypeMapping, TypeRelation, VarianceInferable,
1919
};
@@ -93,8 +93,41 @@ impl<'db> Type<'db> {
9393
matches!(self, Type::NominalInstance(_))
9494
}
9595

96-
pub(crate) const fn is_generic_nominal_instance(self) -> bool {
97-
matches!(self, Type::NominalInstance(instance_type) if matches!(instance_type.0, NominalInstanceInner::NonTuple(class) if class.is_generic()))
96+
/// Returns whether the definition of this type is generic
97+
/// (this is different from whether this type *is* a generic type; a type that is already fully specialized is not a generic type).
98+
pub(crate) fn is_definition_generic(self, db: &'db dyn Db) -> bool {
99+
match self {
100+
Type::Union(union) => union
101+
.elements(db)
102+
.iter()
103+
.any(|ty| ty.is_definition_generic(db)),
104+
Type::Intersection(intersection) => {
105+
intersection
106+
.positive(db)
107+
.iter()
108+
.any(|ty| ty.is_definition_generic(db))
109+
|| intersection
110+
.negative(db)
111+
.iter()
112+
.any(|ty| ty.is_definition_generic(db))
113+
}
114+
Type::NominalInstance(instance_type) => match instance_type.0 {
115+
NominalInstanceInner::NonTuple(class) => class.is_generic(),
116+
NominalInstanceInner::ExactTuple(_) => true,
117+
NominalInstanceInner::Object => false,
118+
},
119+
Type::ProtocolInstance(protocol) => {
120+
matches!(protocol.inner, Protocol::FromClass(class) if class.is_generic())
121+
}
122+
Type::TypedDict(typed_dict) => typed_dict.defining_class().is_generic(),
123+
Type::Dynamic(dynamic) => {
124+
matches!(dynamic, DynamicType::UnknownGeneric(_))
125+
}
126+
// Due to inheritance rules, enums cannot be generic.
127+
Type::EnumLiteral(_) => false,
128+
// Once generic NewType is officially specified, handle it.
129+
_ => false,
130+
}
98131
}
99132

100133
pub(crate) const fn as_nominal_instance(self) -> Option<NominalInstanceType<'db>> {

0 commit comments

Comments
 (0)