[red-knot] Surround intersections with () in potentially ambiguous contexts (#17568)

## Summary

Add parentheses to multi-element intersections, when displayed in a
context that's otherwise potentially ambiguous.

## Test Plan

Update mdtest files

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Matthew Mckee 2025-04-23 05:18:20 +01:00 committed by GitHub
parent f9da115fdc
commit aa46047649
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 48 additions and 41 deletions

View file

@ -13,7 +13,7 @@ reveal_type(1 is not 1) # revealed: bool
reveal_type(1 is 2) # revealed: Literal[False]
reveal_type(1 is not 7) # revealed: Literal[True]
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`"
reveal_type(1 <= "" and 0 < 1) # revealed: Unknown & ~AlwaysTruthy | Literal[True]
reveal_type(1 <= "" and 0 < 1) # revealed: (Unknown & ~AlwaysTruthy) | Literal[True]
```
## Integer instance

View file

@ -37,7 +37,7 @@ class C:
return self
x = A() < B() < C()
reveal_type(x) # revealed: A & ~AlwaysTruthy | B
reveal_type(x) # revealed: (A & ~AlwaysTruthy) | B
y = 0 < 1 < A() < 3
reveal_type(y) # revealed: Literal[False] | A

View file

@ -10,8 +10,8 @@ def _(foo: str):
reveal_type(False or "z") # revealed: Literal["z"]
reveal_type(False or True) # revealed: Literal[True]
reveal_type(False or False) # revealed: Literal[False]
reveal_type(foo or False) # revealed: str & ~AlwaysFalsy | Literal[False]
reveal_type(foo or True) # revealed: str & ~AlwaysFalsy | Literal[True]
reveal_type(foo or False) # revealed: (str & ~AlwaysFalsy) | Literal[False]
reveal_type(foo or True) # revealed: (str & ~AlwaysFalsy) | Literal[True]
```
## AND
@ -20,8 +20,8 @@ def _(foo: str):
def _(foo: str):
reveal_type(True and False) # revealed: Literal[False]
reveal_type(False and True) # revealed: Literal[False]
reveal_type(foo and False) # revealed: str & ~AlwaysTruthy | Literal[False]
reveal_type(foo and True) # revealed: str & ~AlwaysTruthy | Literal[True]
reveal_type(foo and False) # revealed: (str & ~AlwaysTruthy) | Literal[False]
reveal_type(foo and True) # revealed: (str & ~AlwaysTruthy) | Literal[True]
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
reveal_type("x" and "y" and "") # revealed: Literal[""]
reveal_type("" and "y") # revealed: Literal[""]

View file

@ -191,9 +191,9 @@ def _(
i2: Intersection[P | Q | R, S],
i3: Intersection[P | Q, R | S],
) -> None:
reveal_type(i1) # revealed: P & Q | P & R | P & S
reveal_type(i2) # revealed: P & S | Q & S | R & S
reveal_type(i3) # revealed: P & R | Q & R | P & S | Q & S
reveal_type(i1) # revealed: (P & Q) | (P & R) | (P & S)
reveal_type(i2) # revealed: (P & S) | (Q & S) | (R & S)
reveal_type(i3) # revealed: (P & R) | (Q & R) | (P & S) | (Q & S)
def simplifications_for_same_elements(
i1: Intersection[P, Q | P],
@ -216,7 +216,7 @@ def simplifications_for_same_elements(
# = P & Q | P & R | Q | Q & R
# = Q | P & R
# (again, because Q is a supertype of P & Q and of Q & R)
reveal_type(i3) # revealed: Q | P & R
reveal_type(i3) # revealed: Q | (P & R)
# (P | Q) & (P | Q)
# = P & P | P & Q | Q & P | Q & Q

View file

@ -10,7 +10,7 @@ def _(x: A | B):
if isinstance(x, A) and isinstance(x, B):
reveal_type(x) # revealed: A & B
else:
reveal_type(x) # revealed: B & ~A | A & ~B
reveal_type(x) # revealed: (B & ~A) | (A & ~B)
```
## Arms might not add narrowing constraints
@ -131,8 +131,8 @@ def _(x: A | B | C, y: A | B | C):
# The same for `y`
reveal_type(y) # revealed: A | B | C
else:
reveal_type(x) # revealed: B & ~A | C & ~A
reveal_type(y) # revealed: B & ~A | C & ~A
reveal_type(x) # revealed: (B & ~A) | (C & ~A)
reveal_type(y) # revealed: (B & ~A) | (C & ~A)
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
@ -155,7 +155,7 @@ def _(x: A | B | C):
reveal_type(x) # revealed: B & ~C
else:
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
reveal_type(x) # revealed: A & ~B | C
reveal_type(x) # revealed: (A & ~B) | C
```
## mixing `or` and `not`
@ -167,7 +167,7 @@ class C: ...
def _(x: A | B | C):
if isinstance(x, B) or not isinstance(x, C):
reveal_type(x) # revealed: B | A & ~C
reveal_type(x) # revealed: B | (A & ~C)
else:
reveal_type(x) # revealed: C & ~B
```
@ -181,7 +181,7 @@ class C: ...
def _(x: A | B | C):
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
reveal_type(x) # revealed: A | B & ~C
reveal_type(x) # revealed: A | (B & ~C)
else:
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
reveal_type(x) # revealed: C & ~A
@ -197,7 +197,7 @@ class C: ...
def _(x: A | B | C):
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
# A & (B | ~C) -> (A & B) | (A & ~C)
reveal_type(x) # revealed: A & B | A & ~C
reveal_type(x) # revealed: (A & B) | (A & ~C)
else:
# ~((A & B) | (A & ~C)) ->
# ~(A & B) & ~(A & ~C) ->
@ -206,7 +206,7 @@ def _(x: A | B | C):
# ~A | (~A & C) | (~B & C) ->
# ~A | (C & ~B) ->
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B
reveal_type(x) # revealed: (B & ~A) | (C & ~A) | (C & ~B)
```
## Boolean expression internal narrowing

View file

@ -82,19 +82,19 @@ class B: ...
def f(x: A | B):
if x:
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy
reveal_type(x) # revealed: (A & ~AlwaysFalsy) | (B & ~AlwaysFalsy)
else:
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy
reveal_type(x) # revealed: (A & ~AlwaysTruthy) | (B & ~AlwaysTruthy)
if x and not x:
reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy
reveal_type(x) # revealed: (A & ~AlwaysFalsy & ~AlwaysTruthy) | (B & ~AlwaysFalsy & ~AlwaysTruthy)
else:
reveal_type(x) # revealed: A | B
if x or not x:
reveal_type(x) # revealed: A | B
else:
reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy
reveal_type(x) # revealed: (A & ~AlwaysTruthy & ~AlwaysFalsy) | (B & ~AlwaysTruthy & ~AlwaysFalsy)
```
### Truthiness of Types
@ -111,9 +111,9 @@ x = int if flag() else str
reveal_type(x) # revealed: Literal[int, str]
if x:
reveal_type(x) # revealed: Literal[int] & ~AlwaysFalsy | Literal[str] & ~AlwaysFalsy
reveal_type(x) # revealed: (Literal[int] & ~AlwaysFalsy) | (Literal[str] & ~AlwaysFalsy)
else:
reveal_type(x) # revealed: Literal[int] & ~AlwaysTruthy | Literal[str] & ~AlwaysTruthy
reveal_type(x) # revealed: (Literal[int] & ~AlwaysTruthy) | (Literal[str] & ~AlwaysTruthy)
```
## Determined Truthiness
@ -176,12 +176,12 @@ if isinstance(x, str) and not isinstance(x, B):
z = x if flag() else y
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42, "", "hello"]
reveal_type(z) # revealed: (A & str & ~B) | Literal[0, 42, "", "hello"]
if z:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42, "hello"]
reveal_type(z) # revealed: (A & str & ~B & ~AlwaysFalsy) | Literal[42, "hello"]
else:
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0, ""]
reveal_type(z) # revealed: (A & str & ~B & ~AlwaysTruthy) | Literal[0, ""]
```
## Narrowing Multiple Variables
@ -264,7 +264,7 @@ def _(
):
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass]
if ta:
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass] & ~AlwaysFalsy
reveal_type(ta) # revealed: type[TruthyClass] | (type[AmbiguousClass] & ~AlwaysFalsy)
reveal_type(af) # revealed: type[AmbiguousClass] | type[FalsyClass]
if af:
@ -296,12 +296,12 @@ def _(x: Literal[0, 1]):
reveal_type(x and A()) # revealed: Literal[0] | A
def _(x: str):
reveal_type(x or A()) # revealed: str & ~AlwaysFalsy | A
reveal_type(x and A()) # revealed: str & ~AlwaysTruthy | A
reveal_type(x or A()) # revealed: (str & ~AlwaysFalsy) | A
reveal_type(x and A()) # revealed: (str & ~AlwaysTruthy) | A
def _(x: bool | str):
reveal_type(x or A()) # revealed: Literal[True] | str & ~AlwaysFalsy | A
reveal_type(x and A()) # revealed: Literal[False] | str & ~AlwaysTruthy | A
reveal_type(x or A()) # revealed: Literal[True] | (str & ~AlwaysFalsy) | A
reveal_type(x and A()) # revealed: Literal[False] | (str & ~AlwaysTruthy) | A
class Falsy:
def __bool__(self) -> Literal[False]:

