diff --git a/mypy/checker.py b/mypy/checker.py index 002f28d4db6c..3016a0eb237f 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2415,6 +2415,29 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type self.infer_variable_type(inferred, lvalue, rvalue_type, rvalue) self.check_assignment_to_slots(lvalue) + def check_except_reachability(self, s: TryStmt) -> None: + """Perform except block reachability checks. + + 1. Check for duplicate exception clauses. + 2. Check if super class has already been caught. + """ + seen: List[Type] = [] + for expr in s.types: + if expr and isinstance(expr, NameExpr) and isinstance(expr.node, TypeInfo): + with self.expr_checker.msg.filter_errors(): + typ = get_proper_type(self.check_except_handler_test(expr)) + if isinstance(typ, AnyType) or isinstance(typ, UnionType): + continue + if typ in seen: + self.fail(message_registry.ALREADY_CAUGHT.format(format_type(typ)), expr) + continue + seen_superclass = next((s for s in seen if is_subtype(typ, s)), None) + if seen_superclass is not None: + self.fail(message_registry.SUPERCLASS_ALREADY_CAUGHT.format( + format_type(seen_superclass), format_type(typ)), expr) + continue + seen.append(typ) + # (type, operator) tuples for augmented assignments supported with partial types partial_type_augmented_ops: Final = { ('builtins.list', '+'), @@ -3777,6 +3800,9 @@ def visit_try_stmt(self, s: TryStmt) -> None: if not self.binder.is_unreachable(): self.accept(s.finally_body) + if self.should_report_unreachable_issues(): + self.check_except_reachability(s) + def visit_try_without_finally(self, s: TryStmt, try_frame: bool) -> None: """Type check a try statement, ignoring the finally block. diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 85d6d9dd4159..53b598bcbd13 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -127,7 +127,7 @@ def __str__(self) -> str: 'Reject returning value with "Any" type if return type is not "Any"', "General", ) -UNREACHABLE: Final = ErrorCode( +UNREACHABLE: Final[ErrorCode] = ErrorCode( "unreachable", "Warn about unreachable statements or expressions", "General" ) REDUNDANT_EXPR: Final = ErrorCode( diff --git a/mypy/message_registry.py b/mypy/message_registry.py index 0f14d706ccca..1f554224207b 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -257,3 +257,13 @@ def format(self, *args: object, **kwargs: object) -> "ErrorMessage": CLASS_PATTERN_UNKNOWN_KEYWORD: Final = 'Class "{}" has no attribute "{}"' MULTIPLE_ASSIGNMENTS_IN_PATTERN: Final = 'Multiple assignments to name "{}" in pattern' CANNOT_MODIFY_MATCH_ARGS: Final = 'Cannot assign to "__match_args__"' + +# Unreachable +ALREADY_CAUGHT: Final = ErrorMessage( + "Except is unreachable, {} has already been caught", + code=codes.UNREACHABLE, +) +SUPERCLASS_ALREADY_CAUGHT: Final = ErrorMessage( + "Except is unreachable, superclass {} of {} has already been caught", + code=codes.UNREACHABLE, +) diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index ef098c42901e..cdfd703ac0fa 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -841,6 +841,103 @@ def baz(x: int) -> int: return x [builtins fixtures/exception.pyi] +[case testUnreachableFlagExcepts] +# flags: --warn-unreachable +from typing import Type, NoReturn + +def error() -> NoReturn: ... + +try: + error() +except Exception: + pass +except Exception as err: # E: Except is unreachable, "Exception" has already been caught + pass +except RuntimeError: # E: Except is unreachable, superclass "Exception" of "RuntimeError" has already been caught + pass +except (NotImplementedError, ): + pass +except NotImplementedError: # E: Except is unreachable, superclass "Exception" of "NotImplementedError" has already been caught + pass +except NotImplementedError: # E: Except is unreachable, superclass "Exception" of "NotImplementedError" has already been caught + pass +[builtins fixtures/exception.pyi] + +[case testUnreachableFlagIgnoreVariablesInExcept] +# flags: --warn-unreachable +from typing import NoReturn, Union, Type, Tuple + +def error() -> NoReturn: ... +class MyError(BaseException): ... + +ignore: Type[Exception] +exclude: Tuple[Type[Exception], ...] +omit: Union[Type[RuntimeError], Type[MyError]] + +try: + error() +except ignore: + pass +except exclude: + pass +except omit: + pass +except RuntimeError: + pass +[builtins fixtures/exception.pyi] + +[case testUnreachableFlagExceptWithUnknownBaseClass] +# flags: --warn-unreachable +from typing import Any, NoReturn + +Parent: Any +class MyError(Parent): ... +def error() -> NoReturn: ... + +try: + error() +except MyError: + pass +except Exception: + pass +except MyError: # E: Except is unreachable, "MyError" has already been caught + pass +[builtins fixtures/exception.pyi] + +[case testUnreachableFlagExceptWithError] +# flags: --warn-unreachable +from typing import NoReturn + +# The following class is not derived from 'BaseException'. Thus an error is raised +# resulting in Any as type for the class in the exception expression. Such cases +# must be ignored in the rest of the reachability analysis. +class NotFromBaseException: ... + +def error() -> NoReturn: ... + +try: + error() +except NotFromBaseException: # E: Exception type must be derived from BaseException + pass +except RuntimeError: + pass +except NotImplementedError: # E: Except is unreachable, superclass "RuntimeError" of "NotImplementedError" has already been caught + pass +[builtins fixtures/exception.pyi] + +[case testUnreachableExceptWithoutUnreachableFlag] +from typing import NoReturn + +def error() -> NoReturn: ... + +try: + error() +except Exception: + pass +except RuntimeError: + pass +[builtins fixtures/exception.pyi] + [case testUnreachableFlagIgnoresSemanticAnalysisUnreachable] # flags: --warn-unreachable --python-version 3.7 --platform win32 --always-false FOOBAR import sys