From 83744d3f87d885beb9f1a1ff11e7e174e4147a17 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Mon, 10 Aug 2026 21:11:30 -0400 Subject: [PATCH 1/3] Add a secret --check-unreachable --- mypy/checker.py | 38 ++++++++++++++++++++++++++++---------- mypy/main.py | 7 +++++++ mypy/options.py | 4 ++++ 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 7d356d30bb123..92de116465738 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -604,18 +604,27 @@ def check_first_pass(self, recurse_into_functions: bool = True) -> None: with self.tscope.module_scope(self.tree.fullname): with self.enter_partial_types(), self.binder.top_frame_context(): marked_unreachable = False + reported_unreachable = False for d in self.tree.defs: if self.binder.is_unreachable(): + finish = False if not marked_unreachable: self.mark_unreachable(self.tree.defs, after=d) marked_unreachable = True if not self.should_report_unreachable_issues(): - break - if not self.is_noop_for_reachability(d): + finish = True + if not self.is_noop_for_reachability(d) and not reported_unreachable: self.msg.unreachable_statement(d) + finish = True + reported_unreachable = True + + if finish and not self.options.check_unreachable: break - else: - self.accept(d) + + if not self.options.check_unreachable: + continue + + self.accept(d) assert not self.current_node_deferred @@ -3303,20 +3312,29 @@ def visit_block(self, b: Block) -> None: self.binder.unreachable() return marked_unreachable = False + reported_unreachable = False for s in b.body: if self.binder.is_unreachable(): + finish = False if self.scope.top_level_function() is None and not marked_unreachable: self.mark_unreachable(b.body, after=s) marked_unreachable = True if not self.should_report_unreachable_issues(): - break - if not self.is_noop_for_reachability(s): + finish = True + if not self.is_noop_for_reachability(s) and not reported_unreachable: self.msg.unreachable_statement(s) + finish = True + reported_unreachable = True + + if finish and not self.options.check_unreachable: break - else: - self.accept(s) - # Clear expression cache after each statement to avoid unlimited growth. - self.expr_checker.expr_cache.clear() + + if not self.options.check_unreachable: + continue + + self.accept(s) + # Clear expression cache after each statement to avoid unlimited growth. + self.expr_checker.expr_cache.clear() def should_report_unreachable_issues(self) -> bool: return ( diff --git a/mypy/main.py b/mypy/main.py index 0cf624a3a5d7b..9d6d3d5c5f7ae 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -965,6 +965,13 @@ def add_invertible_flag( group=strictness_group, ) + add_invertible_flag( + "--check-unreachable", + default=False, + help=argparse.SUPPRESS, # "Type check unreachable code", + group=strictness_group, + ) + strict_help = "Strict mode; enables the following flags: {}".format( ", ".join(strict_flag_names) ) diff --git a/mypy/options.py b/mypy/options.py index 92c9ea3b1701d..e38ce8ba9e5d2 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -29,6 +29,7 @@ class BuildType: "allow_untyped_globals", "always_false", "always_true", + "check_unreachable", "check_untyped_defs", "debug_cache", "disable_error_code", @@ -173,6 +174,9 @@ def __init__(self) -> None: # Disallow defining incompletely typed functions self.disallow_incomplete_defs = False + # Type check unreachable code + self.check_unreachable = False + # Type check unannotated functions self.check_untyped_defs = False From 2bce860d25a3c6c9e79a80e5791393516c00aca0 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Mon, 10 Aug 2026 21:26:13 -0400 Subject: [PATCH 2/3] Oh, add a test too! --- mypy/checker.py | 12 ++++++++++-- test-data/unit/check-unreachable-code.test | 7 +++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 92de116465738..1eaa4c9fa5b98 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -613,7 +613,11 @@ def check_first_pass(self, recurse_into_functions: bool = True) -> None: marked_unreachable = True if not self.should_report_unreachable_issues(): finish = True - if not self.is_noop_for_reachability(d) and not reported_unreachable: + if ( + not self.is_noop_for_reachability(d) + and not reported_unreachable + and not finish + ): self.msg.unreachable_statement(d) finish = True reported_unreachable = True @@ -3321,7 +3325,11 @@ def visit_block(self, b: Block) -> None: marked_unreachable = True if not self.should_report_unreachable_issues(): finish = True - if not self.is_noop_for_reachability(s) and not reported_unreachable: + if ( + not self.is_noop_for_reachability(s) + and not reported_unreachable + and not finish + ): self.msg.unreachable_statement(s) finish = True reported_unreachable = True diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index 93bb6e4e52f29..8238fd77e2298 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -1744,3 +1744,10 @@ assert sys.platform == "win32" 42 + "no way" # type: ignore[operator] [builtins fixtures/isinstancelist.pyi] + + +[case testCheckUnreachableBasics] +# flags: --check-unreachable --warn-unreachable +if False: + reveal_type(5) # E: Statement is unreachable \ + # N: Revealed type is "Literal[5]?" From 18b9df8343e47bb3a4bf16dabcf845bfe938625f Mon Sep 17 00:00:00 2001 From: A5rocks Date: Mon, 10 Aug 2026 22:06:11 -0400 Subject: [PATCH 3/3] is_noop_for_reachability actually does work :/ --- mypy/checker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 1eaa4c9fa5b98..b1c15d20c329b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -614,9 +614,9 @@ def check_first_pass(self, recurse_into_functions: bool = True) -> None: if not self.should_report_unreachable_issues(): finish = True if ( - not self.is_noop_for_reachability(d) + not finish and not reported_unreachable - and not finish + and not self.is_noop_for_reachability(d) ): self.msg.unreachable_statement(d) finish = True @@ -3326,9 +3326,9 @@ def visit_block(self, b: Block) -> None: if not self.should_report_unreachable_issues(): finish = True if ( - not self.is_noop_for_reachability(s) + not finish and not reported_unreachable - and not finish + and not self.is_noop_for_reachability(s) ): self.msg.unreachable_statement(s) finish = True