[ty] Reject legacy TypeVars in PEP 695 functions#25979
Conversation
Typing conformance results improved 🎉The percentage of diagnostics emitted that were expected errors increased from 94.36% to 94.37%. The percentage of expected errors that received a diagnostic increased from 88.91% to 89.00%. The number of fully passing files improved from 93/134 to 94/134. SummaryHow are test cases classified?Each test case represents one expected error annotation or a group of annotations sharing a tag. Counts are per test case, not per diagnostic — multiple diagnostics on the same line count as one. Required annotations (
Test file breakdown1 file altered
True positives added (1)1 diagnostic
|
Memory usage reportMemory usage unchanged ✅ |
|
125113e to
749e106
Compare
cac6cc5 to
3b172df
Compare
4404e35 to
27787ad
Compare
3b172df to
3a7276b
Compare
AlexWaygood
left a comment
There was a problem hiding this comment.
LGTM, just a few missing edge cases
| def legacy(self, value: V, other: K) -> V | K: | ||
| raise NotImplementedError |
There was a problem hiding this comment.
I sort-of understand why this is okay according to the letter of the typing spec, but this does seem to be bad style at the very least! Maybe that's for another (opt-in? or warning-level?) diagnostic to complain about, though
| if node.type_params.is_none() { | ||
| return; | ||
| } | ||
|
|
||
| let legacy_default = node | ||
| .type_params | ||
| .as_deref() | ||
| .into_iter() | ||
| .flatten() |
There was a problem hiding this comment.
| if node.type_params.is_none() { | |
| return; | |
| } | |
| let legacy_default = node | |
| .type_params | |
| .as_deref() | |
| .into_iter() | |
| .flatten() | |
| let Some(type_params) = node.type_params.as_deref() else { | |
| return; | |
| }; | |
| let legacy_default = type_params | |
| .iter() |
| if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = ty | ||
| && matches!( | ||
| typevar.kind(db), | ||
| TypeVarKind::Legacy | TypeVarKind::Pep613Alias | TypeVarKind::ParamSpec |
There was a problem hiding this comment.
this is correct, but it looks wrong, because TypeVarKind::ParamSpec is confusingly named right now. Its name implies that it covers all ParamSpecs, but PEP-695 ParamSpecs are covered by a separate TypeVarKind variant. I suggest that we rename TypeVarKind::ParamSpec to TypeVarKind::LegacyParamSpec in a followup PR to this
| if let Some((typevar, range)) = legacy_default { | ||
| report_pep695_function_legacy_typevar(context, typevar, range); | ||
| return; | ||
| } |
There was a problem hiding this comment.
the way the code is structured here means that if a function has multiple invalid defaults in its parameter list, we'll only report the first invalid one. I think we should report multiple diagnostics on this code, for example (one for the default of A, and another for the default of B), but we only report one on your branch currently:
from typing import TypeVar
T = TypeVar("T")
def f[A = T, B = T](x: T) -> T:
return x| let Some(typevar) = legacy_context | ||
| .variables(db) | ||
| .map(|typevar| typevar.typevar(db)) | ||
| .find(|typevar| !typevar.is_self(db)) | ||
| else { | ||
| return; | ||
| }; |
There was a problem hiding this comment.
there's a similar issue here -- we only report one diagnostic in the following code, but we should report two (one for K and one for L:
from typing import TypeVar
K = TypeVar("K")
L = TypeVar("L")
class C[V]:
# error: [unbound-type-variable] "Legacy type variable `K` cannot be used in a function with PEP 695 type parameters"
def mixed[M](self, value: M, other: K, other_other: L) -> M | K | L:
raise NotImplementedError| // Invalid mixes retained in the inferred signature are reported during | ||
| // post-inference validation. |
There was a problem hiding this comment.
I guess the other option would be to propagate the error out of this method by returning a Result rather than an Option? Doing it as post-inference validation seems less invasive -- I think the way you've done it is fine
## Summary Several tests for PEP 695 type-parameter defaults used traditional `TypeVar`s declared outside the function. Those examples are themselves invalid because a function cannot combine a PEP 695 type-parameter list with traditional type variables introduced by the same function. This updates the existing fixtures to declare both type variables in the PEP 695 parameter list. The tests continue to cover bound and constraint compatibility for defaults without also relying on invalid legacy type-variable usage. (This is a precursor to #25979, which starts to flag those invalid usages.)
3a7276b to
5e3aba3
Compare
5e3aba3 to
0ed1dfd
Compare
## Summary This is a follow-up to #25979. The `TypeVarKind` variants previously used the broad names `Legacy`, `Pep695`, and `ParamSpec`, making it unclear whether a variant represented a `TypeVar` or `ParamSpec` and which declaration syntax produced it. This renames them to `LegacyTypeVar`, `Pep695TypeVar`, and `LegacyParamSpec`, complementing the existing `Pep695ParamSpec` variant. The names now identify both the parameter kind and declaration style without changing behavior.
Summary
A function with its own PEP 695 type parameter list cannot also introduce a traditional
TypeVarorParamSpec. Prior to this change, we discovered the legacy generic context while building the signature but discarded it when merging with the PEP 695 context without reporting an error:With #25975, we now pass the
generics_syntax_compatibilitytest in the conformance suite.