Skip to content

Commit aa46047

Browse files
[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>
1 parent f9da115 commit aa46047

File tree

9 files changed

+48
-41
lines changed

9 files changed

+48
-41
lines changed

crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ reveal_type(1 is not 1) # revealed: bool
1313
reveal_type(1 is 2) # revealed: Literal[False]
1414
reveal_type(1 is not 7) # revealed: Literal[True]
1515
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`"
16-
reveal_type(1 <= "" and 0 < 1) # revealed: Unknown & ~AlwaysTruthy | Literal[True]
16+
reveal_type(1 <= "" and 0 < 1) # revealed: (Unknown & ~AlwaysTruthy) | Literal[True]
1717
```
1818

1919
## Integer instance

crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class C:
3737
return self
3838

3939
x = A() < B() < C()
40-
reveal_type(x) # revealed: A & ~AlwaysTruthy | B
40+
reveal_type(x) # revealed: (A & ~AlwaysTruthy) | B
4141

4242
y = 0 < 1 < A() < 3
4343
reveal_type(y) # revealed: Literal[False] | A

crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ def _(foo: str):
1010
reveal_type(False or "z") # revealed: Literal["z"]
1111
reveal_type(False or True) # revealed: Literal[True]
1212
reveal_type(False or False) # revealed: Literal[False]
13-
reveal_type(foo or False) # revealed: str & ~AlwaysFalsy | Literal[False]
14-
reveal_type(foo or True) # revealed: str & ~AlwaysFalsy | Literal[True]
13+
reveal_type(foo or False) # revealed: (str & ~AlwaysFalsy) | Literal[False]
14+
reveal_type(foo or True) # revealed: (str & ~AlwaysFalsy) | Literal[True]
1515
```
1616

1717
## AND
@@ -20,8 +20,8 @@ def _(foo: str):
2020
def _(foo: str):
2121
reveal_type(True and False) # revealed: Literal[False]
2222
reveal_type(False and True) # revealed: Literal[False]
23-
reveal_type(foo and False) # revealed: str & ~AlwaysTruthy | Literal[False]
24-
reveal_type(foo and True) # revealed: str & ~AlwaysTruthy | Literal[True]
23+
reveal_type(foo and False) # revealed: (str & ~AlwaysTruthy) | Literal[False]
24+
reveal_type(foo and True) # revealed: (str & ~AlwaysTruthy) | Literal[True]
2525
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
2626
reveal_type("x" and "y" and "") # revealed: Literal[""]
2727
reveal_type("" and "y") # revealed: Literal[""]

crates/red_knot_python_semantic/resources/mdtest/intersection_types.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,9 @@ def _(
191191
i2: Intersection[P | Q | R, S],
192192
i3: Intersection[P | Q, R | S],
193193
) -> None:
194-
reveal_type(i1) # revealed: P & Q | P & R | P & S
195-
reveal_type(i2) # revealed: P & S | Q & S | R & S
196-
reveal_type(i3) # revealed: P & R | Q & R | P & S | Q & S
194+
reveal_type(i1) # revealed: (P & Q) | (P & R) | (P & S)
195+
reveal_type(i2) # revealed: (P & S) | (Q & S) | (R & S)
196+
reveal_type(i3) # revealed: (P & R) | (Q & R) | (P & S) | (Q & S)
197197

198198
def simplifications_for_same_elements(
199199
i1: Intersection[P, Q | P],
@@ -216,7 +216,7 @@ def simplifications_for_same_elements(
216216
# = P & Q | P & R | Q | Q & R
217217
# = Q | P & R
218218
# (again, because Q is a supertype of P & Q and of Q & R)
219-
reveal_type(i3) # revealed: Q | P & R
219+
reveal_type(i3) # revealed: Q | (P & R)
220220

221221
# (P | Q) & (P | Q)
222222
# = P & P | P & Q | Q & P | Q & Q

crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def _(x: A | B):
1010
if isinstance(x, A) and isinstance(x, B):
1111
reveal_type(x) # revealed: A & B
1212
else:
13-
reveal_type(x) # revealed: B & ~A | A & ~B
13+
reveal_type(x) # revealed: (B & ~A) | (A & ~B)
1414
```
1515

1616
## Arms might not add narrowing constraints
@@ -131,8 +131,8 @@ def _(x: A | B | C, y: A | B | C):
131131
# The same for `y`
132132
reveal_type(y) # revealed: A | B | C
133133
else:
134-
reveal_type(x) # revealed: B & ~A | C & ~A
135-
reveal_type(y) # revealed: B & ~A | C & ~A
134+
reveal_type(x) # revealed: (B & ~A) | (C & ~A)
135+
reveal_type(y) # revealed: (B & ~A) | (C & ~A)
136136

137137
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
138138
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
@@ -155,7 +155,7 @@ def _(x: A | B | C):
155155
reveal_type(x) # revealed: B & ~C
156156
else:
157157
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
158-
reveal_type(x) # revealed: A & ~B | C
158+
reveal_type(x) # revealed: (A & ~B) | C
159159
```
160160

161161
## mixing `or` and `not`
@@ -167,7 +167,7 @@ class C: ...
167167

168168
def _(x: A | B | C):
169169
if isinstance(x, B) or not isinstance(x, C):
170-
reveal_type(x) # revealed: B | A & ~C
170+
reveal_type(x) # revealed: B | (A & ~C)
171171
else:
172172
reveal_type(x) # revealed: C & ~B
173173
```
@@ -181,7 +181,7 @@ class C: ...
181181

182182
def _(x: A | B | C):
183183
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
184-
reveal_type(x) # revealed: A | B & ~C
184+
reveal_type(x) # revealed: A | (B & ~C)
185185
else:
186186
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
187187
reveal_type(x) # revealed: C & ~A
@@ -197,7 +197,7 @@ class C: ...
197197
def _(x: A | B | C):
198198
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
199199
# A & (B | ~C) -> (A & B) | (A & ~C)
200-
reveal_type(x) # revealed: A & B | A & ~C
200+
reveal_type(x) # revealed: (A & B) | (A & ~C)
201201
else:
202202
# ~((A & B) | (A & ~C)) ->
203203
# ~(A & B) & ~(A & ~C) ->
@@ -206,7 +206,7 @@ def _(x: A | B | C):
206206
# ~A | (~A & C) | (~B & C) ->
207207
# ~A | (C & ~B) ->
208208
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
209-
reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B
209+
reveal_type(x) # revealed: (B & ~A) | (C & ~A) | (C & ~B)
210210
```
211211

212212
## Boolean expression internal narrowing

crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,19 @@ class B: ...
8282

8383
def f(x: A | B):
8484
if x:
85-
reveal_type(x) # revealed: A & ~AlwaysFalsy | B & ~AlwaysFalsy
85+
reveal_type(x) # revealed: (A & ~AlwaysFalsy) | (B & ~AlwaysFalsy)
8686
else:
87-
reveal_type(x) # revealed: A & ~AlwaysTruthy | B & ~AlwaysTruthy
87+
reveal_type(x) # revealed: (A & ~AlwaysTruthy) | (B & ~AlwaysTruthy)
8888

8989
if x and not x:
90-
reveal_type(x) # revealed: A & ~AlwaysFalsy & ~AlwaysTruthy | B & ~AlwaysFalsy & ~AlwaysTruthy
90+
reveal_type(x) # revealed: (A & ~AlwaysFalsy & ~AlwaysTruthy) | (B & ~AlwaysFalsy & ~AlwaysTruthy)
9191
else:
9292
reveal_type(x) # revealed: A | B
9393

9494
if x or not x:
9595
reveal_type(x) # revealed: A | B
9696
else:
97-
reveal_type(x) # revealed: A & ~AlwaysTruthy & ~AlwaysFalsy | B & ~AlwaysTruthy & ~AlwaysFalsy
97+
reveal_type(x) # revealed: (A & ~AlwaysTruthy & ~AlwaysFalsy) | (B & ~AlwaysTruthy & ~AlwaysFalsy)
9898
```
9999

100100
### Truthiness of Types
@@ -111,9 +111,9 @@ x = int if flag() else str
111111
reveal_type(x) # revealed: Literal[int, str]
112112

113113
if x:
114-
reveal_type(x) # revealed: Literal[int] & ~AlwaysFalsy | Literal[str] & ~AlwaysFalsy
114+
reveal_type(x) # revealed: (Literal[int] & ~AlwaysFalsy) | (Literal[str] & ~AlwaysFalsy)
115115
else:
116-
reveal_type(x) # revealed: Literal[int] & ~AlwaysTruthy | Literal[str] & ~AlwaysTruthy
116+
reveal_type(x) # revealed: (Literal[int] & ~AlwaysTruthy) | (Literal[str] & ~AlwaysTruthy)
117117
```
118118

119119
## Determined Truthiness
@@ -176,12 +176,12 @@ if isinstance(x, str) and not isinstance(x, B):
176176

177177
z = x if flag() else y
178178

179-
reveal_type(z) # revealed: A & str & ~B | Literal[0, 42, "", "hello"]
179+
reveal_type(z) # revealed: (A & str & ~B) | Literal[0, 42, "", "hello"]
180180

181181
if z:
182-
reveal_type(z) # revealed: A & str & ~B & ~AlwaysFalsy | Literal[42, "hello"]
182+
reveal_type(z) # revealed: (A & str & ~B & ~AlwaysFalsy) | Literal[42, "hello"]
183183
else:
184-
reveal_type(z) # revealed: A & str & ~B & ~AlwaysTruthy | Literal[0, ""]
184+
reveal_type(z) # revealed: (A & str & ~B & ~AlwaysTruthy) | Literal[0, ""]
185185
```
186186

187187
## Narrowing Multiple Variables
@@ -264,7 +264,7 @@ def _(
264264
):
265265
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass]
266266
if ta:
267-
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass] & ~AlwaysFalsy
267+
reveal_type(ta) # revealed: type[TruthyClass] | (type[AmbiguousClass] & ~AlwaysFalsy)
268268

