diff --git a/mypy/checker.py b/mypy/checker.py index 96e41a5e1786..8224680811e4 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1307,6 +1307,7 @@ def check_func_def( self.check_typevar_defaults(typ.variables) expanded = self.expand_typevars(defn, typ) original_typ = typ + iter_errors = IterationDependentErrors() for item, typ in expanded: old_binder = self.binder self.binder = ConditionalTypeBinder(self.options) @@ -1486,14 +1487,7 @@ def check_func_def( # 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 _is_empty_generator_function(item) or len(expanded) >= 2: + if _is_empty_generator_function(item): self.binder.suppress_unreachable_warnings() # When checking a third-party library, we can skip function body, # if during semantic analysis we found that there are no attributes @@ -1507,7 +1501,13 @@ def check_func_def( or not isinstance(defn, FuncDef) or defn.has_self_attr_def ): - self.accept(item.body) + if len(expanded) > 1: + with IterationErrorWatcher( + self.msg.errors, iter_errors, collect_revealed_types=False + ): + self.accept(item.body) + else: + self.accept(item.body) unreachable = self.binder.is_unreachable() if new_frame is not None: self.binder.pop_frame(True, 0) @@ -1603,6 +1603,9 @@ def check_func_def( self.binder = old_binder + if len(expanded) > 1: + self.msg.iteration_dependent_errors(iter_errors) + def require_correct_self_argument(self, func: Type, defn: FuncDef) -> bool: func = get_proper_type(func) if not isinstance(func, CallableType): diff --git a/mypy/errors.py b/mypy/errors.py index edfb3bd1607a..d2d6d042587d 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -353,6 +353,7 @@ class IterationErrorWatcher(ErrorWatcher): making too-hasty reports.""" iteration_dependent_errors: IterationDependentErrors + collect_revealed_types: bool def __init__( self, @@ -362,6 +363,7 @@ def __init__( filter_errors: bool | Callable[[str, ErrorInfo], bool] = False, save_filtered_errors: bool = False, filter_deprecated: bool = False, + collect_revealed_types: bool = True, ) -> None: super().__init__( errors, @@ -373,6 +375,7 @@ def __init__( iteration_dependent_errors.uselessness_errors.append(set()) iteration_dependent_errors.nonoverlapping_types.append({}) iteration_dependent_errors.unreachable_lines.append(set()) + self.collect_revealed_types = collect_revealed_types def on_error(self, file: str, info: ErrorInfo) -> bool: """Filter out the "iteration-dependent" errors and notes and store their diff --git a/mypy/messages.py b/mypy/messages.py index bbcc93ebfb25..08d3beb5f589 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1778,7 +1778,7 @@ def reveal_type(self, typ: Type, context: Context) -> None: # The `reveal_type` statement might be visited iteratively due to being # placed in a loop or so. Hence, we collect the respective types of # individual iterations so that we can report them all in one step later: - if isinstance(watcher, IterationErrorWatcher): + if isinstance(watcher, IterationErrorWatcher) and watcher.collect_revealed_types: watcher.iteration_dependent_errors.revealed_types[ (context.line, context.column, context.end_line, context.end_column) ].append(typ) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 03586e4109f6..551ca6cc65bf 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2434,6 +2434,20 @@ for x in xs: y = {} # E: Need type annotation for "y" (hint: "y: dict[, ] = ...") [builtins fixtures/list.pyi] +[case testAvoidFalseUnreachableInLoopWithContrainedTypeVar] +# flags: --warn-unreachable --python-version 3.11 +from typing import TypeVar + +T = TypeVar("T", int, str) +def f(x: T) -> list[T]: + y = None + while y is None: + if y is None: + y = [] + y.append(x) + return y +[builtins fixtures/list.pyi] + [case testAvoidFalseRedundantExprInLoop] # flags: --enable-error-code redundant-expr --python-version 3.11 diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index 98c676dbf42b..f8cf1a47b1aa 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -1056,11 +1056,7 @@ def test2(x: T2) -> T2: reveal_type(x) # N: Revealed type is "builtins.str" if False: - # This is unreachable, but we don't report an error, unfortunately. - # The presence of the TypeVar with values unfortunately currently shuts - # down type-checking for this entire function. - # TODO: Find a way of removing this limitation - reveal_type(x) + reveal_type(x) # E: Statement is unreachable return x @@ -1074,20 +1070,14 @@ class Test3(Generic[T2]): reveal_type(self.x) # N: Revealed type is "builtins.str" if False: - # Same issue as above - reveal_type(self.x) + reveal_type(self.x) # E: Statement is unreachable class Test4(Generic[T3]): def __init__(self, x: T3): - # https://github.com/python/mypy/issues/9456 - # On TypeVars with value restrictions, we currently have no way - # of checking a statement for all the type expansions. - # Thus unreachable warnings are disabled if x and False: pass - # This test should fail after this limitation is removed. - if False and x: + if False and x: # E: Right operand of "and" is never evaluated pass [builtins fixtures/isinstancelist.pyi]