Skip to content

Commit 350aa53

Browse files
committed
Sync reachability and comparison overlap checks
1 parent 6f4dbb8 commit 350aa53

File tree

4 files changed

+212
-22
lines changed

4 files changed

+212
-22
lines changed

mypy/checker.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
find_member,
147147
infer_class_variances,
148148
is_callable_compatible,
149+
is_enum_value_pair,
149150
is_equivalent,
150151
is_more_precise,
151152
is_proper_subtype,
@@ -6783,13 +6784,16 @@ def should_coerce_inner(typ: Type) -> bool:
67836784
expr_type = coerce_to_literal(expr_type)
67846785
if not is_valid_target(get_proper_type(expr_type)):
67856786
continue
6786-
if target and not is_same_type(target, expr_type):
6787+
if (
6788+
target is not None
6789+
and not is_same_type(target, expr_type)
6790+
and not is_enum_value_pair(target, expr_type)
6791+
):
67876792
# We have multiple disjoint target types. So the 'if' branch
67886793
# must be unreachable.
67896794
return None, {}
67906795
target = expr_type
67916796
possible_target_indices.append(i)
6792-
67936797
# There's nothing we can currently infer if none of the operands are valid targets,
67946798
# so we end early and infer nothing.
67956799
if target is None:
@@ -9125,7 +9129,9 @@ def _ambiguous_enum_variants(types: list[Type]) -> set[str]:
91259129
# let's be conservative
91269130
result.add("<other>")
91279131
elif isinstance(t, LiteralType):
9128-
result.update(_ambiguous_enum_variants([t.fallback]))
9132+
if t.fallback.type.is_enum:
9133+
result.update(_ambiguous_enum_variants([t.fallback]))
9134+
# Other literals (str, int, bool) cannot introduce any surprises
91299135
elif isinstance(t, NoneType):
91309136
pass
91319137
else:

mypy/meet.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
are_parameters_compatible,
1111
find_member,
1212
is_callable_compatible,
13+
is_enum_value_pair,
1314
is_equivalent,
1415
is_proper_subtype,
1516
is_same_type,
@@ -547,7 +548,11 @@ def _type_object_overlap(left: Type, right: Type) -> bool:
547548
right = right.fallback
548549

549550
if isinstance(left, LiteralType) and isinstance(right, LiteralType):
550-
if left.value == right.value or (left.fallback.type.is_enum ^ right.fallback.type.is_enum):
551+
if (
552+
left.value == right.value
553+
and left.fallback.type.is_enum == right.fallback.type.is_enum
554+
or is_enum_value_pair(left, right)
555+
):
551556
# If values are the same, we still need to check if fallbacks are overlapping,
552557
# this is done below.
553558
# Enums are more interesting:

