From 3a20aeaedeb6660e29ebfb16e9319e9a6f73384f Mon Sep 17 00:00:00 2001 From: Shunsuke Shibayama Date: Mon, 8 Dec 2025 14:55:58 +0900 Subject: [PATCH] [ty] improve bad specialization results & error messages --- .../corpus/cyclic_pep695_typevars.py | 2 + .../mdtest/generics/legacy/paramspec.md | 9 +- .../mdtest/generics/pep695/aliases.md | 31 +++- .../mdtest/generics/pep695/paramspec.md | 8 +- .../resources/mdtest/implicit_type_aliases.md | 15 +- .../src/types/infer/builder.rs | 146 ++++++++++++------ 6 files changed, 145 insertions(+), 66 deletions(-) diff --git a/crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars.py b/crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars.py index 5c9f592768..b7a88b5ca6 100644 --- a/crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars.py +++ b/crates/ty_python_semantic/resources/corpus/cyclic_pep695_typevars.py @@ -3,3 +3,5 @@ def name_1[name_0: name_0](name_2: name_0): pass except name_2: pass + +def _[T: (T if cond else U)[0], U](): 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 c4764c3886..9811ad1452 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -250,7 +250,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-form] "ParamSpec `P2` is unbound" reveal_type(OnlyParamSpec[P2]().attr) # revealed: (...) -> None ``` @@ -273,11 +273,10 @@ 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-form] "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: (...) -> Unknown +reveal_type(TypeVarAndParamSpec[int, int]().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..53223ca6f9 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -74,7 +74,18 @@ type B = ... 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]): ... + +type ListOfInts = list[int] + +# error: [non-subscriptable] "Cannot subscript non-generic type alias" +def _(l: ListOfInts[int]): ... ``` If the type variable has an upper bound, the specialized type must satisfy that bound: @@ -98,6 +109,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 +139,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 6483428bb3..387a4b94da 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -236,7 +236,7 @@ def func[**P2](c: Callable[P2, None]): P2 = ParamSpec("P2") -# TODO: error: paramspec is unbound +# error: [invalid-type-form] "ParamSpec `P2` is unbound" reveal_type(OnlyParamSpec[P2]().attr) # revealed: (...) -> None ``` @@ -259,10 +259,10 @@ 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-form] "ParamSpec `P2` is unbound" +reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> int # error: [invalid-type-arguments] -reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> Unknown +reveal_type(TypeVarAndParamSpec[int, int]().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 0886393143..18a37bcc73 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -656,10 +656,9 @@ A generic alias that is already fully specialized cannot be specialized again: ```py ListOfInts = list[int] -# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1" +# error: [non-subscriptable] "Cannot subscript non-generic type alias: `` 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 ``` Specializing a generic implicit type alias with an incorrect number of type arguments also results @@ -695,23 +694,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 alias" 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 alias" 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/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 6ddaea40a4..7e878c430d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3527,10 +3527,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_FORM, expr) + { + diagnostic_builder.into_diagnostic(format_args!( + "ParamSpec `{}` is unbound", + typevar.name(self.db()) + )); + } return Err(()); } @@ -11609,6 +11616,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, @@ -11661,7 +11679,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 { @@ -11677,8 +11695,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 { @@ -11710,8 +11728,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)) => { @@ -11744,14 +11764,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() { @@ -11786,33 +11808,53 @@ 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}`") + 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) + { + if value_ty.is_generic_alias() { + builder.into_diagnostic(format_args!( + "Cannot subscript non-generic type alias: `{}` is already specialized", + value_ty.display(db), + )); } 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(), - )); + builder.into_diagnostic(format_args!( + "Cannot subscript non-generic type alias" + )); + } + } + 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 { @@ -11822,21 +11864,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(