From 1734ddfb3e6393b0cd45b2b1d3f170cc102b2fcf Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 31 Oct 2025 17:48:34 +0100 Subject: [PATCH] [ty] Do not promote literals in contravariant positions of generic specializations (#21171) ## Summary closes https://github.com/astral-sh/ty/issues/1284 supersedes https://github.com/astral-sh/ruff/pull/20950 by @ibraheemdev ## Test Plan New regression test --- .../resources/mdtest/literal_promotion.md | 36 +++++++++++++++++++ .../ty_python_semantic/src/types/generics.rs | 9 +++-- .../ty_python_semantic/src/types/variance.rs | 7 ++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/literal_promotion.md b/crates/ty_python_semantic/resources/mdtest/literal_promotion.md index 726ca59d20..f13d3229ee 100644 --- a/crates/ty_python_semantic/resources/mdtest/literal_promotion.md +++ b/crates/ty_python_semantic/resources/mdtest/literal_promotion.md @@ -1,5 +1,10 @@ # Literal promotion +```toml +[environment] +python-version = "3.12" +``` + There are certain places where we promote literals to their common supertype: ```py @@ -30,3 +35,34 @@ def double_negation(callback: Callable[[Callable[[Literal[1]], None]], None]): reveal_type([callback]) # revealed: list[Unknown | (((int, /) -> None, /) -> None)] ``` + +Literal promotion should also not apply recursively to type arguments in contravariant/invariant +position: + +```py +class Bivariant[T]: + pass + +class Covariant[T]: + def pop(self) -> T: + raise NotImplementedError + +class Contravariant[T]: + def push(self, value: T) -> None: + pass + +class Invariant[T]: + x: T + +def _( + bivariant: Bivariant[Literal[1]], + covariant: Covariant[Literal[1]], + contravariant: Contravariant[Literal[1]], + invariant: Invariant[Literal[1]], +): + reveal_type([bivariant]) # revealed: list[Unknown | Bivariant[int]] + reveal_type([covariant]) # revealed: list[Unknown | Covariant[int]] + + reveal_type([contravariant]) # revealed: list[Unknown | Contravariant[Literal[1]]] + reveal_type([invariant]) # revealed: list[Unknown | Invariant[Literal[1]]] +``` diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 8485931ff2..98f7cb736f 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -969,10 +969,15 @@ impl<'db> Specialization<'db> { let types: Box<[_]> = self .types(db) .iter() + .zip(self.generic_context(db).variables(db)) .enumerate() - .map(|(i, ty)| { + .map(|(i, (ty, typevar))| { let tcx = TypeContext::new(tcx.get(i).copied()); - ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + if typevar.variance(db).is_covariant() { + ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + } else { + ty.apply_type_mapping_impl(db, &type_mapping.flip(), tcx, visitor) + } }) .collect(); diff --git a/crates/ty_python_semantic/src/types/variance.rs b/crates/ty_python_semantic/src/types/variance.rs index fb9c87d062..5ec1d5a8ff 100644 --- a/crates/ty_python_semantic/src/types/variance.rs +++ b/crates/ty_python_semantic/src/types/variance.rs @@ -85,6 +85,13 @@ impl TypeVarVariance { TypeVarVariance::Bivariant => TypeVarVariance::Bivariant, } } + + pub(crate) const fn is_covariant(self) -> bool { + matches!( + self, + TypeVarVariance::Covariant | TypeVarVariance::Bivariant + ) + } } impl std::iter::FromIterator for TypeVarVariance {