mypy/subtypes.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from mypy.options import Options
3838
from mypy.state import state
3939
from mypy.types import (
40+
ELLIPSIS_TYPE_NAMES,
4041
MYPYC_NATIVE_INT_NAMES,
4142
TUPLE_LIKE_INSTANCE_NAMES,
4243
TYPED_NAMEDTUPLE_NAMES,
@@ -286,6 +287,34 @@ def is_same_type(
286287
)
287288

288289

290+
def is_enum_value_pair(a: ProperType, b: ProperType) -> bool:
291+
if not isinstance(a, LiteralType) or not isinstance(b, LiteralType):
292+
return False
293+
if b.fallback.type.is_enum:
294+
a, b = b, a
295+
if b.fallback.type.is_enum:
296+
return False
297+
# At this point we have a pair (non-enum literal, enum literal).
298+
# Check that the non-enum fallback is compatible
299+
if not is_subtype(a.fallback, b.fallback):
300+
return False
301+
assert isinstance(a.value, str)
302+
enum_value = a.fallback.type.get(a.value)
303+
return (
304+
enum_value is not None
305+
and enum_value.type is not None
306+
and isinstance(enum_value.type, Instance)
307+
and (
308+
enum_value.type.last_known_value == b
309+
# TODO: this is too lax and should only be applied for enums defined in stubs,
310+
# but checking that strictly requires access to the checker. This function
311+
# is needed in `is_overlapping_types` and operates on a lower level,
312+
# so doing this properly would be more difficult.
313+
or enum_value.type.type.fullname in ELLIPSIS_TYPE_NAMES
314+
)
315+
)
316+
317+
289318
# This is a common entry point for subtyping checks (both proper and non-proper).
290319
# Never call this private function directly, use the public versions.
291320
def _is_subtype(

test-data/unit/check-enum.test

Lines changed: 168 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2690,41 +2690,191 @@ class A(str, Enum):
26902690
a = "b"
26912691
b = "a"
26922692

2693-
A.a == "a"
2694-
A.a == "b"
2693+
# Every `if` block in this test should have an error on exactly one of two lines.
2694+
# Either it is reachable (and thus overlapping) or unreachable (and non-overlapping)
26952695

2696-
A.a == A.a
2697-
A.a == A.b # E: Non-overlapping equality check (left operand type: "Literal[A.a]", right operand type: "Literal[A.b]")
2696+
if A.a == "a": # E: Non-overlapping equality check (left operand type: "Literal[A.a]", right operand type: "Literal['a']")
2697+
1 + 'a'
2698+
if A.a == "b":
2699+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2700+
2701+
if A.a == 0: # E: Non-overlapping equality check (left operand type: "Literal[A.a]", right operand type: "Literal[0]")
2702+
1 + 'a'
2703+
2704+
if A.a == A.a:
2705+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2706+
if A.a == A.b: # E: Non-overlapping equality check (left operand type: "Literal[A.a]", right operand type: "Literal[A.b]")
2707+
1 + 'a'
26982708

26992709
class B(StrEnum):
27002710
a = "b"
27012711
b = "a"
27022712

2703-
B.a == "a"
2704-
B.a == "b"
2713+
if B.a == "a": # E: Non-overlapping equality check (left operand type: "Literal[B.a]", right operand type: "Literal['a']")
2714+
1 + 'a'
2715+
if B.a == "b":
2716+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
27052717

2706-
B.a == B.a
2707-
B.a == B.b # E: Non-overlapping equality check (left operand type: "Literal[B.a]", right operand type: "Literal[B.b]")
2718+
if B.a == 0: # E: Non-overlapping equality check (left operand type: "Literal[B.a]", right operand type: "Literal[0]")
2719+
1 + 'a'
27082720

2709-
B.a == A.a # E: Non-overlapping equality check (left operand type: "Literal[B.a]", right operand type: "Literal[A.a]")
2721+
if B.a == B.a:
2722+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2723+
if B.a == B.b: # E: Non-overlapping equality check (left operand type: "Literal[B.a]", right operand type: "Literal[B.b]")
2724+
1 + 'a'
2725+
2726+
if B.a == A.a: # E: Non-overlapping equality check (left operand type: "Literal[B.a]", right operand type: "Literal[A.a]")
2727+
1 + 'a'
27102728

27112729
class C(IntEnum):
27122730
a = 0
2731+
b = 1
2732+
2733+
if C.a == "a": # E: Non-overlapping equality check (left operand type: "Literal[C.a]", right operand type: "Literal['a']")
2734+
1 + 'a'
2735+
if C.a == "b": # E: Non-overlapping equality check (left operand type: "Literal[C.a]", right operand type: "Literal['b']")
2736+
1 + 'a'
27132737

2714-
C.a == "a" # E: Non-overlapping equality check (left operand type: "Literal[C.a]", right operand type: "Literal['a']")
2715-
C.a == "b" # E: Non-overlapping equality check (left operand type: "Literal[C.a]", right operand type: "Literal['b']")
2738+
if C.a == 0:
2739+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2740+
if C.a == 1: # E: Non-overlapping equality check (left operand type: "Literal[C.a]", right operand type: "Literal[1]")
2741+
1 + 'a'
27162742

2717-
C.a == C.a
2718-
C.a == C.b
2743+
if C.a == C.a:
2744+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2745+
if C.a == C.b: # E: Non-overlapping equality check (left operand type: "Literal[C.a]", right operand type: "Literal[C.b]")
2746+
1 + 'a'
27192747

27202748
class D(int, Enum):
27212749
a = 0
2750+
b = 1
2751+
2752+
if D.a == "a": # E: Non-overlapping equality check (left operand type: "Literal[D.a]", right operand type: "Literal['a']")
2753+
1 + 'a'
2754+
if D.a == "b": # E: Non-overlapping equality check (left operand type: "Literal[D.a]", right operand type: "Literal['b']")
2755+
1 + 'a'
2756+
2757+
if D.a == 0:
2758+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2759+
if D.a == 1: # E: Non-overlapping equality check (left operand type: "Literal[D.a]", right operand type: "Literal[1]")
2760+
1 + 'a'
2761+
2762+
if D.a == D.a:
2763+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2764+
if D.a == D.b: # E: Non-overlapping equality check (left operand type: "Literal[D.a]", right operand type: "Literal[D.b]")
2765+
1 + 'a'
27222766

2723-
D.a == "a" # E: Non-overlapping equality check (left operand type: "Literal[D.a]", right operand type: "Literal['a']")
2724-
D.a == "b" # E: Non-overlapping equality check (left operand type: "Literal[D.a]", right operand type: "Literal['b']")
2767+
if D.a == C.a: # E: Non-overlapping equality check (left operand type: "Literal[D.a]", right operand type: "Literal[C.a]")
2768+
1 + 'a'
2769+
[builtins fixtures/dict.pyi]
2770+
2771+
2772+
[case testEnumItemsEqualityToLiteralsInStub]
2773+
# flags: --python-version=3.11 --strict-equality
2774+
from mystub import A, B, C, D
2775+
2776+
# Every `if` block in this test should have an error on exactly one of two lines.
2777+
# Either it is reachable (and thus overlapping) or unreachable (and non-overlapping)
2778+
2779+
if A.a == "a":
2780+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2781+
if A.a == "b":
2782+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2783+
2784+
if A.a == 0: # E: Non-overlapping equality check (left operand type: "Literal[A.a]", right operand type: "Literal[0]")
2785+
1 + 'a'
2786+
2787+
if A.a == A.a:
2788+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2789+
if A.a == A.b: # E: Non-overlapping equality check (left operand type: "Literal[A.a]", right operand type: "Literal[A.b]")
2790+
1 + 'a'
2791+
2792+
if B.a == "a":
2793+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2794+
if B.a == "b":
2795+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2796+
2797+
if B.a == 0: # E: Non-overlapping equality check (left operand type: "Literal[B.a]", right operand type: "Literal[0]")
2798+
1 + 'a'
2799+
2800+
if B.a == B.a:
2801+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2802+
if B.a == B.b: # E: Non-overlapping equality check (left operand type: "Literal[B.a]", right operand type: "Literal[B.b]")
2803+
1 + 'a'
2804+
2805+
if B.a == A.a: # E: Non-overlapping equality check (left operand type: "Literal[B.a]", right operand type: "Literal[A.a]")
2806+
1 + 'a'
2807+
2808+
if C.a == "a": # E: Non-overlapping equality check (left operand type: "Literal[C.a]", right operand type: "Literal['a']")
2809+
1 + 'a'
2810+
if C.a == "b": # E: Non-overlapping equality check (left operand type: "Literal[C.a]", right operand type: "Literal['b']")
2811+
1 + 'a'
2812+
2813+
if C.a == 0:
2814+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2815+
if C.a == 1:
2816+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2817+
2818+
if C.a == C.a:
2819+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2820+
if C.a == C.b: # E: Non-overlapping equality check (left operand type: "Literal[C.a]", right operand type: "Literal[C.b]")
2821+
1 + 'a'
2822+
2823+
if D.a == "a": # E: Non-overlapping equality check (left operand type: "Literal[D.a]", right operand type: "Literal['a']")
2824+
1 + 'a'
2825+
if D.a == "b": # E: Non-overlapping equality check (left operand type: "Literal[D.a]", right operand type: "Literal['b']")
2826+
1 + 'a'
2827+
2828+
if D.a == 0:
2829+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2830+
if D.a == 1:
2831+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2832+
2833+
if D.a == D.a:
2834+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2835+
if D.a == D.b: # E: Non-overlapping equality check (left operand type: "Literal[D.a]", right operand type: "Literal[D.b]")
2836+
1 + 'a'
2837+
2838+
if D.a == C.a: # E: Non-overlapping equality check (left operand type: "Literal[D.a]", right operand type: "Literal[C.a]")
2839+
1 + 'a'
2840+
2841+
[file mystub.pyi]
2842+
from enum import Enum, StrEnum, IntEnum
2843+
2844+
class A(str, Enum):
2845+
a = ...
2846+
b = ...
27252847

2726-
D.a == D.a
2727-
D.a == D.b
2848+
class B(StrEnum):
2849+
a = ...
2850+
b = ...
2851+
2852+
class C(int, Enum):
2853+
a = ...
2854+
b = ...
2855+
2856+
class D(IntEnum):
2857+
a = ...
2858+
b = ...
2859+
[builtins fixtures/dict.pyi]
2860+
2861+
2862+
[case testEnumItemsEqualityToLiteralsWithAlias-xfail]
2863+
# flags: --python-version=3.11 --strict-equality
2864+
# TODO: mypy does not support enum member aliases now.
2865+
from enum import Enum, IntEnum
2866+
2867+
class A(str, Enum):
2868+
a = "c"
2869+
b = a
2870+
2871+
if A.a == A.b:
2872+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
2873+
2874+
class B(IntEnum):
2875+
a = 0
2876+
b = a
27282877

2729-
D.a == C.a # E: Non-overlapping equality check (left operand type: "Literal[D.a]", right operand type: "Literal[C.a]")
2878+
if B.a == B.b:
2879+
1 + 'a' # E: Unsupported operand types for + ("int" and "str")
27302880
[builtins fixtures/dict.pyi]

0 commit comments

Comments
 (0)