From 0639da255263219e8cf0289301661b748681db7c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 2 Oct 2025 07:52:47 +0100 Subject: [PATCH] [ty] `~T` should never be assignable to `T` (#20606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Currently we do not emit an error on this code: ```py from ty_extensions import Not def f[T](x: T, y: Not[T]) -> T: x = y return x ``` But we should do! `~T` should never be assignable to `T`. This fixes a small regression introduced in https://github.com/astral-sh/ruff/commit/14fe1228e72dca90db6b3dbb7e07f973d175ea0e#diff-8049ab5af787dba29daa389bbe2b691560c15461ef536f122b1beab112a4b48aR1443-R1446, where a branch that previously returned `false` was replaced with a branch that returns `C::always_satisfiable` -- the opposite of what it used to be! The regression occurred because we didn't have any tests for this -- so I added some tests in this PR that fail on `main`. I only spotted the problem because I was going through the code of `has_relation_to_impl` with a fine toothcomb for https://github.com/astral-sh/ruff/pull/20602 😄 --- .../resources/mdtest/generics/legacy/functions.md | 14 ++++++++++++++ .../resources/mdtest/generics/pep695/functions.md | 11 +++++++++++ crates/ty_python_semantic/src/types.rs | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md index 50a5be7058..34c4f04ead 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -530,3 +530,17 @@ age, name = team.employees[0] reveal_type(age) # revealed: Age reveal_type(name) # revealed: Name ``` + +## `~T` is never assignable to `T` + +```py +from typing import TypeVar +from ty_extensions import Not + +T = TypeVar("T") + +def f(x: T, y: Not[T]) -> T: + x = y # error: [invalid-assignment] + y = x # error: [invalid-assignment] + return x +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index 1193e91ba5..dd75b2c56a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -543,3 +543,14 @@ def _(x: int): reveal_type(C().implicit_self(x)) # revealed: tuple[C, int] ``` + +## `~T` is never assignable to `T` + +```py +from ty_extensions import Not + +def f[T](x: T, y: Not[T]) -> T: + x = y # error: [invalid-assignment] + y = x # error: [invalid-assignment] + return x +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index a9111f3581..916f9471a0 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1567,7 +1567,7 @@ impl<'db> Type<'db> { (Type::Intersection(intersection), Type::NonInferableTypeVar(_)) if intersection.negative(db).contains(&target) => { - ConstraintSet::from(true) + ConstraintSet::from(false) } // Two identical typevars must always solve to the same type, so they are always