View file

@ -127,7 +127,7 @@ class B: ...
def _[T](x: A | B):
if type(x) is A[str]:
reveal_type(x) # revealed: A[int] & A[Unknown] | B & A[Unknown]
reveal_type(x) # revealed: (A[int] & A[Unknown]) | (B & A[Unknown])
else:
reveal_type(x) # revealed: A[int] | B
```

View file

@ -6993,6 +6993,10 @@ impl<'db> IntersectionType<'db> {
pub fn iter_positive(&self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> {
self.positive(db).iter().copied()
}
pub fn has_one_element(&self, db: &'db dyn Db) -> bool {
(self.positive(db).len() + self.negative(db).len()) == 1
}
}
#[salsa::interned(debug)]

View file

@ -679,14 +679,17 @@ struct DisplayMaybeParenthesizedType<'db> {
impl Display for DisplayMaybeParenthesizedType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Type::Callable(_)
| Type::MethodWrapper(_)
| Type::FunctionLiteral(_)
| Type::BoundMethod(_) = self.ty
{
write!(f, "({})", self.ty.display(self.db))
} else {
self.ty.display(self.db).fmt(f)
let write_parentheses = |f: &mut Formatter<'_>| write!(f, "({})", self.ty.display(self.db));
match self.ty {
Type::Callable(_)
| Type::MethodWrapper(_)
| Type::FunctionLiteral(_)
| Type::BoundMethod(_)
| Type::Union(_) => write_parentheses(f),
Type::Intersection(intersection) if !intersection.has_one_element(self.db) => {
write_parentheses(f)
}
_ => self.ty.display(self.db).fmt(f),
}
}
}