Repository: wemake-services/wemake-python-styleguide

Issue:
Type annotation check doesn't work for type aliases
# Bug report

<!--
Hi, thanks for submitting a bug. We appreciate that.

But, we will need some information about what's wrong to help you.
-->

## What's wrong
Type annotation check doesn't work if type aliases are used. For example, this code
```
type_alias = Literal[None]

def my_func(arg: type_alias):
    pass
```
passes flake8 checks without raising `WPS701` error, while this code
```
def my_func(arg: Literal[None]):
    pass
```
makes flake8 complain: "WPS701 Found useless `Literal[None]` typing annotation".
## How is that should be
Type annotation errors should be raised whether type aliases are used or not.

## System information
wemake-python-styleguide: 0.12.5


Discussion:
Thanks! That's true.

Do you want to fix it? 
@sobolevn well, I'd like to try but I guess it'll take too much time and it's better to be fixed by someone more experienced.
I can make a guess, that one would need to:

- add `visit_Assign` here: https://github.com/wemake-services/wemake-python-styleguide/blob/master/wemake_python_styleguide/visitors/ast/annotations.py#L26
- cover two assign cases: `T = Literal[None]` and `T, R  = Literal[None], Other` where we only care about the value part
- add a regression test, in a form of these two new templates here: https://github.com/wemake-services/wemake-python-styleguide/blob/master/tests/test_visitors/test_ast/test_annotations/test_annotation_metadata/conftest.py#L25

That's it.

Resolving patch:
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
@@ -54,6 +54,8 @@ def func(empty: None):
             '''Empty function.'''
 
         # Wrong:
+        NONE_ALIAS = Literal[None]
+
         def func(empty: Literal[None]):
             '''Empty function.'''
 
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,7 +1,9 @@
 # -*- coding: utf-8 -*-
 
 import ast
-from typing import ClassVar, FrozenSet, Optional
+import re
+from typing import ClassVar, FrozenSet, Union
+from typing.re import Pattern
 
 from typing_extensions import final
 
@@ -31,6 +33,11 @@ class _GenericAnnotationVisitor(BaseNodeVisitor):
         'typing_extensions.',
     ))
 