269269
reveal_type(af) # revealed: type[AmbiguousClass] | type[FalsyClass]
270270
if af:
@@ -296,12 +296,12 @@ def _(x: Literal[0, 1]):
296296
reveal_type(x and A()) # revealed: Literal[0] | A
297297

298298
def _(x: str):
299-
reveal_type(x or A()) # revealed: str & ~AlwaysFalsy | A
300-
reveal_type(x and A()) # revealed: str & ~AlwaysTruthy | A
299+
reveal_type(x or A()) # revealed: (str & ~AlwaysFalsy) | A
300+
reveal_type(x and A()) # revealed: (str & ~AlwaysTruthy) | A
301301

302302
def _(x: bool | str):
303-
reveal_type(x or A()) # revealed: Literal[True] | str & ~AlwaysFalsy | A
304-
reveal_type(x and A()) # revealed: Literal[False] | str & ~AlwaysTruthy | A
303+
reveal_type(x or A()) # revealed: Literal[True] | (str & ~AlwaysFalsy) | A
304+
reveal_type(x and A()) # revealed: Literal[False] | (str & ~AlwaysTruthy) | A
305305

306306
class Falsy:
307307
def __bool__(self) -> Literal[False]:

crates/red_knot_python_semantic/resources/mdtest/narrow/type.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ class B: ...
127127

