Repository: wemake-services/wemake-python-styleguide

Issue:
Forbid nested `typing.Literal` and `typing.Union`
# Rule request

<!-- Thanks for coming up with a proposal! -->

## Thesis

<!-- What exactly do you propose? Please, provide code examples. -->

From PEP-586:

> Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]
> This should be exactly equivalent to the following type:
> Literal[1, 2, 3, "foo", 5, None]

Let's follow the last one.

The same applies for:

- `Annotated`
- `Union`

## Reasoning

Consistency :upside_down_face:  Heavily related to #829

<!--
Why do you think this is a good idea?

Please, include some strong points and considerations.
-->

<!--

  Love wemake-python-styleguide? ❤️
  Do you want your features to be implemented faster?

  Please consider supporting our collective:
👉  https://opencollective.com/wemake-python-styleguide/donate

-->



Discussion:
Can I work on this?
Sure, @hhsdev! Thanks a lot!

Resolving patch:
diff --git a/wemake_python_styleguide/presets/types/tree.py b/wemake_python_styleguide/presets/types/tree.py
--- a/wemake_python_styleguide/presets/types/tree.py
+++ b/wemake_python_styleguide/presets/types/tree.py
@@ -43,6 +43,7 @@
 
     attributes.WrongAttributeVisitor,
     annotations.WrongAnnotationVisitor,
+    annotations.WrongNestedAnnotationVisitor,
 
     functions.WrongFunctionCallVisitor,
     functions.FunctionDefinitionVisitor,
diff --git a/wemake_python_styleguide/violations/annotations.py b/wemake_python_styleguide/violations/annotations.py
--- a/wemake_python_styleguide/violations/annotations.py
+++ b/wemake_python_styleguide/violations/annotations.py
@@ -17,11 +17,13 @@
    :nosignatures:
 
    LiteralNoneViolation
+   NestedAnnotationsViolation
 
 Annotation checks
 ------------------
 
 .. autoclass:: LiteralNoneViolation
+.. autoclass:: NestedAnnotationsViolation
 
 """
 
@@ -59,3 +61,33 @@ def func(empty: Literal[None]):
 
     code = 701
     error_template = 'Found useless `Literal[None]` typing annotation'
+
+
+@final
+class NestedAnnotationsViolation(ASTViolation):
+    """
+    Forbids use of nested Literal and Union Annotation.
+
+    Reasoning:
+        There is no need to nest certain annotations of the same type.
+        They are exactly equivalent to the flattened version.
+        Use the flattened version for consistency.
+
+    Solution:
+        Flatten consecutively nested ``typing.Literal`` and ``typing.Union``.
+
+    Example::
+        # Correct:
+        Literal[1, 2, 3, "foo", 5, None]
+        Union[Union[int, str], float]
+
+        # Wrong:
+        Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]
+        Union[int, str, float]
+
+    .. versionadded:: 0.13.0
+
+    """
+
+    error_template = 'Found redundant nested typing annotation'
+    code = 702
diff --git a/wemake_python_styleguide/visitors/ast/annotations.py b/wemake_python_styleguide/visitors/ast/annotations.py
--- a/wemake_python_styleguide/visitors/ast/annotations.py
+++ b/wemake_python_styleguide/visitors/ast/annotations.py
@@ -1,13 +1,14 @@
 # -*- coding: utf-8 -*-
 
 import ast
-from typing import cast
+from typing import ClassVar, FrozenSet, cast
 
 from typing_extensions import final
 
 from wemake_python_styleguide.types import AnyFunctionDef
 from wemake_python_styleguide.violations.annotations import (
     LiteralNoneViolation,
+    NestedAnnotationsViolation,
 )
 from wemake_python_styleguide.violations.consistency import (
     MultilineFunctionAnnotationViolation,
@@ -16,6 +17,86 @@
 from wemake_python_styleguide.visitors.decorators import alias
 
 
+@final
+@alias('visit_any_function', (
+    'visit_FunctionDef',
+    'visit_AsyncFunctionDef',
+))
+class WrongNestedAnnotationVisitor(BaseNodeVisitor):
+    """Ensures that nested annotations are used correctly."""
+
+    _flat_types: ClassVar[FrozenSet[str]] = frozenset((
+        'Literal', 'Union', 'Annotated',
+    ))
+
+    def visit_any_function(self, node: AnyFunctionDef) -> None:
+        """
+        Checks return type annotations.
+
+        Raises:
+            NestedAnnotationsViolation
+
+        """
+        self._check_return_nested_annotation(node)
+        self.generic_visit(node)
+
+    def visit_arg(self, node: ast.arg) -> None:
+        """
+        Checks arguments annotations.
+
+        Raises:
+            NestedAnnotationsViolation
+
+        """
+        self._check_arg_nested_annotation(node)
+        self.generic_visit(node)
+
+    def _same_subscript(self, parent_node: ast.Subscript, child_node) -> bool:
+        parent_annotation = self._get_annotation(parent_node)
+        child_annotation = self._get_annotation(child_node)
+        return parent_annotation == child_annotation
+
+    def _get_annotation(self, node) -> str:
+        if isinstance(node, ast.Name):
+            return node.id
+        if isinstance(node, ast.Attribute):
+            return node.attr
+        if isinstance(node, ast.Subscript):
+            return self._get_annotation(node.value)
+        return ''
+
+    def _check_nested_annotation(self, node: ast.Subscript) -> None:
+        node_annotation = self._get_annotation(node)
+
+        if node_annotation not in self._flat_types:
+            return
+
+        slice_index = cast(ast.Index, node.slice)
+        slice_args = slice_index.value
+
+        if self._same_subscript(node, slice_args):
+            self.add_violation(NestedAnnotationsViolation(node))
+            return
+        if isinstance(slice_args, ast.Tuple):
+            for arg in slice_args.elts:
+                if self._same_subscript(node, arg):
+                    self.add_violation(NestedAnnotationsViolation(node))
+                    return
+
+    def _check_arg_nested_annotation(self, node: ast.arg) -> None:
+        for sub_node in ast.walk(node):
+            if isinstance(sub_node, ast.Subscript):
+                self._check_nested_annotation(sub_node)
+
+    def _check_return_nested_annotation(self, node: AnyFunctionDef) -> None:
+        if not node.returns:
+            return
+
+        for sub_node in ast.walk(node.returns):
+            if isinstance(sub_node, ast.Subscript):
+                self._check_nested_annotation(sub_node)
+
+
 @final
 @alias('visit_any_function', (
     'visit_FunctionDef',
@@ -30,6 +111,7 @@ def visit_any_function(self, node: AnyFunctionDef) -> None:
 
         Raises:
             MultilineFunctionAnnotationViolation
+            LiteralNoneAnnotation
 
         """
         self._check_return_annotation(node)
