From 01c6994ac01e3822fe89c5dc46f8bc8b656c8263 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Wed, 26 Jul 2023 03:34:51 -0400 Subject: [PATCH] Don't flag intentionally empty generators unreachable (#15722) Co-authored-by: Alex Waygood --- mypy/binder.py | 8 ------- mypy/checker.py | 25 ++++++++++++++++++++-- test-data/unit/check-unreachable-code.test | 16 ++++++++++++++ 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/mypy/binder.py b/mypy/binder.py index 37c0b6bb90060..8a68f24f661e5 100644 --- a/mypy/binder.py +++ b/mypy/binder.py @@ -42,13 +42,6 @@ def __init__(self, id: int, conditional_frame: bool = False) -> None: self.types: dict[Key, Type] = {} self.unreachable = False self.conditional_frame = conditional_frame - - # Should be set only if we're entering a frame where it's not - # possible to accurately determine whether or not contained - # statements will be unreachable or not. - # - # Long-term, we should improve mypy to the point where we no longer - # need this field. self.suppress_unreachable_warnings = False def __repr__(self) -> str: @@ -174,7 +167,6 @@ def is_unreachable(self) -> bool: return any(f.unreachable for f in self.frames) def is_unreachable_warning_suppressed(self) -> bool: - # TODO: See todo in 'is_unreachable' return any(f.suppress_unreachable_warnings for f in self.frames) def cleanse(self, expr: Expression) -> None: diff --git a/mypy/checker.py b/mypy/checker.py index 724a1dd1f7d73..e0cd02e745732 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -132,6 +132,7 @@ Var, WhileStmt, WithStmt, + YieldExpr, is_final_node, ) from mypy.options import Options @@ -1241,13 +1242,17 @@ def check_func_def( new_frame.types[key] = narrowed_type self.binder.declarations[key] = old_binder.declarations[key] with self.scope.push_function(defn): - # We suppress reachability warnings when we use TypeVars with value + # We suppress reachability warnings for empty generator functions + # (return; yield) which have a "yield" that's unreachable by definition + # since it's only there to promote the function into a generator function. + # + # We also suppress reachability warnings when we use TypeVars with value # restrictions: we only want to report a warning if a certain statement is # marked as being suppressed in *all* of the expansions, but we currently # have no good way of doing this. # # TODO: Find a way of working around this limitation - if len(expanded) >= 2: + if _is_empty_generator_function(item) or len(expanded) >= 2: self.binder.suppress_unreachable_warnings() self.accept(item.body) unreachable = self.binder.is_unreachable() @@ -6968,6 +6973,22 @@ def is_literal_not_implemented(n: Expression) -> bool: return isinstance(n, NameExpr) and n.fullname == "builtins.NotImplemented" +def _is_empty_generator_function(func: FuncItem) -> bool: + """ + Checks whether a function's body is 'return; yield' (the yield being added only + to promote the function into a generator function). + """ + body = func.body.body + return ( + len(body) == 2 + and isinstance(ret_stmt := body[0], ReturnStmt) + and (ret_stmt.expr is None or is_literal_none(ret_stmt.expr)) + and isinstance(expr_stmt := body[1], ExpressionStmt) + and isinstance(yield_expr := expr_stmt.expr, YieldExpr) + and (yield_expr.expr is None or is_literal_none(yield_expr.expr)) + ) + + def builtin_item_type(tp: Type) -> Type | None: """Get the item type of a builtin container. diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index 76ecd9f51e351..7a6c2cbfd1c7a 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -1446,3 +1446,19 @@ def f() -> None: Foo()['a'] = 'a' x = 0 # This should not be reported as unreachable [builtins fixtures/exception.pyi] + +[case testIntentionallyEmptyGeneratorFunction] +# flags: --warn-unreachable +from typing import Generator + +def f() -> Generator[None, None, None]: + return + yield + +[case testIntentionallyEmptyGeneratorFunction_None] +# flags: --warn-unreachable +from typing import Generator + +def f() -> Generator[None, None, None]: + return None + yield None