[ty] Show the user where the type variable was defined in `invalid-type-arguments` diagnostics (#21727)

This commit is contained in:
Alex Waygood 2025-12-01 12:25:49 +00:00 committed by GitHub
parent a2096ee2cb
commit 3a11e714c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 238 additions and 12 deletions

View File

@ -210,6 +210,37 @@ reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str]
reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int]
```
## Diagnostics for bad specializations
We show the user where the type variable was defined if a specialization is given that doesn't
satisfy the type variable's upper bound or constraints:
<!-- snapshot-diagnostics -->
`library.py`:
```py
from typing import TypeVar, Generic
T = TypeVar("T", bound=str)
U = TypeVar("U", int, bytes)
class Bounded(Generic[T]):
x: T
class Constrained(Generic[U]):
x: U
```
`main.py`:
```py
from library import Bounded, Constrained
x: Bounded[int] # error: [invalid-type-arguments]
y: Constrained[str] # error: [invalid-type-arguments]
```
## Inferring generic class parameters
We can infer the type parameter from a type context:

View File

@ -191,6 +191,32 @@ reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str]
reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int]
```
## Diagnostics for bad specializations
We show the user where the type variable was defined if a specialization is given that doesn't
satisfy the type variable's upper bound or constraints:
<!-- snapshot-diagnostics -->
`library.py`:
```py
class Bounded[T: str]:
x: T
class Constrained[U: (int, bytes)]:
x: U
```
`main.py`:
```py
from library import Bounded, Constrained
x: Bounded[int] # error: [invalid-type-arguments]
y: Constrained[str] # error: [invalid-type-arguments]
```
## Inferring generic class parameters
We can infer the type parameter from a type context:

View File

@ -0,0 +1,78 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: classes.md - Generic classes: Legacy syntax - Diagnostics for bad specializations
mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md
---
# Python source files
## library.py
```
1 | from typing import TypeVar, Generic
2 |
3 | T = TypeVar("T", bound=str)
4 | U = TypeVar("U", int, bytes)
5 |
6 | class Bounded(Generic[T]):
7 | x: T
8 |
9 | class Constrained(Generic[U]):
10 | x: U
```
## main.py
```
1 | from library import Bounded, Constrained
2 |
3 | x: Bounded[int] # error: [invalid-type-arguments]
4 | y: Constrained[str] # error: [invalid-type-arguments]
```
# Diagnostics
```
error[invalid-type-arguments]: Type `int` is not assignable to upper bound `str` of type variable `T@Bounded`
--> src/main.py:3:12
|
1 | from library import Bounded, Constrained
2 |
3 | x: Bounded[int] # error: [invalid-type-arguments]
| ^^^
4 | y: Constrained[str] # error: [invalid-type-arguments]
|
::: src/library.py:3:1
|
1 | from typing import TypeVar, Generic
2 |
3 | T = TypeVar("T", bound=str)
| - Type variable defined here
4 | U = TypeVar("U", int, bytes)
|
info: rule `invalid-type-arguments` is enabled by default
```
```
error[invalid-type-arguments]: Type `str` does not satisfy constraints `int`, `bytes` of type variable `U@Constrained`
--> src/main.py:4:16
|
3 | x: Bounded[int] # error: [invalid-type-arguments]
4 | y: Constrained[str] # error: [invalid-type-arguments]
| ^^^
|
::: src/library.py:4:1
|
3 | T = TypeVar("T", bound=str)
4 | U = TypeVar("U", int, bytes)
| - Type variable defined here
5 |
6 | class Bounded(Generic[T]):
|
info: rule `invalid-type-arguments` is enabled by default
```

View File