@@ -41,6 +123,7 @@ def visit_arg(self, node: ast.arg) -> None:
 
         Raises:
             MultilineFunctionAnnotationViolation
+            LiteralNoneAnnotation
 
         """
         self._check_arg_annotation(node)


Test patch:
diff --git a/tests/fixtures/noqa.py b/tests/fixtures/noqa.py
--- a/tests/fixtures/noqa.py
+++ b/tests/fixtures/noqa.py
@@ -563,11 +563,19 @@ async def async_gen(self):
 
 
 def literal_none_func(arg: Literal[None]):  # noqa: WPS701
-    '''Literal[None]'''
+    """Literal[None]"""
 
 
 def literal_none_return_func() -> Literal[None]:  # noqa: WPS701
-    '''Literal[None]'''
+    """Literal[None]"""
+
+
+def nested_annotation_func(arg: Literal[Literal[1, 2], 3]): # noqa: WPS702
+    """Literal[Literal[1, 2], 3]"""
+
+
+def nested_annotation_return_func() -> Union[str, Union[int, float]]: # noqa: WPS702
+    """Union[str, Union[int, float]]"""
 
 
 from json import loads  # noqa: WPS347
diff --git a/tests/test_checker/test_noqa.py b/tests/test_checker/test_noqa.py
--- a/tests/test_checker/test_noqa.py
+++ b/tests/test_checker/test_noqa.py
@@ -208,6 +208,7 @@
     'WPS612': 1,
 
     'WPS701': 2,
+    'WPS702': 2,
 })
 
 
diff --git a/tests/test_visitors/test_ast/test_annotations/test_argument_annotations.py b/tests/test_visitors/test_ast/test_annotations/test_argument_annotations.py
--- a/tests/test_visitors/test_ast/test_annotations/test_argument_annotations.py
+++ b/tests/test_visitors/test_ast/test_annotations/test_argument_annotations.py
@@ -5,7 +5,9 @@
 from wemake_python_styleguide.visitors.ast.annotations import (
     LiteralNoneViolation,
     MultilineFunctionAnnotationViolation,
+    NestedAnnotationsViolation,
     WrongAnnotationVisitor,
+    WrongNestedAnnotationVisitor,
 )
 
 # Correct:
@@ -41,6 +43,30 @@ def function(arg: Literal[True]) -> Literal["foo"]: ...
 def function(empty_arg: None): ...
 """
 
