Skip to content

Commit 4136715

Browse files
Don-Burnssterliakovhauntsaninja
authored
Add flag to raise error if match statement does not match exaustively (#19144)
Fixes #19136 Change is to add a mode to catch when a match statement is not handling all cases exhaustively, similar to what pyright does by default. After discussion on #19136 I put it behind a new flag that is not enabled by default. I updated docs to include information on the new flag also. Please let me know if anything is not following standards, in particular I wasn't sure what to name this new flag to be descriptive while following existing flag naming style. --------- Co-authored-by: Stanislav Terliakov <[email protected]> Co-authored-by: hauntsaninja <[email protected]>
1 parent 5a0fa55 commit 4136715

File tree

7 files changed

+226
-0
lines changed

7 files changed

+226
-0
lines changed

docs/source/command_line.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,7 @@ of the above sections.
845845
x = 'a string'
846846
x.trim() # error: "str" has no attribute "trim" [attr-defined]
847847
848+
848849
.. _configuring-error-messages:
849850

850851
Configuring error messages

docs/source/error_code_list2.rst

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,3 +612,44 @@ Example:
612612
# mypy: disallow-any-explicit
613613
from typing import Any
614614
x: Any = 1 # Error: Explicit "Any" type annotation [explicit-any]
615+
616+
617+
.. _code-exhaustive-match:
618+
619+
Check that match statements match exhaustively [match-exhaustive]
620+
-----------------------------------------------------------------------
621+
622+
If enabled with :option:`--enable-error-code exhaustive-match <mypy --enable-error-code>`,
623+
mypy generates an error if a match statement does not match all possible cases/types.
624+
625+
626+
Example:
627+
628+
.. code-block:: python
629+
630+
import enum
631+
632+
633+
class Color(enum.Enum):
634+
RED = 1
635+
BLUE = 2
636+
637+
val: Color = Color.RED
638+
639+
# OK without --enable-error-code exhaustive-match
640+
match val:
641+
case Color.RED:
642+
print("red")
643+
644+
# With --enable-error-code exhaustive-match
645+
# Error: Match statement has unhandled case for values of type "Literal[Color.BLUE]"
646+
match val:
647+
case Color.RED:
648+
print("red")
649+
650+
# OK with or without --enable-error-code exhaustive-match, since all cases are handled
651+
match val:
652+
case Color.RED:
653+
print("red")
654+
case _:
655+
print("other")

docs/source/literal_types.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,10 @@ If we forget to handle one of the cases, mypy will generate an error:
468468
assert_never(direction) # E: Argument 1 to "assert_never" has incompatible type "Direction"; expected "NoReturn"
469469
470470
Exhaustiveness checking is also supported for match statements (Python 3.10 and later).
471+
For match statements specifically, inexhaustive matches can be caught
472+
without needing to use ``assert_never`` by using
473+
:option:`--enable-error-code exhaustive-match <mypy --enable-error-code>`.
474+
471475

472476
Extra Enum checks
473477
*****************

mypy/checker.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5455,6 +5455,7 @@ def visit_match_stmt(self, s: MatchStmt) -> None:
54555455
inferred_types = self.infer_variable_types_from_type_maps(type_maps)
54565456

54575457
# The second pass narrows down the types and type checks bodies.
5458+
unmatched_types: TypeMap = None
54585459
for p, g, b in zip(s.patterns, s.guards, s.bodies):
54595460
current_subject_type = self.expr_checker.narrow_type_from_binder(
54605461
named_subject, subject_type
@@ -5511,6 +5512,11 @@ def visit_match_stmt(self, s: MatchStmt) -> None:
55115512
else:
55125513
self.accept(b)
55135514
self.push_type_map(else_map, from_assignment=False)
5515+
unmatched_types = else_map
5516+
5517+
if unmatched_types is not None:
5518+
for typ in list(unmatched_types.values()):
5519+
self.msg.match_statement_inexhaustive_match(typ, s)
55145520

55155521
# This is needed due to a quirk in frame_context. Without it types will stay narrowed
55165522
# after the match.

mypy/errorcodes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,12 @@ def __hash__(self) -> int:
264264
"General",
265265
default_enabled=False,
266266
)
267+
EXHAUSTIVE_MATCH: Final = ErrorCode(
268+
"exhaustive-match",
269+
"Reject match statements that are not exhaustive",
270+
"General",
271+
default_enabled=False,
272+
)
267273

268274
# Syntax errors are often blocking.
269275
SYNTAX: Final[ErrorCode] = ErrorCode("syntax", "Report syntax errors", "General")

mypy/messages.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2491,6 +2491,16 @@ def type_parameters_should_be_declared(self, undeclared: list[str], context: Con
24912491
code=codes.VALID_TYPE,
24922492
)
24932493

2494+
def match_statement_inexhaustive_match(self, typ: Type, context: Context) -> None:
2495+
type_str = format_type(typ, self.options)
2496+
msg = f"Match statement has unhandled case for values of type {type_str}"
2497+
self.fail(msg, context, code=codes.EXHAUSTIVE_MATCH)
2498+
self.note(
2499+
"If match statement is intended to be non-exhaustive, add `case _: pass`",
2500+
context,
2501+
code=codes.EXHAUSTIVE_MATCH,
2502+
)
2503+
24942504

24952505
def quote_type_string(type_string: str) -> str:
24962506
"""Quotes a type representation for use in messages."""

test-data/unit/check-python310.test

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2639,6 +2639,164 @@ def f2() -> None:
26392639
reveal_type(y) # N: Revealed type is "builtins.str"
26402640
[builtins fixtures/list.pyi]
26412641

2642+
[case testExhaustiveMatchNoFlag]
2643+
2644+
a: int = 5
2645+
match a:
2646+
case 1:
2647+
pass
2648+
case _:
2649+
pass
2650+
2651+
b: str = "hello"
2652+
match b:
2653+
case "bye":
2654+
pass
2655+
case _:
2656+
pass
2657+
2658+
[case testNonExhaustiveMatchNoFlag]
2659+
2660+
a: int = 5
2661+
match a:
2662+
case 1:
2663+
pass
2664+
2665+
b: str = "hello"
2666+
match b:
2667+
case "bye":
2668+
pass
2669+
2670+
2671+
[case testExhaustiveMatchWithFlag]
2672+
# flags: --enable-error-code exhaustive-match
2673+
2674+
a: int = 5
2675+
match a:
2676+
case 1:
2677+
pass
2678+
case _:
2679+
pass
2680+
2681+
b: str = "hello"
2682+
match b:
2683+
case "bye":
2684+
pass
2685+
case _:
2686+
pass
2687+
2688+
[case testNonExhaustiveMatchWithFlag]
2689+
# flags: --enable-error-code exhaustive-match
2690+
2691+
a: int = 5
2692+
match a: # E: Match statement has unhandled case for values of type "int" \
2693+
# N: If match statement is intended to be non-exhaustive, add `case _: pass`
2694+
case 1:
2695+
pass
2696+
2697+
b: str = "hello"
2698+
match b: # E: Match statement has unhandled case for values of type "str" \
2699+
# N: If match statement is intended to be non-exhaustive, add `case _: pass`
2700+
case "bye":
2701+
pass
2702+
[case testNonExhaustiveMatchEnumWithFlag]
2703+
# flags: --enable-error-code exhaustive-match
2704+
2705+
import enum
2706+
2707+
class Color(enum.Enum):
2708+
RED = 1
2709+
BLUE = 2
2710+
GREEN = 3
2711+
2712+
val: Color = Color.RED
2713+
2714+
match val: # E: Match statement has unhandled case for values of type "Literal[Color.GREEN]" \
2715+
# N: If match statement is intended to be non-exhaustive, add `case _: pass`
2716+
case Color.RED:
2717+
a = "red"
2718+
case Color.BLUE:
2719+
a= "blue"
2720+
[builtins fixtures/enum.pyi]
2721+
2722+
[case testExhaustiveMatchEnumWithFlag]
2723+
# flags: --enable-error-code exhaustive-match
2724+
2725+
import enum
2726+
2727+
class Color(enum.Enum):
2728+
RED = 1
2729+
BLUE = 2
2730+
2731+
val: Color = Color.RED
2732+
2733+
match val:
2734+
case Color.RED:
2735+
a = "red"
2736+
case Color.BLUE:
2737+
a= "blue"
2738+
[builtins fixtures/enum.pyi]
2739+
2740+
[case testNonExhaustiveMatchEnumMultipleMissingMatchesWithFlag]
2741+
# flags: --enable-error-code exhaustive-match
2742+
2743+
import enum
2744+
2745+
class Color(enum.Enum):
2746+
RED = 1
2747+
BLUE = 2
2748+
GREEN = 3
2749+
2750+
val: Color = Color.RED
2751+
2752+
match val: # E: Match statement has unhandled case for values of type "Literal[Color.BLUE, Color.GREEN]" \
2753+
# N: If match statement is intended to be non-exhaustive, add `case _: pass`
2754+
case Color.RED:
2755+
a = "red"
2756+
[builtins fixtures/enum.pyi]
2757+
2758+
[case testExhaustiveMatchEnumFallbackWithFlag]
2759+
# flags: --enable-error-code exhaustive-match
2760+
2761+
import enum
2762+
2763+
class Color(enum.Enum):
2764+
RED = 1
2765+
BLUE = 2
2766+
GREEN = 3
2767+
2768+
val: Color = Color.RED
2769+
2770+
match val:
2771+
case Color.RED:
2772+
a = "red"
2773+
case _:
2774+
a = "other"
2775+
[builtins fixtures/enum.pyi]
2776+
2777+
# Fork of testMatchNarrowingUnionTypedDictViaIndex to check behaviour with exhaustive match flag
2778+
[case testExhaustiveMatchNarrowingUnionTypedDictViaIndex]
2779+
# flags: --enable-error-code exhaustive-match
2780+
2781+
from typing import Literal, TypedDict
2782+
2783+
class A(TypedDict):
2784+
tag: Literal["a"]
2785+
name: str
2786+
2787+
class B(TypedDict):
2788+
tag: Literal["b"]
2789+
num: int
2790+
2791+
d: A | B
2792+
match d["tag"]: # E: Match statement has unhandled case for values of type "Literal['b']" \
2793+
# N: If match statement is intended to be non-exhaustive, add `case _: pass` \
2794+
# E: Match statement has unhandled case for values of type "B"
2795+
case "a":
2796+
reveal_type(d) # N: Revealed type is "TypedDict('__main__.A', {'tag': Literal['a'], 'name': builtins.str})"
2797+
reveal_type(d["name"]) # N: Revealed type is "builtins.str"
2798+
[typing fixtures/typing-typeddict.pyi]
2799+
26422800
[case testEnumTypeObjectMember]
26432801
import enum
26442802
from typing import NoReturn

0 commit comments

Comments
 (0)