diff --git a/conformance/third_party/conformance.exp b/conformance/third_party/conformance.exp index 837cb9cb1d..152be6c778 100644 --- a/conformance/third_party/conformance.exp +++ b/conformance/third_party/conformance.exp @@ -11025,17 +11025,6 @@ "stop_column": 17, "stop_line": 22 }, - { - "code": -2, - "column": 7, - "concise_description": "Type variable `T3` in class `Protocol2` is declared as invariant, but could be contravariant based on its usage", - "description": "Type variable `T3` in class `Protocol2` is declared as invariant, but could be contravariant based on its usage", - "line": 41, - "name": "variance-mismatch", - "severity": "warn", - "stop_column": 16, - "stop_line": 41 - }, { "code": -2, "column": 7, diff --git a/conformance/third_party/conformance.result b/conformance/third_party/conformance.result index ff37e9423a..4e97a0b626 100644 --- a/conformance/third_party/conformance.result +++ b/conformance/third_party/conformance.result @@ -144,6 +144,7 @@ "protocols_self.py": [], "protocols_subtyping.py": [], "protocols_variance.py": [ + "Line 41: Expected 1 errors", "Line 15: Unexpected errors ['Unexpected keyword argument `contravariant` to ParamSpec']" ], "qualifiers_annotated.py": [], diff --git a/conformance/third_party/results.json b/conformance/third_party/results.json index 2533501bc8..d616143d15 100644 --- a/conformance/third_party/results.json +++ b/conformance/third_party/results.json @@ -3,7 +3,7 @@ "pass": 140, "fail": 5, "pass_rate": 0.97, - "differences": 23, + "differences": 24, "passing": [ "aliases_explicit.py", "aliases_implicit.py", @@ -151,7 +151,7 @@ "annotations_future_import.py": 2, "generics_paramspec_variance.py": 9, "generics_typevartuple_variance.py": 9, - "protocols_variance.py": 1 + "protocols_variance.py": 2 }, "comment": "@generated" } \ No newline at end of file diff --git a/pyrefly/lib/alt/solve.rs b/pyrefly/lib/alt/solve.rs index c46b158ddf..9e7ac531ec 100644 --- a/pyrefly/lib/alt/solve.rs +++ b/pyrefly/lib/alt/solve.rs @@ -3977,16 +3977,22 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { // with a narrower variance. We only check invariant TypeVars here // because wrong variance on covariant/contravariant TypeVars is // already caught by InvalidVariance at the usage site. + // Constrained TypeVars are excluded because invariance is a sound default for a fixed + // set of constraints, and the narrower-variance suggestion is usually noise. let metadata = self.get_metadata_for_class(class); if metadata.is_protocol() - && tparams - .as_vec() - .iter() - .any(|p| p.is_type_var() && p.variance() == PreInferenceVariance::Invariant) + && tparams.as_vec().iter().any(|p| { + p.is_type_var() + && p.variance() == PreInferenceVariance::Invariant + && !matches!(p.restriction(), Restriction::Constraints(_)) + }) { let inferred = self.infer_variance_ignoring_declared(class); for tparam in tparams.as_vec() { - if !tparam.is_type_var() || tparam.variance() != PreInferenceVariance::Invariant { + if !tparam.is_type_var() + || tparam.variance() != PreInferenceVariance::Invariant + || matches!(tparam.restriction(), Restriction::Constraints(_)) + { continue; } let inferred_v = inferred.get(tparam.name()); diff --git a/pyrefly/lib/test/variance_inference.rs b/pyrefly/lib/test/variance_inference.rs index 6a27fb8d2f..4ac4c22f3e 100644 --- a/pyrefly/lib/test/variance_inference.rs +++ b/pyrefly/lib/test/variance_inference.rs @@ -605,6 +605,21 @@ class Foo(Generic[T_co]): "#, ); +testcase!( + test_constrained_typevar_protocol_variance_mismatch, + r#" +from typing import Protocol, TypeVar + +T = TypeVar("T", str, bytes) + +class Foo(Protocol[T]): + def value(self) -> T: ... + +class Bar(Protocol[T]): + def consume(self, x: T) -> None: ... +"#, +); + testcase!( test_shallow_contravariant_in_return, r#"