+correct_unnested_literal_annotation = """
+def function(arg: Literal[1, 2, 3]): ...
+"""
+
+correct_unnested_union_annotation = """
+def function(arg: Union[int, str, float]): ...
+"""
+
+correct_unnested_combined_annotation = """
+def function(arg: Union[str, Literal[1]]): ...
+"""
+
+correct_unnested_annotated_annotation = """
+def function(arg: Annotated[int]): ...
+"""
+
+correct_unnested_prefixed_literal_annotation = """
+def function(arg: typing.Literal[1, 2, 3]): ...
+"""
+
+correct_unnested_combined_prefixed_annotation = """
+def function(arg: typing.Union[Literal[1]]): ...
+"""
+
 # Wrong:
 
 wrong_multiline_arguments = """
@@ -68,6 +94,90 @@ def function(empty_arg: Literal[None]): ...
 def function(empty_arg: Union[Literal[None], Optional[int]]): ...
 """
 
+wrong_nested_literal_annotation = """
+def function(arg: Literal[Literal[1, 2], 3]): ...
+"""
+
+wrong_nested_union_annotation = """
+def function(arg: Union[Union[int, str], float]): ...
+"""
+
+wrong_nested_annotated_annotation = """
+def function(arg: Annotated[Annotated[int, str], float]): ...
+"""
+
+wrong_deep_nested_literal_annotation = """
+def function(arg: Literal[Literal[Literal[1]]]): ...
+"""
+
+wrong_deep_nested_union_annotation = """
+def function(arg: Union[Union[Union[int]]]): ...
+"""
+
+wrong_deep_nested_annotated_annotation = """
+def function(arg: Annotated[Annotated[Annotated[int]]]): ...
+"""
+
+wrong_nested_prefixed_literal_annotation = """
+def function(arg: typing.Literal[typing.Literal[1]]): ...
+"""
+
+wrong_deep_nested_prefixed_literal_annotation = """
+def function(arg: typing.Literal[typing.Literal[typing.Literal[1]]]): ...
+"""
+
+wrong_nested_combined_annotation = """
+def function(arg: typing.Union[Union[int], str]): ...
+"""
+
+
+@pytest.mark.parametrize('code', [
+    wrong_nested_literal_annotation,
+    wrong_nested_union_annotation,
+    wrong_nested_annotated_annotation,
+    wrong_nested_combined_annotation,
+    wrong_nested_prefixed_literal_annotation,
+])
+def test_forbidden_nested_annotations(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+    mode,
+):
+    """Ensures that using nested ``Literal`` and ``Union`` is forbidden."""
+    tree = parse_ast_tree(mode(code))
+
+    visitor = WrongNestedAnnotationVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [NestedAnnotationsViolation])
+
+
+@pytest.mark.parametrize('code', [
+    wrong_deep_nested_literal_annotation,
+    wrong_deep_nested_union_annotation,
+    wrong_deep_nested_annotated_annotation,
+    wrong_deep_nested_prefixed_literal_annotation,
+])
+def test_forbidden_deep_nested_annotations(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+    mode,
+):
+    """Ensures that using nested ``Literal`` and ``Union`` is forbidden."""
+    tree = parse_ast_tree(mode(code))
+
+    visitor = WrongNestedAnnotationVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [
+        NestedAnnotationsViolation,
+        NestedAnnotationsViolation,
+    ])
+
 
 @pytest.mark.parametrize('code', [
     wrong_arg_none_annotation,
@@ -132,3 +242,27 @@ def test_correct_argument_annotation(
     visitor.run()
 
     assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize('code', [
+    correct_unnested_union_annotation,
+    correct_unnested_literal_annotation,
+    correct_unnested_annotated_annotation,
+    correct_unnested_combined_annotation,
+    correct_unnested_prefixed_literal_annotation,
+    correct_unnested_combined_prefixed_annotation,
+])
+def test_correct_unnested_argument_annotation(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+    mode,
+):
+    """Ensures that it is possible to use correct type annotations."""
+    tree = parse_ast_tree(mode(code))
+
+    visitor = WrongNestedAnnotationVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])
diff --git a/tests/test_visitors/test_ast/test_annotations/test_return_annotations.py b/tests/test_visitors/test_ast/test_annotations/test_return_annotations.py
--- a/tests/test_visitors/test_ast/test_annotations/test_return_annotations.py
+++ b/tests/test_visitors/test_ast/test_annotations/test_return_annotations.py
@@ -5,7 +5,9 @@
 from wemake_python_styleguide.visitors.ast.annotations import (
     LiteralNoneViolation,
     MultilineFunctionAnnotationViolation,
+    NestedAnnotationsViolation,
     WrongAnnotationVisitor,
+    WrongNestedAnnotationVisitor,
 )
 
 # Correct:
@@ -36,8 +38,52 @@ def function(arg) -> None: ...
 def function(arg) -> Union[None, Optional[int]]: ...
 """
 
+correct_unnested_literal_return = """
+def function() -> Literal[1, 2, 3]: ...
+"""
+
+correct_unnested_union_return = """
+def function() -> Union[int, str, float]: ...
+"""
+
+correct_unnested_annotated_return = """
+def function() -> Annotated[int, str, float]: ...
+"""
+
+correct_unnested_prefixed_literal_return = """
+def function() -> typing.Literal[1, 2, 3]: ...
+"""
+
+correct_unnested_combined_literal_return = """
+def function() -> Union[typing.Literal[2, 3], int]: ...
+"""
+
 # Wrong:
 
+wrong_nested_literal_return = """
+def function() -> Literal[Literal[1, 2], 3]: ...
+"""
+
+wrong_nested_union_return = """
+def function() -> Union[Union[int, str], float]: ...
+"""
+
+wrong_nested_annotated_return = """
+def function() -> Annotated[Annotated[int, str], float]: ...
+"""
+
+wrong_deep_nested_annotated_return = """
+def function() -> Annotated[Annotated[Annotated[int]]]: ...
+"""
+
+wrong_deep_nested_literal_return = """
+def function() -> Literal[Literal[Literal[1]]]: ...
+"""
+
+wrong_deep_nested_union_return = """
+def function() -> Union[Union[Union[int]]]: ...
+"""
+
 wrong_embedded_return_none_annotation = """
 def function(arg) -> Union[Literal[None], Optional[int]]: ...
 """
@@ -75,6 +121,18 @@ def function(
 ]: ...
 """
 
+wrong_nested_prefixed_literal_return = """
+def function() -> typing.Literal[typing.Literal[1]]: ...
+"""
+
+wrong_deep_nested_prefixed_literal_return = """
+def function() -> typing.Literal[typing.Literal[typing.Literal[1]]]: ...
+"""
+
+wrong_nested_combined_union_return = """
+def function() -> typing.Union[Union[1]]: ...
+"""
+
 
 @pytest.mark.parametrize('code', [
     wrong_multiline_return1,
@@ -117,6 +175,54 @@ def test_wrong_literal_none_return_annotation(
     assert_errors(visitor, [LiteralNoneViolation])
 
 
+@pytest.mark.parametrize('code', [
+    wrong_nested_literal_return,
+    wrong_nested_union_return,
+    wrong_nested_prefixed_literal_return,
+    wrong_nested_combined_union_return,
+    wrong_nested_annotated_return,
+])
+def test_wrong_nested_return_annotations(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+    mode,
+):
+    """Ensures that nested return ``Literal`` and ``Union`` is forbidden."""
+    tree = parse_ast_tree(mode(code))
+
+    visitor = WrongNestedAnnotationVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [NestedAnnotationsViolation])
+
+
+@pytest.mark.parametrize('code', [
+    wrong_deep_nested_literal_return,
+    wrong_deep_nested_union_return,
+    wrong_deep_nested_annotated_return,
+    wrong_deep_nested_prefixed_literal_return,
+])
+def test_wrong_deep_nested_return_annotations(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+    mode,
+):
+    """Ensures that nested return ``Literal`` and ``Union`` is forbidden."""
+    tree = parse_ast_tree(mode(code))
+
+    visitor = WrongNestedAnnotationVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [
+        NestedAnnotationsViolation,
+        NestedAnnotationsViolation,
+    ])
+
+
 @pytest.mark.parametrize('code', [
     correct_function_without_annotations,
     correct_simple_return,
@@ -139,3 +245,25 @@ def test_correct_return_annotation(
     visitor.run()
 
     assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize('code', [
+    correct_unnested_literal_return,
+    correct_unnested_union_return,
+    correct_unnested_annotated_return,
+    correct_unnested_prefixed_literal_return,
+])
+def test_correct_unnested_annotation(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+    mode,
+):
+    """Ensures that it is possible to use correct unnested annotations."""
+    tree = parse_ast_tree(mode(code))
+
+    visitor = WrongNestedAnnotationVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])