[ty] Do not promote literals in contravariant position (#21164)

## Summary

closes https://github.com/astral-sh/ty/issues/1463

## Test Plan

Regression tests
This commit is contained in:
David Peter 2025-10-31 16:00:30 +01:00 committed by GitHub
parent 1d6ae8596a
commit 0c2cf75869
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 84 additions and 23 deletions

View File

@ -0,0 +1,32 @@
# Literal promotion
There are certain places where we promote literals to their common supertype:
```py
reveal_type([1, 2, 3]) # revealed: list[Unknown | int]
reveal_type({"a", "b", "c"}) # revealed: set[Unknown | str]
```
This promotion should not take place if the literal type appears in contravariant position:
```py
from typing import Callable, Literal
def in_negated_position(non_zero_number: int):
if non_zero_number == 0:
raise ValueError()
reveal_type(non_zero_number) # revealed: int & ~Literal[0]
reveal_type([non_zero_number]) # revealed: list[Unknown | (int & ~Literal[0])]
def in_parameter_position(callback: Callable[[Literal[1]], None]):
reveal_type(callback) # revealed: (Literal[1], /) -> None
reveal_type([callback]) # revealed: list[Unknown | ((Literal[1], /) -> None)]
def double_negation(callback: Callable[[Callable[[Literal[1]], None]], None]):
reveal_type(callback) # revealed: ((Literal[1], /) -> None, /) -> None
reveal_type([callback]) # revealed: list[Unknown | (((int, /) -> None, /) -> None)]
```

View File

@ -1270,7 +1270,11 @@ impl<'db> Type<'db> {
///
/// It also avoids literal promotion if a literal type annotation was provided as type context.
pub(crate) fn promote_literals(self, db: &'db dyn Db, tcx: TypeContext<'db>) -> Type<'db> {
self.apply_type_mapping(db, &TypeMapping::PromoteLiterals, tcx)
self.apply_type_mapping(
db,
&TypeMapping::PromoteLiterals(PromoteLiteralsMode::On),
tcx,
)
}
/// Like [`Type::promote_literals`], but does not recurse into nested types.
@ -6765,7 +6769,7 @@ impl<'db> Type<'db> {
self
}
}
TypeMapping::PromoteLiterals
TypeMapping::PromoteLiterals(_)
| TypeMapping::ReplaceParameterDefaults
| TypeMapping::BindLegacyTypevars(_) => self,
TypeMapping::Materialize(materialization_kind) => {
@ -6779,7 +6783,7 @@ impl<'db> Type<'db> {
}
TypeMapping::Specialization(_) |
TypeMapping::PartialSpecialization(_) |
TypeMapping::PromoteLiterals |
TypeMapping::PromoteLiterals(_) |
TypeMapping::BindSelf(_) |
TypeMapping::ReplaceSelf { .. } |
TypeMapping::Materialize(_) |
@ -6790,7 +6794,7 @@ impl<'db> Type<'db> {
let function = Type::FunctionLiteral(function.apply_type_mapping_impl(db, type_mapping, tcx, visitor));
match type_mapping {
TypeMapping::PromoteLiterals => function.promote_literals_impl(db, tcx),
TypeMapping::PromoteLiterals(PromoteLiteralsMode::On) => function.promote_literals_impl(db, tcx),
_ => function
}
}
@ -6867,13 +6871,9 @@ impl<'db> Type<'db> {
builder =
builder.add_positive(positive.apply_type_mapping_impl(db, type_mapping, tcx, visitor));
}
let flipped_mapping = match type_mapping {
TypeMapping::Materialize(materialization_kind) => &TypeMapping::Materialize(materialization_kind.flip()),
_ => type_mapping,
};
for negative in intersection.negative(db) {
builder =
builder.add_negative(negative.apply_type_mapping_impl(db, flipped_mapping, tcx, visitor));
builder.add_negative(negative.apply_type_mapping_impl(db, &type_mapping.flip(), tcx, visitor));
}
builder.build()
}
@ -6902,8 +6902,9 @@ impl<'db> Type<'db> {
TypeMapping::BindSelf(_) |
TypeMapping::ReplaceSelf { .. } |
TypeMapping::Materialize(_) |
TypeMapping::ReplaceParameterDefaults => self,
TypeMapping::PromoteLiterals => self.promote_literals_impl(db, tcx)
TypeMapping::ReplaceParameterDefaults |
TypeMapping::PromoteLiterals(PromoteLiteralsMode::Off) => self,
TypeMapping::PromoteLiterals(PromoteLiteralsMode::On) => self.promote_literals_impl(db, tcx)
}
Type::Dynamic(_) => match type_mapping {
@ -6912,7 +6913,7 @@ impl<'db> Type<'db> {
TypeMapping::BindLegacyTypevars(_) |
TypeMapping::BindSelf(_) |
TypeMapping::ReplaceSelf { .. } |
TypeMapping::PromoteLiterals |
TypeMapping::PromoteLiterals(_) |
TypeMapping::ReplaceParameterDefaults => self,
TypeMapping::Materialize(materialization_kind) => match materialization_kind {
MaterializationKind::Top => Type::object(),
@ -7456,6 +7457,21 @@ fn apply_specialization_cycle_initial<'db>(
Type::Never
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)]
pub enum PromoteLiteralsMode {
On,
Off,
}
impl PromoteLiteralsMode {
const fn flip(self) -> Self {
match self {
PromoteLiteralsMode::On => PromoteLiteralsMode::Off,
PromoteLiteralsMode::Off => PromoteLiteralsMode::On,
}
}
}
/// A mapping that can be applied to a type, producing another type. This is applied inductively to
/// the components of complex types.
///
@ -7470,7 +7486,7 @@ pub enum TypeMapping<'a, 'db> {
PartialSpecialization(PartialSpecialization<'a, 'db>),
/// Replaces any literal types with their corresponding promoted type form (e.g. `Literal["string"]`
/// to `str`, or `def _() -> int` to `Callable[[], int]`).
PromoteLiterals,
PromoteLiterals(PromoteLiteralsMode),
/// Binds a legacy typevar with the generic context (class, function, type alias) that it is
/// being used in.
BindLegacyTypevars(BindingContext<'db>),
@ -7495,7 +7511,7 @@ impl<'db> TypeMapping<'_, 'db> {
match self {
TypeMapping::Specialization(_)
| TypeMapping::PartialSpecialization(_)
| TypeMapping::PromoteLiterals
| TypeMapping::PromoteLiterals(_)
| TypeMapping::BindLegacyTypevars(_)
| TypeMapping::Materialize(_)
| TypeMapping::ReplaceParameterDefaults => context,
@ -7521,6 +7537,22 @@ impl<'db> TypeMapping<'_, 'db> {
),
}
}
/// Returns a new `TypeMapping` that should be applied in contravariant positions.
pub(crate) fn flip(&self) -> Self {
match self {
TypeMapping::Materialize(materialization_kind) => {
TypeMapping::Materialize(materialization_kind.flip())
}
TypeMapping::PromoteLiterals(mode) => TypeMapping::PromoteLiterals(mode.flip()),
TypeMapping::Specialization(_)
| TypeMapping::PartialSpecialization(_)
| TypeMapping::BindLegacyTypevars(_)
| TypeMapping::BindSelf(_)
| TypeMapping::ReplaceSelf { .. }
| TypeMapping::ReplaceParameterDefaults => self.clone(),
}
}
}
/// A Salsa-tracked constraint set. This is only needed to have something appropriately small to

View File

@ -509,20 +509,17 @@ impl<'db> Signature<'db> {
tcx: TypeContext<'db>,
visitor: &ApplyTypeMappingVisitor<'db>,
) -> Self {
let flipped_mapping = match type_mapping {
TypeMapping::Materialize(materialization_kind) => {
&TypeMapping::Materialize(materialization_kind.flip())
}
_ => type_mapping,
};
Self {
generic_context: self
.generic_context
.map(|context| type_mapping.update_signature_generic_context(db, context)),
definition: self.definition,
parameters: self
.parameters
.apply_type_mapping_impl(db, flipped_mapping, tcx, visitor),
parameters: self.parameters.apply_type_mapping_impl(
db,
&type_mapping.flip(),
tcx,
visitor,
),
return_ty: self
.return_ty
.map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)),