diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 5efec6855593..ccb69d9a134d 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -477,6 +477,11 @@ Configuring warnings The following flags enable warnings for code that is sound but is potentially problematic or redundant in some way. +.. option:: --warn-redundant-annotation + + This flag will make mypy report an error when a function local variable uses + an unnecessary annotation in an assignment that can safely be removed. + .. option:: --warn-redundant-casts This flag will make mypy report an error whenever your code uses diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index 7abd1f02db68..970fcd27a235 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -635,6 +635,15 @@ Configuring warnings For more information, see the :ref:`Configuring warnings ` section of the command line docs. +.. confval:: warn_redundant_annotation + + :type: boolean + :default: False + + Warns when the annotation type is the same as the inferred type. + + This option may only be set in the global section (``[mypy]``). + .. confval:: warn_redundant_casts :type: boolean diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index bd2436061974..02f79aa08bc1 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -66,6 +66,23 @@ Example: def __init__(self) -> None: self.value = 0 +.. _code-redundant-annotation: + +Check that annotation is not redundant [redundant-annotation] +------------------------------------------------------------- + +If you use :option:`--warn-redundant-annotation `, mypy will generate an error if a +function local annotation type is the same as the inferred type. + +Example: + +.. code-block:: python + + # mypy: warn-redundant-annotation + + # Error: Annotation "int" is redundant [redundant-annotation] + count: int = 4 + .. _code-redundant-cast: Check that cast is not redundant [redundant-cast] diff --git a/mypy/checker.py b/mypy/checker.py index 07f5c520de95..ddb2c7a1b47b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3206,6 +3206,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: Handle all kinds of assignment statements (simple, indexed, multiple). """ + self.check_redundant_annotation(s) + # Avoid type checking type aliases in stubs to avoid false # positives about modern type syntax available in stubs such # as X | Y. @@ -3258,6 +3260,20 @@ def check_type_alias_rvalue(self, s: AssignmentStmt) -> None: alias_type = self.expr_checker.accept(s.rvalue) self.store_type(s.lvalues[-1], alias_type) + def check_redundant_annotation(self, s: AssignmentStmt) -> None: + if ( + self.options.warn_redundant_annotation + and not s.is_final_def + and not s.is_alias_def + and s.unanalyzed_type is not None + and s.type is not None + and not is_same_type(s.type, AnyType(TypeOfAny.special_form)) + and is_same_type(s.type, self.expr_checker.accept(s.rvalue)) + and (defn := self.scope.current_function()) is not None + and defn.name != "__init__" + ): + self.msg.redundant_annotation(s.type, s.type) + def check_assignment( self, lvalue: Lvalue, diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 785b6166b18b..f2632c9feed6 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -162,6 +162,9 @@ def __hash__(self) -> int: "Disallow calling functions without type annotations from annotated functions", "General", ) +REDUNDANT_ANNOTATION: Final = ErrorCode( + "redundant-annotation", "Check that the annotation is necessary or can be omitted", "General" +) REDUNDANT_CAST: Final = ErrorCode( "redundant-cast", "Check that cast changes type of expression", "General" ) diff --git a/mypy/main.py b/mypy/main.py index 7d5721851c3d..35828dfedf65 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -820,6 +820,13 @@ def add_invertible_flag( title="Configuring warnings", description="Detect code that is sound but redundant or problematic.", ) + add_invertible_flag( + "--warn-redundant-annotation", + default=False, + strict_flag=False, + help="Warn when an annotation is the same as its inferred type", + group=lint_group, + ) add_invertible_flag( "--warn-redundant-casts", default=False, diff --git a/mypy/messages.py b/mypy/messages.py index 9fdfb748b288..5230248f6942 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1784,6 +1784,13 @@ def unsupported_type_type(self, item: Type, context: Context) -> None: f'Cannot instantiate type "type[{format_type_bare(item, self.options)}]"', context ) + def redundant_annotation(self, typ: Type, context: Context) -> None: + self.fail( + f"Annotation {format_type(typ, self.options)} is redundant (inferred type is the same)", + context, + code=codes.REDUNDANT_ANNOTATION, + ) + def redundant_cast(self, typ: Type, context: Context) -> None: self.fail( f"Redundant cast to {format_type(typ, self.options)}", diff --git a/mypy/options.py b/mypy/options.py index 39490c9f0bee..4665abc0118e 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -175,6 +175,9 @@ def __init__(self) -> None: # Also check typeshed for missing annotations self.warn_incomplete_stub = False + # Warn when an annotation is the same as its inferred type + self.warn_redundant_annotation = False + # Warn about casting an expression to its inferred type self.warn_redundant_casts = False diff --git a/test-data/unit/check-warnings.test b/test-data/unit/check-warnings.test index a2d201fa301d..46a707d8fd4f 100644 --- a/test-data/unit/check-warnings.test +++ b/test-data/unit/check-warnings.test @@ -68,6 +68,62 @@ z = Any def f(q: Union[x, y, z]) -> None: cast(Union[x, y], q) +-- Redundant annotation +-- -------------------- + +[case testRedundantAnnotation] +# flags: --warn-redundant-annotation +from typing import Literal +a = 1 +b: int = a +c: Literal[1] = 1 +d: list[str] = [] +def f() -> None: + a = 1 + b: int = a # E: Annotation "int" is redundant (inferred type is the same) + c: Literal[1] = 1 # E: Annotation "Literal[1]" is redundant (inferred type is the same) + d: list[str] = [] + +class x: + def __init__(self) -> None: + self.a: int = 1 + + def f(self) -> None: + a = 1 + b: int = a # E: Annotation "int" is redundant (inferred type is the same) + c: Literal[1] = 1 # E: Annotation "Literal[1]" is redundant (inferred type is the same) + d: list[str] = [] + +[case testRedundantAnnotationTypeVar] +# flags: --warn-redundant-annotation +from typing import Literal, TypeVar + +T = TypeVar("T") + +def f(x: T) -> T: + return x + +def g() -> None: + x: Literal[1] = f(1) + y: list[str] = f([]) + +[case testRedundantAnnotationSkips] +# flags: --warn-redundant-annotation +from dataclasses import dataclass +from typing import ClassVar, NamedTuple + +class a: + b: ClassVar[int] = 1 + c: ClassVar = "test" + +class d(NamedTuple): + e: int = 1 + +@dataclass +class f: + g: int = 1 +[builtins fixtures/tuple.pyi] + -- Unused 'type: ignore' comments -- ------------------------------