From 5e42926eee4de54a7884e99715ea6746a39e8b60 Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama <45118249+mtshiba@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:21:34 +0900 Subject: [PATCH] [ty] improve bad specialization results & error messages (#21840) ## Summary This PR includes the following changes: * When attempting to specialize a non-generic type (or a type that is already specialized), the result is `Unknown`. Also, the error message is improved. * When an implicit type alias is incorrectly specialized, the result is `Unknown`. Also, the error message is improved. * When only some of the type alias bounds and constraints are not satisfied, not all substitutions are `Unknown`. * Double specialization is prohibited. e.g. `G[int][int]` Furthermore, after applying this PR, the fuzzing tests for seeds 1052 and 4419, which panic in main, now pass. This is because the false recursions on type variables have been removed. ```python # name_2[0] => Unknown class name_1[name_2: name_2[0]]: def name_4(name_3: name_2, /): if name_3: pass # (name_5 if unique_name_0 else name_1)[0] => Unknown def name_4[name_5: (name_5 if unique_name_0 else name_1)[0], **name_1](): ... ``` ## Test Plan New corpus test mdtest files updated --- .../cyclic_pep695_typevars_invalid_bound.py | 1 + ...lic_pep695_typevars_invalid_constraints.py | 4 + .../mdtest/generics/legacy/paramspec.md | 15 +- .../mdtest/generics/pep695/aliases.md | 98 ++++++++++- .../mdtest/generics/pep695/paramspec.md | 12 +- .../resources/mdtest/implicit_type_aliases.md | 95 +++++++++- crates/ty_python_semantic/src/types.rs | 35 ++++ .../src/types/infer/builder.rs | 164 ++++++++++++------ .../types/infer/builder/type_expression.rs | 22 ++- .../ty_python_semantic/src/types/instance.rs | 8 + 10 files changed, 377 insertions(+), 77 deletions(-) create mode 100644 crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars_invalid_bound.py create mode 100644 crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars_invalid_constraints.py diff --git a/crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars_invalid_bound.py b/crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars_invalid_bound.py new file mode 100644 index 0000000000..8d63530cf3 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars_invalid_bound.py @@ -0,0 +1 @@ +def _[T: (T if cond else U)[0], U](): pass diff --git a/crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars_invalid_constraints.py b/crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars_invalid_constraints.py new file mode 100644 index 0000000000..549470e214 --- /dev/null +++ b/crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars_invalid_constraints.py @@ -0,0 +1,4 @@ +class _[T: (0, T[0])]: + def _(x: T): + if x: + pass diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md index 201ce8d0e2..ec3d608506 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -283,7 +283,7 @@ reveal_type(OnlyParamSpec[...]().attr) # revealed: (...) -> None def func(c: Callable[P2, None]): reveal_type(OnlyParamSpec[P2]().attr) # revealed: (**P2@func) -> None -# TODO: error: paramspec is unbound +# error: [invalid-type-arguments] "ParamSpec `P2` is unbound" reveal_type(OnlyParamSpec[P2]().attr) # revealed: (...) -> None # error: [invalid-type-arguments] "No type argument provided for required type variable `P1` of class `OnlyParamSpec`" @@ -327,15 +327,14 @@ reveal_type(TypeVarAndParamSpec[int, [int, str]]().attr) # revealed: (int, str, reveal_type(TypeVarAndParamSpec[int, [str]]().attr) # revealed: (str, /) -> int reveal_type(TypeVarAndParamSpec[int, ...]().attr) # revealed: (...) -> int -# TODO: We could still specialize for `T1` as the type is valid which would reveal `(...) -> int` -# TODO: error: paramspec is unbound -reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> Unknown +# error: [invalid-type-arguments] "ParamSpec `P2` is unbound" +reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> int +# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`" +reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> int # error: [invalid-type-arguments] "Type argument for `ParamSpec` must be" -reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> Unknown +reveal_type(TypeVarAndParamSpec[int, ()]().attr) # revealed: (...) -> int # error: [invalid-type-arguments] "Type argument for `ParamSpec` must be" -reveal_type(TypeVarAndParamSpec[int, ()]().attr) # revealed: (...) -> Unknown -# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be" -reveal_type(TypeVarAndParamSpec[int, (int, str)]().attr) # revealed: (...) -> Unknown +reveal_type(TypeVarAndParamSpec[int, (int, str)]().attr) # revealed: (...) -> int ``` Nor can they be omitted when there are more than one `ParamSpec`s. diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md index 490ae01fc6..4af89f9d2a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -68,13 +68,91 @@ reveal_type(C[int, int]) # revealed: And non-generic types cannot be specialized: ```py +from typing import TypeVar, Protocol, TypedDict + type B = ... # error: [non-subscriptable] "Cannot subscript non-generic type alias" reveal_type(B[int]) # revealed: Unknown # error: [non-subscriptable] "Cannot subscript non-generic type alias" -def _(b: B[int]): ... +def _(b: B[int]): + reveal_type(b) # revealed: Unknown + +type IntOrStr = int | str + +# error: [non-subscriptable] "Cannot subscript non-generic type alias" +def _(c: IntOrStr[int]): + reveal_type(c) # revealed: Unknown + +type ListOfInts = list[int] + +# error: [non-subscriptable] "Cannot subscript non-generic type alias: `list[int]` is already specialized" +def _(l: ListOfInts[int]): + reveal_type(l) # revealed: Unknown + +type List[T] = list[T] + +# error: [non-subscriptable] "Cannot subscript non-generic type alias: Double specialization is not allowed" +def _(l: List[int][int]): + reveal_type(l) # revealed: Unknown + +# error: [non-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +type DoubleSpecialization[T] = list[T][T] + +def _(d: DoubleSpecialization[int]): + reveal_type(d) # revealed: Unknown + +type Tuple = tuple[int, str] + +# error: [non-subscriptable] "Cannot subscript non-generic type alias: `tuple[int, str]` is already specialized" +def _(doubly_specialized: Tuple[int]): + reveal_type(doubly_specialized) # revealed: Unknown + +T = TypeVar("T") + +class LegacyProto(Protocol[T]): + pass + +type LegacyProtoInt = LegacyProto[int] + +# error: [non-subscriptable] "Cannot subscript non-generic type alias: `LegacyProto[int]` is already specialized" +def _(x: LegacyProtoInt[int]): + reveal_type(x) # revealed: Unknown + +class Proto[T](Protocol): + pass + +type ProtoInt = Proto[int] + +# error: [non-subscriptable] "Cannot subscript non-generic type alias: `Proto[int]` is already specialized" +def _(x: ProtoInt[int]): + reveal_type(x) # revealed: Unknown + +# TODO: TypedDict is just a function object at runtime, we should emit an error +class LegacyDict(TypedDict[T]): + x: T + +type LegacyDictInt = LegacyDict[int] + +# error: [non-subscriptable] "Cannot subscript non-generic type alias" +def _(x: LegacyDictInt[int]): + reveal_type(x) # revealed: Unknown + +class Dict[T](TypedDict): + x: T + +type DictInt = Dict[int] + +# error: [non-subscriptable] "Cannot subscript non-generic type alias: `Dict` is already specialized" +def _(x: DictInt[int]): + reveal_type(x) # revealed: Unknown + +type Union = list[str] | list[int] + +# error: [non-subscriptable] "Cannot subscript non-generic type alias: `list[str] | list[int]` is already specialized" +def _(x: Union[int]): + reveal_type(x) # revealed: Unknown ``` If the type variable has an upper bound, the specialized type must satisfy that bound: @@ -98,6 +176,15 @@ reveal_type(BoundedByUnion[int]) # revealed: reveal_type(BoundedByUnion[IntSubclass]) # revealed: reveal_type(BoundedByUnion[str]) # revealed: reveal_type(BoundedByUnion[int | str]) # revealed: + +type TupleOfIntAndStr[T: int, U: str] = tuple[T, U] + +def _(x: TupleOfIntAndStr[int, str]): + reveal_type(x) # revealed: tuple[int, str] + +# error: [invalid-type-arguments] "Type `int` is not assignable to upper bound `str` of type variable `U@TupleOfIntAndStr`" +def _(x: TupleOfIntAndStr[int, int]): + reveal_type(x) # revealed: tuple[int, Unknown] ``` If the type variable is constrained, the specialized type must satisfy those constraints: @@ -119,6 +206,15 @@ reveal_type(Constrained[int | str]) # revealed: + +type TupleOfIntOrStr[T: (int, str), U: (int, str)] = tuple[T, U] + +def _(x: TupleOfIntOrStr[int, str]): + reveal_type(x) # revealed: tuple[int, str] + +# error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `U@TupleOfIntOrStr`" +def _(x: TupleOfIntOrStr[int, object]): + reveal_type(x) # revealed: tuple[int, Unknown] ``` If the type variable has a default, it can be omitted: diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index 81c1960de4..92e1e64116 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -237,7 +237,7 @@ def func[**P2](c: Callable[P2, None]): P2 = ParamSpec("P2") -# TODO: error: paramspec is unbound +# error: [invalid-type-arguments] "ParamSpec `P2` is unbound" reveal_type(OnlyParamSpec[P2]().attr) # revealed: (...) -> None # error: [invalid-type-arguments] "No type argument provided for required type variable `P1` of class `OnlyParamSpec`" @@ -281,14 +281,14 @@ reveal_type(TypeVarAndParamSpec[int, [int, str]]().attr) # revealed: (int, str, reveal_type(TypeVarAndParamSpec[int, [str]]().attr) # revealed: (str, /) -> int reveal_type(TypeVarAndParamSpec[int, ...]().attr) # revealed: (...) -> int -# TODO: error: paramspec is unbound -reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> Unknown +# error: [invalid-type-arguments] "ParamSpec `P2` is unbound" +reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> int # error: [invalid-type-arguments] "Type argument for `ParamSpec` must be" -reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> Unknown +reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> int # error: [invalid-type-arguments] "Type argument for `ParamSpec` must be" -reveal_type(TypeVarAndParamSpec[int, ()]().attr) # revealed: (...) -> Unknown +reveal_type(TypeVarAndParamSpec[int, ()]().attr) # revealed: (...) -> int # error: [invalid-type-arguments] "Type argument for `ParamSpec` must be" -reveal_type(TypeVarAndParamSpec[int, (int, str)]().attr) # revealed: (...) -> Unknown +reveal_type(TypeVarAndParamSpec[int, (int, str)]().attr) # revealed: (...) -> int ``` Nor can they be omitted when there are more than one `ParamSpec`. diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 99a5de8aa9..c2569023f1 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -653,13 +653,92 @@ def g(obj: Y[bool, range]): A generic alias that is already fully specialized cannot be specialized again: +```toml +[environment] +python-version = "3.12" +``` + ```py +from typing import Protocol, TypeVar, TypedDict + ListOfInts = list[int] -# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" +# error: [non-subscriptable] "Cannot subscript non-generic type: `` is already specialized" def _(doubly_specialized: ListOfInts[int]): - # TODO: This should ideally be `list[Unknown]` or `Unknown` - reveal_type(doubly_specialized) # revealed: list[int] + reveal_type(doubly_specialized) # revealed: Unknown + +type ListOfInts2 = list[int] +# error: [non-subscriptable] "Cannot subscript non-generic type alias: `list[int]` is already specialized" +DoublySpecialized = ListOfInts2[int] + +def _(doubly_specialized: DoublySpecialized): + reveal_type(doubly_specialized) # revealed: Unknown + +# error: [non-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +List = list[int][int] + +def _(doubly_specialized: List): + reveal_type(doubly_specialized) # revealed: Unknown + +Tuple = tuple[int, str] + +# error: [non-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +def _(doubly_specialized: Tuple[int]): + reveal_type(doubly_specialized) # revealed: Unknown + +T = TypeVar("T") + +class LegacyProto(Protocol[T]): + pass + +LegacyProtoInt = LegacyProto[int] + +# error: [non-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +def _(doubly_specialized: LegacyProtoInt[int]): + reveal_type(doubly_specialized) # revealed: Unknown + +class Proto[T](Protocol): + pass + +ProtoInt = Proto[int] + +# error: [non-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +def _(doubly_specialized: ProtoInt[int]): + reveal_type(doubly_specialized) # revealed: Unknown + +# TODO: TypedDict is just a function object at runtime, we should emit an error +class LegacyDict(TypedDict[T]): + x: T + +# TODO: should be a `non-subscriptable` error +LegacyDictInt = LegacyDict[int] + +# TODO: should be a `non-subscriptable` error +def _(doubly_specialized: LegacyDictInt[int]): + # TODO: should be `Unknown` + reveal_type(doubly_specialized) # revealed: @Todo(Inference of subscript on special form) + +class Dict[T](TypedDict): + x: T + +DictInt = Dict[int] + +# error: [non-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +def _(doubly_specialized: DictInt[int]): + reveal_type(doubly_specialized) # revealed: Unknown + +Union = list[str] | list[int] + +# error: [non-subscriptable] "Cannot subscript non-generic type: `` is already specialized" +def _(doubly_specialized: Union[int]): + reveal_type(doubly_specialized) # revealed: Unknown + +type MyListAlias[T] = list[T] +MyListOfInts = MyListAlias[int] + +# error: [non-subscriptable] "Cannot subscript non-generic type alias: Double specialization is not allowed" +def _(doubly_specialized: MyListOfInts[int]): + reveal_type(doubly_specialized) # revealed: Unknown ``` Specializing a generic implicit type alias with an incorrect number of type arguments also results @@ -695,23 +774,21 @@ def this_does_not_work() -> TypeOf[IntOrStr]: raise NotImplementedError() def _( - # TODO: Better error message (of kind `invalid-type-form`)? - # error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" + # error: [non-subscriptable] "Cannot subscript non-generic type" specialized: this_does_not_work()[int], ): - reveal_type(specialized) # revealed: int | str + reveal_type(specialized) # revealed: Unknown ``` Similarly, if you try to specialize a union type without a binding context, we emit an error: ```py -# TODO: Better error message (of kind `invalid-type-form`)? -# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" +# error: [non-subscriptable] "Cannot subscript non-generic type" x: (list[T] | set[T])[int] def _(): # TODO: `list[Unknown] | set[Unknown]` might be better - reveal_type(x) # revealed: list[typing.TypeVar] | set[typing.TypeVar] + reveal_type(x) # revealed: Unknown ``` ### Multiple definitions diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index cfbafef322..bdf406b060 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -989,6 +989,41 @@ impl<'db> Type<'db> { matches!(self, Type::GenericAlias(_)) } + /// Returns whether the definition of this type is generic + /// (this is different from whether this type *is* a generic type; a type that is already fully specialized is not a generic type). + pub(crate) fn is_definition_generic(self, db: &'db dyn Db) -> bool { + match self { + Type::Union(union) => union + .elements(db) + .iter() + .any(|ty| ty.is_definition_generic(db)), + Type::Intersection(intersection) => { + intersection + .positive(db) + .iter() + .any(|ty| ty.is_definition_generic(db)) + || intersection + .negative(db) + .iter() + .any(|ty| ty.is_definition_generic(db)) + } + Type::NominalInstance(instance_type) => instance_type.is_definition_generic(), + Type::ProtocolInstance(protocol) => { + matches!(protocol.inner, Protocol::FromClass(class) if class.is_generic()) + } + Type::TypedDict(typed_dict) => typed_dict + .defining_class() + .is_some_and(ClassType::is_generic), + Type::Dynamic(dynamic) => { + matches!(dynamic, DynamicType::UnknownGeneric(_)) + } + // Due to inheritance rules, enums cannot be generic. + Type::EnumLiteral(_) => false, + // Once generic NewType is officially specified, handle it. + _ => false, + } + } + const fn is_dynamic(&self) -> bool { matches!(self, Type::Dynamic(_)) } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index e4a488dc7e..57e3df8eb9 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3541,10 +3541,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return Ok(param_type); } - Type::KnownInstance(known_instance) + Type::KnownInstance(known_instance @ KnownInstanceType::TypeVar(typevar)) if known_instance.class(self.db()) == KnownClass::ParamSpec => { - // TODO: Emit diagnostic: "ParamSpec "P" is unbound" + if let Some(diagnostic_builder) = + self.context.report_lint(&INVALID_TYPE_ARGUMENTS, expr) + { + diagnostic_builder.into_diagnostic(format_args!( + "ParamSpec `{}` is unbound", + typevar.name(self.db()) + )); + } return Err(()); } @@ -11636,6 +11643,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { generic_context: GenericContext<'db>, specialize: impl FnOnce(&[Option>]) -> Type<'db>, ) -> Type<'db> { + enum ExplicitSpecializationError { + InvalidParamSpec, + UnsatisfiedBound, + UnsatisfiedConstraints, + /// These two errors override the errors above, causing all specializations to be `Unknown`. + MissingTypeVars, + TooManyArguments, + /// This error overrides the errors above, causing the type itself to be `Unknown`. + NonGeneric, + } + fn add_typevar_definition<'db>( db: &'db dyn Db, diagnostic: &mut Diagnostic, @@ -11688,7 +11706,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } }; - let mut has_error = false; + let mut error: Option = None; for (index, item) in typevars.zip_longest(type_arguments.iter()).enumerate() { match item { @@ -11704,8 +11722,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) { Ok(paramspec_value) => paramspec_value, Err(()) => { - has_error = true; - Type::unknown() + error = Some(ExplicitSpecializationError::InvalidParamSpec); + Type::paramspec_value_callable(db, Parameters::unknown()) } } } else { @@ -11737,8 +11755,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); add_typevar_definition(db, &mut diagnostic, typevar); } - has_error = true; - continue; + error = Some(ExplicitSpecializationError::UnsatisfiedBound); + specialization_types.push(Some(Type::unknown())); + } else { + specialization_types.push(Some(provided_type)); } } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { @@ -11771,14 +11791,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); add_typevar_definition(db, &mut diagnostic, typevar); } - has_error = true; - continue; + error = Some(ExplicitSpecializationError::UnsatisfiedConstraints); + specialization_types.push(Some(Type::unknown())); + } else { + specialization_types.push(Some(provided_type)); } } - None => {} + None => { + specialization_types.push(Some(provided_type)); + } } - - specialization_types.push(Some(provided_type)); } EitherOrBoth::Left(typevar) => { if typevar.default_type(db).is_none() { @@ -11813,33 +11835,57 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } )); } - has_error = true; + error = Some(ExplicitSpecializationError::MissingTypeVars); } if let Some(first_excess_type_argument_index) = first_excess_type_argument_index { - let node = get_node(first_excess_type_argument_index); - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node) { - let description = CallableDescription::new(db, value_ty); - builder.into_diagnostic(format_args!( - "Too many type arguments{}: expected {}, got {}", - if let Some(CallableDescription { kind, name }) = description { - format!(" to {kind} `{name}`") - } else { - String::new() - }, - if typevar_with_defaults == 0 { - format!("{typevars_len}") - } else { - format!( - "between {} and {}", - typevars_len - typevar_with_defaults, - typevars_len - ) - }, - type_arguments.len(), - )); + if typevars_len == 0 { + // Type parameter list cannot be empty, so if we reach here, `value_ty` is not a generic type. + if let Some(builder) = self + .context + .report_lint(&NON_SUBSCRIPTABLE, &*subscript.value) + { + let mut diagnostic = + builder.into_diagnostic("Cannot subscript non-generic type"); + if match value_ty { + Type::GenericAlias(_) => true, + Type::KnownInstance(KnownInstanceType::UnionType(union)) => union + .value_expression_types(db) + .is_ok_and(|mut tys| tys.any(|ty| ty.is_generic_alias())), + _ => false, + } { + diagnostic.set_primary_message(format_args!( + "`{}` is already specialized", + value_ty.display(db) + )); + } + } + error = Some(ExplicitSpecializationError::NonGeneric); + } else { + let node = get_node(first_excess_type_argument_index); + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node) { + let description = CallableDescription::new(db, value_ty); + builder.into_diagnostic(format_args!( + "Too many type arguments{}: expected {}, got {}", + if let Some(CallableDescription { kind, name }) = description { + format!(" to {kind} `{name}`") + } else { + String::new() + }, + if typevar_with_defaults == 0 { + format!("{typevars_len}") + } else { + format!( + "between {} and {}", + typevars_len - typevar_with_defaults, + typevars_len + ) + }, + type_arguments.len(), + )); + } + error = Some(ExplicitSpecializationError::TooManyArguments); } - has_error = true; } if store_inferred_type_arguments { @@ -11849,21 +11895,31 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } - if has_error { - let unknowns = generic_context - .variables(self.db()) - .map(|typevar| { - Some(if typevar.is_paramspec(db) { - Type::paramspec_value_callable(db, Parameters::unknown()) - } else { - Type::unknown() + match error { + Some(ExplicitSpecializationError::NonGeneric) => Type::unknown(), + Some( + ExplicitSpecializationError::MissingTypeVars + | ExplicitSpecializationError::TooManyArguments, + ) => { + let unknowns = generic_context + .variables(self.db()) + .map(|typevar| { + Some(if typevar.is_paramspec(db) { + Type::paramspec_value_callable(db, Parameters::unknown()) + } else { + Type::unknown() + }) }) - }) - .collect::>(); - return specialize(&unknowns); + .collect::>(); + specialize(&unknowns) + } + Some( + ExplicitSpecializationError::UnsatisfiedBound + | ExplicitSpecializationError::UnsatisfiedConstraints + | ExplicitSpecializationError::InvalidParamSpec, + ) + | None => specialize(&specialization_types), } - - specialize(&specialization_types) } fn infer_subscript_expression_types( @@ -12044,9 +12100,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(alias))), _, ) if alias.generic_context(db).is_none() => { + debug_assert!(alias.specialization(db).is_none()); if let Some(builder) = self.context.report_lint(&NON_SUBSCRIPTABLE, subscript) { - builder - .into_diagnostic(format_args!("Cannot subscript non-generic type alias")); + let value_type = alias.raw_value_type(db); + let mut diagnostic = + builder.into_diagnostic("Cannot subscript non-generic type alias"); + if value_type.is_definition_generic(db) { + diagnostic.set_primary_message(format_args!( + "`{}` is already specialized", + value_type.display(db) + )); + } } Some(Type::unknown()) diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 626225cdc8..c8df4df756 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -919,6 +919,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::unknown() } KnownInstanceType::TypeAliasType(type_alias @ TypeAliasType::PEP695(_)) => { + if type_alias.specialization(self.db()).is_some() { + if let Some(builder) = + self.context.report_lint(&NON_SUBSCRIPTABLE, subscript) + { + let mut diagnostic = + builder.into_diagnostic("Cannot subscript non-generic type alias"); + diagnostic.set_primary_message("Double specialization is not allowed"); + } + return Type::unknown(); + } match type_alias.generic_context(self.db()) { Some(generic_context) => { let specialized_type_alias = self @@ -943,9 +953,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> { if let Some(builder) = self.context.report_lint(&NON_SUBSCRIPTABLE, subscript) { - builder.into_diagnostic(format_args!( - "Cannot subscript non-generic type alias" - )); + let value_type = type_alias.raw_value_type(self.db()); + let mut diagnostic = builder + .into_diagnostic("Cannot subscript non-generic type alias"); + if value_type.is_definition_generic(self.db()) { + diagnostic.set_primary_message(format_args!( + "`{}` is already specialized", + value_type.display(self.db()), + )); + } } Type::unknown() diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index fb53f10ef4..9e674065b9 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -304,6 +304,14 @@ impl<'db> NominalInstanceType<'db> { matches!(self.0, NominalInstanceInner::Object) } + pub(super) fn is_definition_generic(self) -> bool { + match self.0 { + NominalInstanceInner::NonTuple(class) => class.is_generic(), + NominalInstanceInner::ExactTuple(_) => true, + NominalInstanceInner::Object => false, + } + } + /// If this type is an *exact* tuple type (*not* a subclass of `tuple`), returns the /// tuple spec. ///