+    def visit_Assign(self, node: ast.Assign) -> None:
+        """Checks assignment patterns."""
+        self._check_annotation(node)
+        self.generic_visit(node)
+
     @final
     def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
         """
@@ -75,7 +82,9 @@ def _get_annotation(self, node: ast.AST) -> str:
             full_annotation = full_annotation.replace(prefix, '')
         return full_annotation
 
-    def _check_annotation(self, annotation: Optional[ast.expr]) -> None:
+    def _check_annotation(
+        self, annotation: Union[ast.expr, ast.Assign, None],
+    ) -> None:
         """The only method that need to me implemented in child visitors."""
 
 
@@ -83,13 +92,17 @@ def _check_annotation(self, annotation: Optional[ast.expr]) -> None:
 class SemanticAnnotationVisitor(_GenericAnnotationVisitor):
     """Ensures that nested annotations are used correctly."""
 
-    _flat_types: ClassVar[FrozenSet[str]] = frozenset((
-        'Literal',
-        'Union',
-        'Annotated',
+    # matches nested annotation like Literal[Literal[]],
+    # but not neighbors like Literal[], Literal[]
+    _flat_types: ClassVar[FrozenSet[Pattern]] = frozenset((
+        re.compile(r'^.*Literal\[[^\]]*Literal'),
+        re.compile(r'^.*Union\[[^\]]*Union'),
+        re.compile(r'^.*Annotated\[[^\]]*Annotated'),
     ))
 
-    def _check_annotation(self, annotation: Optional[ast.expr]) -> None:
+    def _check_annotation(
+        self, annotation: Union[ast.expr, ast.Assign, None],
+    ) -> None:
         if not annotation:
             return
 
@@ -97,18 +110,28 @@ def _check_annotation(self, annotation: Optional[ast.expr]) -> None:
         self._check_literal_none(annotation)
         self._check_union_nested_in_optional(annotation)
 
-    def _check_nested_annotations(self, annotation: ast.expr) -> None:
+    def _check_nested_annotations(
+        self, annotation: Union[ast.expr, ast.Assign],
+    ) -> None:
         annotation_string = self._get_annotation(annotation)
         for flat_type in self._flat_types:
-            if annotation_string.count(flat_type) > 1:
+            if flat_type.search(annotation_string):
                 self.add_violation(NestedAnnotationsViolation(annotation))
 
-    def _check_literal_none(self, annotation: ast.expr) -> None:
+    def _check_literal_none(
+        self, annotation: Union[ast.expr, ast.Assign],
+    ) -> None:
         annotation_string = self._get_annotation(annotation)
-        if 'Literal[None]' in annotation_string:
+        assign_string = (
+            isinstance(annotation, ast.Assign) and
+            isinstance(annotation.value, ast.Str)
+        )
+        if not assign_string and 'Literal[None]' in annotation_string:
             self.add_violation(LiteralNoneViolation(annotation))
 
-    def _check_union_nested_in_optional(self, annotation: ast.expr) -> None:
+    def _check_union_nested_in_optional(
+        self, annotation: Union[ast.expr, ast.Assign],
+    ) -> None:
         annotation_string = self._get_annotation(annotation)
         if 'Optional[Union[' in annotation_string:
             self.add_violation(UnionNestedInOptionalViolation(annotation))


Test patch:
diff --git a/tests/test_visitors/test_ast/test_annotations/test_annotation_metadata/conftest.py b/tests/test_visitors/test_ast/test_annotations/test_annotation_metadata/conftest.py
--- a/tests/test_visitors/test_ast/test_annotations/test_annotation_metadata/conftest.py
+++ b/tests/test_visitors/test_ast/test_annotations/test_annotation_metadata/conftest.py
@@ -21,12 +21,27 @@ class Test(object):
 variable: {0} = some()
 """
 
+annotation_alias_template1 = """
+a = {0}
+"""
+
+annotation_alias_template2 = """
+b = c = {0}
+"""
+
+annotation_alias_template3 = """
+d, e = {0}, {0}
+"""
+
 
 @pytest.fixture(params=[
     function_arg_template,
     function_return_template,
     class_field_template,
     variable_template,
+    annotation_alias_template1,
+    annotation_alias_template2,
+    annotation_alias_template3,
 ])
 def annotation_template(request):
     """Returns all possible annotation places."""
diff --git a/tests/test_visitors/test_ast/test_annotations/test_multiline_annotations/test_argument_annotations.py b/tests/test_visitors/test_ast/test_annotations/test_multiline_annotations/test_argument_annotations.py
--- a/tests/test_visitors/test_ast/test_annotations/test_multiline_annotations/test_argument_annotations.py
+++ b/tests/test_visitors/test_ast/test_annotations/test_multiline_annotations/test_argument_annotations.py
@@ -28,7 +28,7 @@ def function(arg: Optional[int]): ...
 correct_multiline_arguments = """
 def function(
     arg1: str,
-    arg2: Optional[Union[int, str]],
+    arg2: Union[int, str, None],
 ): ...
 """
 
diff --git a/tests/test_visitors/test_ast/test_annotations/test_multiline_annotations/test_return_annotations.py b/tests/test_visitors/test_ast/test_annotations/test_multiline_annotations/test_return_annotations.py
--- a/tests/test_visitors/test_ast/test_annotations/test_multiline_annotations/test_return_annotations.py
+++ b/tests/test_visitors/test_ast/test_annotations/test_multiline_annotations/test_return_annotations.py
@@ -24,7 +24,7 @@ def function() -> Optional[int]: ...
 correct_multiline_return = """
 def function(
     arg1,
-) -> Optional[Union[int, str]]: ...
+) -> Union[int, str, None]: ...
 """
 
 # Wrong: