Skip to content

Commit b7555c7

Browse files
committed
Improve reachability in narrowing logic
1 parent 60d136e commit b7555c7

11 files changed

Lines changed: 120 additions & 80 deletions

File tree

mypy/checker.py

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6676,7 +6676,8 @@ def narrow_type_by_identity_equality(
66766676
else:
66776677
raise AssertionError
66786678

6679-
partial_type_maps = []
6679+
all_if_maps: list[TypeMap] = []
6680+
all_else_maps: list[TypeMap] = []
66806681

66816682
# For each narrowable index, we see what we can narrow based on each relevant target
66826683
for i in expr_indices:
@@ -6715,13 +6716,13 @@ def narrow_type_by_identity_equality(
67156716
continue
67166717

67176718
target = TypeRange(target_type, is_upper_bound=False)
6718-
is_value_target = is_target_for_value_narrowing(get_proper_type(target_type))
67196719

6720-
if is_value_target:
6720+
if is_target_for_value_narrowing(get_proper_type(target_type)):
67216721
if_map, else_map = conditional_types_to_typemaps(
67226722
operands[i], *conditional_types(expanded_expr_type, [target])
67236723
)
6724-
partial_type_maps.append((if_map, else_map))
6724+
all_if_maps.append(if_map)
6725+
all_else_maps.append(else_map)
67256726
else:
67266727
if_map, else_map = conditional_types_to_typemaps(
67276728
operands[i], *conditional_types(expr_type, [target])
@@ -6731,8 +6732,7 @@ def narrow_type_by_identity_equality(
67316732
# However, for non-value targets, we cannot do this narrowing,
67326733
# and so we ignore else_map
67336734
# e.g. if (x: str | None) != (y: str), we cannot narrow x to None
6734-
if if_map:
6735-
partial_type_maps.append((if_map, {}))
6735+
all_if_maps.append(if_map)
67366736

67376737
# Handle narrowing for operands with custom __eq__ methods specially
67386738
# In most cases, we won't be able to do any narrowing
@@ -6754,14 +6754,12 @@ def narrow_type_by_identity_equality(
67546754
if should_coerce_literals:
67556755
target_type = coerce_to_literal(target_type)
67566756
target = TypeRange(target_type, is_upper_bound=False)
6757-
is_value_target = is_target_for_value_narrowing(get_proper_type(target_type))
6758-
6759-
if is_value_target:
6757+
if is_target_for_value_narrowing(get_proper_type(target_type)):
67606758
if_map, else_map = conditional_types_to_typemaps(
67616759
operands[i], *conditional_types(expr_type, [target])
67626760
)
67636761
if else_map:
6764-
partial_type_maps.append(({}, else_map))
6762+
all_else_maps.append(else_map)
67656763
continue
67666764

67676765
# If our operand with custom __eq__ is a union, where only some members of the union
@@ -6794,18 +6792,8 @@ def narrow_type_by_identity_equality(
67946792
if is_value_target:
67956793
or_else_maps.append(else_map)
67966794

6797-
final_if_map: TypeMap = {}
6798-
final_else_map: TypeMap = {}
6799-
if or_if_maps:
6800-
final_if_map = or_if_maps[0]
6801-
for if_map in or_if_maps[1:]:
6802-
final_if_map = or_conditional_maps(final_if_map, if_map)
6803-
if or_else_maps:
6804-
final_else_map = or_else_maps[0]
6805-
for else_map in or_else_maps[1:]:
6806-
final_else_map = or_conditional_maps(final_else_map, else_map)
6807-
6808-
partial_type_maps.append((final_if_map, final_else_map))
6795+
all_if_maps.append(reduce_or_conditional_type_maps(or_if_maps))
6796+
all_else_maps.append(reduce_or_conditional_type_maps(or_else_maps))
68096797

68106798
# Handle narrowing for comparisons that produce additional narrowing, like
68116799
# `type(x) == T` or `x.__class__ is T`
@@ -6839,13 +6827,16 @@ def narrow_type_by_identity_equality(
68396827
if isinstance(expr, RefExpr) and isinstance(expr.node, TypeInfo)
68406828
else False
68416829
)
6842-
if not is_final:
6843-
else_map = {}
6844-
partial_type_maps.append((if_map, else_map))
6830+
all_if_maps.append(if_map)
6831+
if is_final:
6832+
# We can only narrow `type(x) == T` in the negative case if T is final
6833+
all_else_maps.append(else_map)
68456834

68466835
# We will not have duplicate entries in our type maps if we only have two operands,
68476836
# so we can skip running meets on the intersections
6848-
return reduce_conditional_maps(partial_type_maps, use_meet=len(operands) > 2)
6837+
if_map = reduce_and_conditional_type_maps(all_if_maps, use_meet=len(operands) > 2)
6838+
else_map = reduce_or_conditional_type_maps(all_else_maps)
6839+
return if_map, else_map
68496840

68506841
def propagate_up_typemap_info(self, new_types: TypeMap) -> TypeMap:
68516842
"""Attempts refining parent expressions of any MemberExpr or IndexExprs in new_types.
@@ -8481,7 +8472,7 @@ def builtin_item_type(tp: Type) -> Type | None:
84818472
return None
84828473

84838474

8484-
def and_conditional_maps(m1: TypeMap, m2: TypeMap, use_meet: bool = False) -> TypeMap:
8475+
def and_conditional_maps(m1: TypeMap, m2: TypeMap, *, use_meet: bool = False) -> TypeMap:
84858476
"""Calculate what information we can learn from the truth of (e1 and e2)
84868477
in terms of the information that we can learn from the truth of e1 and
84878478
the truth of e2.
@@ -8514,7 +8505,7 @@ def and_conditional_maps(m1: TypeMap, m2: TypeMap, use_meet: bool = False) -> Ty
85148505
return result
85158506

85168507

8517-
def or_conditional_maps(m1: TypeMap, m2: TypeMap, coalesce_any: bool = False) -> TypeMap:
8508+
def or_conditional_maps(m1: TypeMap, m2: TypeMap, *, coalesce_any: bool = False) -> TypeMap:
85188509
"""Calculate what information we can learn from the truth of (e1 or e2)
85198510
in terms of the information that we can learn from the truth of e1 and
85208511
the truth of e2. If coalesce_any is True, consider Any a supertype when
@@ -8579,6 +8570,30 @@ def reduce_conditional_maps(
85798570
return final_if_map, final_else_map
85808571

85818572

8573+
def reduce_or_conditional_type_maps(ms: list[TypeMap]) -> TypeMap:
8574+
"""Reduces a list of TypeMaps into a single TypeMap by "or"-ing them together."""
8575+
if len(ms) == 0:
8576+
return {}
8577+
if len(ms) == 1:
8578+
return ms[0]
8579+
result = ms[0]
8580+
for m in ms[1:]:
8581+
result = or_conditional_maps(result, m)
8582+
return result
8583+
8584+
8585+
def reduce_and_conditional_type_maps(ms: list[TypeMap], *, use_meet: bool) -> TypeMap:
8586+
"""Reduces a list of TypeMaps into a single TypeMap by "and"-ing them together."""
8587+
if len(ms) == 0:
8588+
return {}
8589+
if len(ms) == 1:
8590+
return ms[0]
8591+
result = ms[0]
8592+
for m in ms[1:]:
8593+
result = and_conditional_maps(result, m, use_meet=use_meet)
8594+
return result
8595+
8596+
85828597
BUILTINS_CUSTOM_EQ_CHECKS: Final = {
85838598
"builtins.bytes",
85848599
"builtins.bytearray",

mypy/server/aststrip.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ def visit_class_def(self, node: ClassDef) -> None:
137137
node.base_type_exprs.extend(node.removed_base_type_exprs)
138138
node.removed_base_type_exprs = []
139139
node.defs.body = [
140-
s for s in node.defs.body if s not in to_delete # type: ignore[comparison-overlap]
140+
s
141+
for s in node.defs.body
142+
if s not in to_delete # type: ignore[comparison-overlap, redundant-expr]
141143
]
142144
with self.enter_class(node.info):
143145
super().visit_class_def(node)

mypy/stubutil.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
)
3232

3333
# Modules that may fail when imported, or that may have side effects (fully qualified).
34-
NOT_IMPORTABLE_MODULES = ()
34+
NOT_IMPORTABLE_MODULES: tuple[str, ...] = ()
3535

3636
# Typing constructs to be replaced by their builtin equivalents.
3737
TYPING_BUILTIN_REPLACEMENTS: Final = {

mypy/typeshed/stubs/mypy-extensions/mypy_extensions.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ class i64:
111111
def __ge__(self, x: i64) -> bool: ...
112112
def __gt__(self, x: i64) -> bool: ...
113113
def __index__(self) -> int: ...
114+
def __eq__(self, x: object) -> bool: ...
114115

115116
class i32:
116117
@overload
@@ -146,6 +147,7 @@ class i32:
146147
def __ge__(self, x: i32) -> bool: ...
147148
def __gt__(self, x: i32) -> bool: ...
148149
def __index__(self) -> int: ...
150+
def __eq__(self, x: object) -> bool: ...
149151

150152
class i16:
151153
@overload
@@ -181,6 +183,7 @@ class i16:
181183
def __ge__(self, x: i16) -> bool: ...
182184
def __gt__(self, x: i16) -> bool: ...
183185
def __index__(self) -> int: ...
186+
def __eq__(self, x: object) -> bool: ...
184187

185188
class u8:
186189
@overload
@@ -216,3 +219,4 @@ class u8:
216219
def __ge__(self, x: u8) -> bool: ...
217220
def __gt__(self, x: u8) -> bool: ...
218221
def __index__(self) -> int: ...
222+
def __eq__(self, x: object) -> bool: ...

test-data/unit/check-isinstance.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1995,17 +1995,17 @@ else:
19951995
[out]
19961996

19971997
[case testNarrowTypeAfterInListNonOverlapping]
1998+
# flags: --warn-unreachable
19981999
from typing import List, Optional
19992000

20002001
x: List[str]
20012002
y: Optional[int]
20022003

20032004
if y in x:
2004-
reveal_type(y) # N: Revealed type is "builtins.int | None"
2005+
reveal_type(y) # E: Statement is unreachable
20052006
else:
20062007
reveal_type(y) # N: Revealed type is "builtins.int | None"
20072008
[builtins fixtures/list.pyi]
2008-
[out]
20092009

20102010
[case testNarrowTypeAfterInListNested]
20112011
from typing import List, Optional, Any

test-data/unit/check-narrowing.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2982,7 +2982,7 @@ def narrow_tuple(x: Literal['c'], overlap: list[Literal['b', 'c']], no_overlap:
29822982
reveal_type(x) # N: Revealed type is "Literal['c']"
29832983

29842984
if x in no_overlap:
2985-
reveal_type(x) # N: Revealed type is "Literal['c']"
2985+
reveal_type(x) # E: Statement is unreachable
29862986
else:
29872987
reveal_type(x) # N: Revealed type is "Literal['c']"
29882988
[builtins fixtures/tuple.pyi]

test-data/unit/check-optional.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,11 +493,11 @@ from typing import Optional
493493

494494
def main(x: Optional[str]):
495495
if x == 0:
496-
reveal_type(x) # N: Revealed type is "builtins.str | None"
496+
reveal_type(x) # E: Statement is unreachable
497497
else:
498498
reveal_type(x) # N: Revealed type is "builtins.str | None"
499499
if x is 0:
500-
reveal_type(x) # N: Revealed type is "builtins.str | None"
500+
reveal_type(x) # E: Statement is unreachable
501501
else:
502502
reveal_type(x) # N: Revealed type is "builtins.str | None"
503503
[builtins fixtures/ops.pyi]

test-data/unit/check-python310.test

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ match m:
5656
-- Value Pattern --
5757

5858
[case testMatchValuePatternNarrows]
59+
# flags: --warn-unreachable
5960
import b
6061
m: object
6162

@@ -66,6 +67,7 @@ match m:
6667
b: int
6768

6869
[case testMatchValuePatternAlreadyNarrower]
70+
# flags: --warn-unreachable
6971
import b
7072
m: bool
7173

@@ -76,27 +78,28 @@ match m:
7678
b: int
7779

7880
[case testMatchValuePatternIntersect]
81+
# flags: --warn-unreachable
7982
import b
8083

8184
class A: ...
8285
m: A
8386

8487
match m:
8588
case b.b:
86-
reveal_type(m) # N: Revealed type is "__main__.A"
89+
reveal_type(m) # E: Statement is unreachable
8790
[file b.py]
8891
class B: ...
8992
b: B
9093

9194
[case testMatchValuePatternUnreachable]
92-
# primitives are needed because otherwise mypy doesn't see that int and str are incompatible
95+
# flags: --warn-unreachable
9396
import b
9497

9598
m: int
9699

97100
match m:
98101
case b.b:
99-
reveal_type(m) # N: Revealed type is "builtins.int"
102+
reveal_type(m) # E: Statement is unreachable
100103
[file b.py]
101104
b: str
102105
[builtins fixtures/primitives.pyi]
@@ -2766,7 +2769,7 @@ def x() -> tuple[Literal["test"]]: ...
27662769

27672770
match x():
27682771
case (x,) if x == "test": # E: Incompatible types in capture pattern (pattern captures type "Literal['test']", variable has type "Callable[[], tuple[Literal['test']]]")
2769-
reveal_type(x) # N: Revealed type is "def () -> tuple[Literal['test']]"
2772+
reveal_type(x) # E: Statement is unreachable
27702773
case foo:
27712774
foo
27722775

test-data/unit/check-tuples.test

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1533,17 +1533,24 @@ reveal_type(x) # N: Revealed type is "tuple[builtins.int, builtins.int]"
15331533
[builtins fixtures/tuple.pyi]
15341534

15351535
[case testTupleOverlapDifferentTuples]
1536+
# flags: --warn-unreachable
15361537
from typing import Optional, Tuple
15371538
class A: pass
15381539
class B: pass
15391540

1540-
possibles: Tuple[int, Tuple[A]]
1541-
x: Optional[Tuple[B]]
1541+
def f1(possibles: Tuple[int, Tuple[A]], x: Optional[Tuple[B]]):
1542+
if x in possibles:
1543+
reveal_type(x) # E: Statement is unreachable
1544+
else:
1545+
reveal_type(x) # N: Revealed type is "tuple[__main__.B] | None"
1546+
1547+
class AA(A): pass
15421548

1543-
if x in possibles:
1544-
reveal_type(x) # N: Revealed type is "tuple[__main__.B]"
1545-
else:
1546-
reveal_type(x) # N: Revealed type is "tuple[__main__.B] | None"
1549+
def f2(possibles: Tuple[int, Tuple[A]], x: Optional[Tuple[AA]]):
1550+
if x in possibles:
1551+
reveal_type(x) # N: Revealed type is "tuple[__main__.AA]"
1552+
else:
1553+
reveal_type(x) # N: Revealed type is "tuple[__main__.AA] | None"
15471554

15481555
[builtins fixtures/tuple.pyi]
15491556

0 commit comments

Comments
 (0)