From aa46047649b60ae04bc3578445697ffb182d17fe Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Wed, 23 Apr 2025 05:18:20 +0100 Subject: [PATCH] [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 --- .../resources/mdtest/comparison/integers.md | 2 +- .../mdtest/comparison/non_bool_returns.md | 2 +- .../resources/mdtest/expression/boolean.md | 8 +++--- .../resources/mdtest/intersection_types.md | 8 +++--- .../mdtest/narrow/conditionals/boolean.md | 16 +++++------ .../resources/mdtest/narrow/truthiness.md | 28 +++++++++---------- .../resources/mdtest/narrow/type.md | 2 +- crates/red_knot_python_semantic/src/types.rs | 4 +++ .../src/types/display.rs | 19 +++++++------ 9 files changed, 48 insertions(+), 41 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md index bf956e8413..eeccd6a60a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/integers.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md b/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md index 6cf77e5f1e..2da190aa5d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md +++ b/crates/red_knot_python_semantic/resources/mdtest/comparison/non_bool_returns.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md b/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md index ce3363636d..160189ef0c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md @@ -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[""] diff --git a/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md b/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md index 51bf653ef6..d2f409036c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md +++ b/crates/red_knot_python_semantic/resources/mdtest/intersection_types.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md index 566ec10a78..8b9394a380 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/boolean.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md index f8f5ab8c5e..d9bf54e8f8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md @@ -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]: diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md index 602265039d..07cabd76d5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md @@ -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 ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 87f4715269..f65054c182 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -6993,6 +6993,10 @@ impl<'db> IntersectionType<'db> { pub fn iter_positive(&self, db: &'db dyn Db) -> impl Iterator> { 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)] diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 5687b6c35c..70e8cd99cf 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -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), } } }