@ -0,0 +1,71 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: classes.md - Generic classes: PEP 695 syntax - Diagnostics for bad specializations
mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md
---
# Python source files
## library.py
```
1 | class Bounded[T: str]:
2 | x: T
3 |
4 | class Constrained[U: (int, bytes)]:
5 | x: U
```
## main.py
```
1 | from library import Bounded, Constrained
2 |
3 | x: Bounded[int] # error: [invalid-type-arguments]
4 | y: Constrained[str] # error: [invalid-type-arguments]
```
# Diagnostics
```
error[invalid-type-arguments]: Type `int` is not assignable to upper bound `str` of type variable `T@Bounded`
--> src/main.py:3:12
|
1 | from library import Bounded, Constrained
2 |
3 | x: Bounded[int] # error: [invalid-type-arguments]
| ^^^
4 | y: Constrained[str] # error: [invalid-type-arguments]
|
::: src/library.py:1:15
|
1 | class Bounded[T: str]:
| - Type variable defined here
2 | x: T
|
info: rule `invalid-type-arguments` is enabled by default
```
```
error[invalid-type-arguments]: Type `str` does not satisfy constraints `int`, `bytes` of type variable `U@Constrained`
--> src/main.py:4:16
|
3 | x: Bounded[int] # error: [invalid-type-arguments]
4 | y: Constrained[str] # error: [invalid-type-arguments]
| ^^^
|
::: src/library.py:4:19
|
2 | x: T
3 |
4 | class Constrained[U: (int, bytes)]:
| - Type variable defined here
5 | x: U
|
info: rule `invalid-type-arguments` is enabled by default
```

View File

@ -1,9 +1,9 @@
use std::iter;
use itertools::{Either, EitherOrBoth, Itertools};
use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity};
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span};
use ruff_db::files::File;
use ruff_db::parsed::ParsedModuleRef;
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast::visitor::{Visitor, walk_expr};
use ruff_python_ast::{
self as ast, AnyNodeRef, ExprContext, HasNodeIndex, NodeIndex, PythonVersion,
@ -102,14 +102,15 @@ use crate::types::typed_dict::{
};
use crate::types::visitor::any_over_type;
use crate::types::{
CallDunderError, CallableBinding, CallableType, CallableTypes, ClassLiteral, ClassType,
DataclassParams, DynamicType, InternedType, IntersectionBuilder, IntersectionType, KnownClass,
KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate,
PEP695TypeAliasType, ParameterForm, SpecialFormType, SubclassOfType, TrackedConstraintSet,
Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation,
TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder,
UnionType, UnionTypeInstance, binding_type, infer_scope_types, overrides, todo_type,
BoundTypeVarInstance, CallDunderError, CallableBinding, CallableType, CallableTypes,
ClassLiteral, ClassType, DataclassParams, DynamicType, InternedType, IntersectionBuilder,
IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy,
MetaclassCandidate, PEP695TypeAliasType, ParameterForm, SpecialFormType, SubclassOfType,
TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext,
TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation,
TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance,
TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, binding_type, infer_scope_types,
overrides, todo_type,
};
use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
use crate::unpack::{EvaluationMode, UnpackPosition};
@ -11292,6 +11293,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
generic_context: GenericContext<'db>,
specialize: impl FnOnce(&[Option<Type<'db>>]) -> Type<'db>,
) -> Type<'db> {
fn add_typevar_definition<'db>(
db: &'db dyn Db,
diagnostic: &mut Diagnostic,
typevar: BoundTypeVarInstance<'db>,
) {
let Some(definition) = typevar.typevar(db).definition(db) else {
return;
};
let file = definition.file(db);
let module = parsed_module(db, file).load(db);
let range = definition.focus_range(db, &module).range();
diagnostic.annotate(
Annotation::secondary(Span::from(file).with_range(range))
.message("Type variable defined here"),
);
}
let db = self.db();
let slice_node = subscript.slice.as_ref();
@ -11349,13 +11367,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if let Some(builder) =
self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node)
{
builder.into_diagnostic(format_args!(
let mut diagnostic = builder.into_diagnostic(format_args!(
"Type `{}` is not assignable to upper bound `{}` \
of type variable `{}`",
provided_type.display(db),
bound.display(db),
typevar.identity(db).display(db),
));
add_typevar_definition(db, &mut diagnostic, typevar);
}
has_error = true;
continue;
@ -11374,7 +11393,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if let Some(builder) =
self.context.report_lint(&INVALID_TYPE_ARGUMENTS, node)
{
builder.into_diagnostic(format_args!(
let mut diagnostic = builder.into_diagnostic(format_args!(
"Type `{}` does not satisfy constraints `{}` \
of type variable `{}`",
provided_type.display(db),
@ -11385,6 +11404,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.format("`, `"),
typevar.identity(db).display(db),
));
add_typevar_definition(db, &mut diagnostic, typevar);
}
has_error = true;
continue;