Skip to content

Commit 71a3b28

Browse files
committed
Improve the handling of "iteration dependend" errors and notes in finally clauses.
1 parent 325f776 commit 71a3b28

File tree

4 files changed

+123
-59
lines changed

4 files changed

+123
-59
lines changed

mypy/checker.py

Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@
2525
from mypy.constraints import SUPERTYPE_OF
2626
from mypy.erasetype import erase_type, erase_typevars, remove_instance_last_known_values
2727
from mypy.errorcodes import TYPE_VAR, UNUSED_AWAITABLE, UNUSED_COROUTINE, ErrorCode
28-
from mypy.errors import Errors, ErrorWatcher, LoopErrorWatcher, report_internal_error
28+
from mypy.errors import (
29+
Errors,
30+
ErrorWatcher,
31+
IterationErrorWatcher,
32+
ÎterationDependentErrors,
33+
report_internal_error,
34+
)
2935
from mypy.expandtype import expand_type
3036
from mypy.literals import Key, extract_var_from_literal_hash, literal, literal_hash
3137
from mypy.maptype import map_instance_to_supertype
@@ -599,26 +605,15 @@ def accept_loop(
599605
# on without bound otherwise)
600606
widened_old = len(self.widened_vars)
601607

602-
# one set of `unreachable`, `redundant-expr`, and `redundant-casts` errors
603-
# per iteration step:
604-
uselessness_errors = []
605-
# one set of unreachable line numbers per iteration step:
606-
unreachable_lines = []
607-
# one set of revealed types per line where `reveal_type` is used (each
608-
# created set can grow during the iteration):
609-
revealed_types = defaultdict(set)
608+
iter_errors = ÎterationDependentErrors()
610609
iter = 1
611610
while True:
612611
with self.binder.frame_context(can_skip=True, break_frame=2, continue_frame=1):
613612
if on_enter_body is not None:
614613
on_enter_body()
615614

616-
with LoopErrorWatcher(self.msg.errors) as watcher:
615+
with IterationErrorWatcher(self.msg.errors, iter_errors) as watcher:
617616
self.accept(body)
618-
uselessness_errors.append(watcher.uselessness_errors)
619-
unreachable_lines.append(watcher.unreachable_lines)
620-
for key, values in watcher.revealed_types.items():
621-
revealed_types[key].update(values)
622617

623618
partials_new = sum(len(pts.map) for pts in self.partial_types)
624619
widened_new = len(self.widened_vars)
@@ -640,29 +635,10 @@ def accept_loop(
640635
if iter == 20:
641636
raise RuntimeError("Too many iterations when checking a loop")
642637

643-
# Report only those `unreachable`, `redundant-expr`, and `redundant-casts`
644-
# errors that could not be ruled out in any iteration step:
645-
persistent_uselessness_errors = set()
646-
for candidate in set(itertools.chain(*uselessness_errors)):
647-
if all(
648-
(candidate in errors) or (candidate[2] in lines)
649-
for errors, lines in zip(uselessness_errors, unreachable_lines)
650-
):
651-
persistent_uselessness_errors.add(candidate)
652-
for error_info in persistent_uselessness_errors:
653-
context = Context(line=error_info[2], column=error_info[3])
654-
context.end_line = error_info[4]
655-
context.end_column = error_info[5]
656-
self.msg.fail(error_info[1], context, code=error_info[0])
657-
658-
# Report all types revealed in at least one iteration step:
659-
for note_info, types in revealed_types.items():
660-
sorted_ = sorted(types, key=lambda typ: typ.lower())
661-
revealed = sorted_[0] if len(types) == 1 else f"Union[{', '.join(sorted_)}]"
662-
context = Context(line=note_info[1], column=note_info[2])
663-
context.end_line = note_info[3]
664-
context.end_column = note_info[4]
665-
self.note(f'Revealed type is "{revealed}"', context)
638+
for error_info in watcher.yield_error_infos():
639+
self.msg.fail(*error_info[:2], code=error_info[2])
640+
for note_info in watcher.yield_note_infos():
641+
self.note(*note_info)
666642

667643
# If exit_condition is set, assume it must be False on exit from the loop:
668644
if exit_condition:
@@ -4974,6 +4950,9 @@ def type_check_raise(self, e: Expression, s: RaiseStmt, optional: bool = False)
49744950

49754951
def visit_try_stmt(self, s: TryStmt) -> None:
49764952
"""Type check a try statement."""
4953+
4954+
iter_errors = None
4955+
49774956
# Our enclosing frame will get the result if the try/except falls through.
49784957
# This one gets all possible states after the try block exited abnormally
49794958
# (by exception, return, break, etc.)
@@ -4988,7 +4967,9 @@ def visit_try_stmt(self, s: TryStmt) -> None:
49884967
self.visit_try_without_finally(s, try_frame=bool(s.finally_body))
49894968
if s.finally_body:
49904969
# First we check finally_body is type safe on all abnormal exit paths
4991-
self.accept(s.finally_body)
4970+
iter_errors = ÎterationDependentErrors()
4971+
with IterationErrorWatcher(self.msg.errors, iter_errors) as watcher:
4972+
self.accept(s.finally_body)
49924973

49934974
if s.finally_body:
49944975
# Then we try again for the more restricted set of options
@@ -5002,8 +4983,15 @@ def visit_try_stmt(self, s: TryStmt) -> None:
50024983
# type checks in both contexts, but only the resulting types
50034984
# from the latter context affect the type state in the code
50044985
# that follows the try statement.)
4986+
assert iter_errors is not None
50054987
if not self.binder.is_unreachable():
5006-
self.accept(s.finally_body)
4988+
with IterationErrorWatcher(self.msg.errors, iter_errors) as watcher:
4989+
self.accept(s.finally_body)
4990+
4991+
for error_info in watcher.yield_error_infos():
4992+
self.msg.fail(*error_info[:2], code=error_info[2])
4993+
for note_info in watcher.yield_note_infos():
4994+
self.msg.note(*note_info)
50074995

50084996
def visit_try_without_finally(self, s: TryStmt, try_frame: bool) -> None:
50094997
"""Type check a try statement, ignoring the finally block.

mypy/errors.py

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
import traceback
66
from collections import defaultdict
77
from collections.abc import Iterable
8-
from typing import Callable, Final, NoReturn, Optional, TextIO, TypeVar
9-
from typing_extensions import Literal, Self, TypeAlias as _TypeAlias
8+
from itertools import chain
9+
from typing import Callable, Final, Iterator, NoReturn, Optional, TextIO, TypeVar
10+
from typing_extensions import Literal, NamedTuple, Self, TypeAlias as _TypeAlias
1011

1112
from mypy import errorcodes as codes
1213
from mypy.error_formatter import ErrorFormatter
1314
from mypy.errorcodes import IMPORT, IMPORT_NOT_FOUND, IMPORT_UNTYPED, ErrorCode, mypy_error_codes
15+
from mypy.nodes import Context
1416
from mypy.options import Options
1517
from mypy.scope import Scope
1618
from mypy.util import DEFAULT_SOURCE_OFFSET, is_typeshed_file
@@ -220,23 +222,44 @@ def filtered_errors(self) -> list[ErrorInfo]:
220222
return self._filtered
221223

222224

223-
class LoopErrorWatcher(ErrorWatcher):
224-
"""Error watcher that filters and separately collects `unreachable` errors,
225-
`redundant-expr` and `redundant-casts` errors, and revealed types when analysing
226-
loops iteratively to help avoid making too-hasty reports."""
227225

228-
# Meaning of the tuple items: ErrorCode, message, line, column, end_line, end_column:
229-
uselessness_errors: set[tuple[ErrorCode, str, int, int, int, int]]
226+
class ÎterationDependentErrors:
227+
"""An `ÎterationDependentErrors` instance serves to collect the `unreachable`,
228+
`redundant-expr`, and `redundant-casts` errors, as well as the revealed types,
229+
handled by the individual `IterationErrorWatcher` instances sequentially applied to
230+
the same code section."""
231+
232+
# One set of `unreachable`, `redundant-expr`, and `redundant-casts` errors per
233+
# iteration step. Meaning of the tuple items: ErrorCode, message, line, column,
234+
# end_line, end_column.
235+
uselessness_errors: list[set[tuple[ErrorCode, str, int, int, int, int]]]
236+
237+
# One set of unreachable line numbers per iteration step. Not only the lines where
238+
# the error report occurs but really all unreachable lines.
239+
unreachable_lines: list[set[int]]
230240

231-
# Meaning of the tuple items: function_or_member, line, column, end_line, end_column:
241+
# One set of revealed types for each `reveal_type` statement. Each created set can
242+
# grow during the iteration. Meaning of the tuple items: function_or_member, line,
243+
# column, end_line, end_column:
232244
revealed_types: dict[tuple[str | None, int, int, int, int], set[str]]
233245

234-
# Not only the lines where the error report occurs but really all unreachable lines:
235-
unreachable_lines: set[int]
246+
def __init__(self) -> None:
247+
self.uselessness_errors = []
248+
self.unreachable_lines = []
249+
self.revealed_types = defaultdict(set)
250+
251+
252+
class IterationErrorWatcher(ErrorWatcher):
253+
"""Error watcher that filters and separately collects `unreachable` errors,
254+
`redundant-expr` and `redundant-casts` errors, and revealed types when analysing
255+
code sections iteratively to help avoid making too-hasty reports."""
256+
257+
iteration_dependent_errors: ÎterationDependentErrors
236258

237259
def __init__(
238260
self,
239261
errors: Errors,
262+
iteration_dependent_errors: ÎterationDependentErrors,
240263
*,
241264
filter_errors: bool | Callable[[str, ErrorInfo], bool] = False,
242265
save_filtered_errors: bool = False,
@@ -248,31 +271,66 @@ def __init__(
248271
save_filtered_errors=save_filtered_errors,
249272
filter_deprecated=filter_deprecated,
250273
)
251-
self.uselessness_errors = set()
252-
self.unreachable_lines = set()
253-
self.revealed_types = defaultdict(set)
274+
self.iteration_dependent_errors = iteration_dependent_errors
275+
iteration_dependent_errors.uselessness_errors.append(set())
276+
iteration_dependent_errors.unreachable_lines.append(set())
254277

255278
def on_error(self, file: str, info: ErrorInfo) -> bool:
279+
"""Filter out the "iteration-dependent" errors and notes and store their
280+
information to handle them after iteration is completed."""
281+
282+
iter_errors = self.iteration_dependent_errors
256283

257284
if info.code in (codes.UNREACHABLE, codes.REDUNDANT_EXPR, codes.REDUNDANT_CAST):
258-
self.uselessness_errors.add(
285+
iter_errors.uselessness_errors[-1].add(
259286
(info.code, info.message, info.line, info.column, info.end_line, info.end_column)
260287
)
261288
if info.code == codes.UNREACHABLE:
262-
self.unreachable_lines.update(range(info.line, info.end_line + 1))
289+
iter_errors.unreachable_lines[-1].update(range(info.line, info.end_line + 1))
263290
return True
264291

265292
if info.code == codes.MISC and info.message.startswith("Revealed type is "):
266293
key = info.function_or_member, info.line, info.column, info.end_line, info.end_column
267294
types = info.message.split('"')[1]
268295
if types.startswith("Union["):
269-
self.revealed_types[key].update(types[6:-1].split(", "))
296+
iter_errors.revealed_types[key].update(types[6:-1].split(", "))
270297
else:
271-
self.revealed_types[key].add(types)
298+
iter_errors.revealed_types[key].add(types)
272299
return True
273300

274301
return super().on_error(file, info)
275302

303+
def yield_error_infos(self) -> Iterator[tuple[str, Context, ErrorCode]]:
304+
"""Report only those `unreachable`, `redundant-expr`, and `redundant-casts`
305+
errors that could not be ruled out in any iteration step."""
306+
307+
persistent_uselessness_errors = set()
308+
iter_errors = self.iteration_dependent_errors
309+
for candidate in set(chain(*iter_errors.uselessness_errors)):
310+
if all(
311+
(candidate in errors) or (candidate[2] in lines)
312+
for errors, lines in zip(
313+
iter_errors.uselessness_errors, iter_errors.unreachable_lines
314+
)
315+
):
316+
persistent_uselessness_errors.add(candidate)
317+
for error_info in persistent_uselessness_errors:
318+
context = Context(line=error_info[2], column=error_info[3])
319+
context.end_line = error_info[4]
320+
context.end_column = error_info[5]
321+
yield error_info[1], context, error_info[0]
322+
323+
def yield_note_infos(self) -> Iterator[tuple[str, Context]]:
324+
"""Yield all types revealed in at least one iteration step."""
325+
326+
for note_info, types in self.iteration_dependent_errors.revealed_types.items():
327+
sorted_ = sorted(types, key=lambda typ: typ.lower())
328+
revealed = sorted_[0] if len(types) == 1 else f"Union[{', '.join(sorted_)}]"
329+
context = Context(line=note_info[1], column=note_info[2])
330+
context.end_line = note_info[3]
331+
context.end_column = note_info[4]
332+
yield f'Revealed type is "{revealed}"', context
333+
276334

277335
class Errors:
278336
"""Container for compile errors.

test-data/unit/check-narrowing.test

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2453,6 +2453,25 @@ while x is not None and b():
24532453

24542454
[builtins fixtures/primitives.pyi]
24552455

2456+
[case testAvoidFalseUnreachableInFinally]
2457+
# flags: --allow-redefinition-new --local-partial-types --warn-unreachable
2458+
def f() -> None:
2459+
try:
2460+
x = 1
2461+
if int():
2462+
x = ""
2463+
return
2464+
if int():
2465+
x = None
2466+
return
2467+
finally:
2468+
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str, None]"
2469+
if isinstance(x, str):
2470+
reveal_type(x) # N: Revealed type is "builtins.str"
2471+
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str, None]"
2472+
2473+
[builtins fixtures/isinstancelist.pyi]
2474+
24562475
[case testNarrowingTypeVarMultiple]
24572476
from typing import TypeVar
24582477

test-data/unit/check-redefine2.test

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -791,8 +791,7 @@ def f3() -> None:
791791
x = ""
792792
return
793793
finally:
794-
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" \
795-
# N: Revealed type is "builtins.int"
794+
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]"
796795
reveal_type(x) # N: Revealed type is "builtins.int"
797796

798797
def f4() -> None:

0 commit comments

Comments
 (0)