128128
def _[T](x: A | B):
129129
if type(x) is A[str]:
130-
reveal_type(x) # revealed: A[int] & A[Unknown] | B & A[Unknown]
130+
reveal_type(x) # revealed: (A[int] & A[Unknown]) | (B & A[Unknown])
131131
else:
132132
reveal_type(x) # revealed: A[int] | B
133133
```

crates/red_knot_python_semantic/src/types.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6993,6 +6993,10 @@ impl<'db> IntersectionType<'db> {
69936993
pub fn iter_positive(&self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> {
69946994
self.positive(db).iter().copied()
69956995
}
6996+
6997+
pub fn has_one_element(&self, db: &'db dyn Db) -> bool {
6998+
(self.positive(db).len() + self.negative(db).len()) == 1
6999+
}
69967000
}
69977001

69987002
#[salsa::interned(debug)]

crates/red_knot_python_semantic/src/types/display.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -679,14 +679,17 @@ struct DisplayMaybeParenthesizedType<'db> {
679679

680680
impl Display for DisplayMaybeParenthesizedType<'_> {
681681
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
682-
if let Type::Callable(_)
683-
| Type::MethodWrapper(_)
684-
| Type::FunctionLiteral(_)
685-
| Type::BoundMethod(_) = self.ty
686-
{
687-
write!(f, "({})", self.ty.display(self.db))
688-
} else {
689-
self.ty.display(self.db).fmt(f)
682+
let write_parentheses = |f: &mut Formatter<'_>| write!(f, "({})", self.ty.display(self.db));
683+
match self.ty {
684+
Type::Callable(_)
685+
| Type::MethodWrapper(_)
686+
| Type::FunctionLiteral(_)
687+
| Type::BoundMethod(_)
688+
| Type::Union(_) => write_parentheses(f),
689+
Type::Intersection(intersection) if !intersection.has_one_element(self.db) => {
690+
write_parentheses(f)
691+
}
692+
_ => self.ty.display(self.db).fmt(f),
690693
}
691694
}
692695
}

0 commit comments

Comments
 (0)