mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-24 13:33:50 +00:00
[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:
parent
f9da115fdc
commit
aa46047649
9 changed files with 48 additions and 41 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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[""]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue