Repository: wemake-services/wemake-python-styleguide

Issue:
False positive BlockAndLocalOverlapViolation with type annotation
# 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
With given code
```python
import asyncpg as apg


async def db_operation(db: apg.pool.Pool):
    """Operation with db."""
    connection: apg.Connection
    async with db.acquire() as connection:
        return await connection.fetch(
            """
            SELECT * FROM table
            """
        )
```
`BlockAndLocalOverlapViolation` raises with 
```
     7:   5 WPS440 Found block variables overlap: connection [wemake-python-styleguide]
  async with db.acquire() as connection:
  ^
```

Without line ```connection: apg.Connection``` everything is ok.
<!-- Describe what is not working. Please, attach a traceback. -->

## How is that should be
This is valid python annotation and `BlockAndLocalOverlapViolation` violation should not be raised here.

<!-- Describe how it should work. -->

<!--

  Love wemake-python-styleguide? ❤️
  Do you want your bugs to be solved faster?

  Please consider supporting our collective:
👉  https://opencollective.com/wemake-python-styleguide/donate

-->



Discussion:
Yeap, this is a bug. @AlwxSin do you want to fix it?

We should ignore names from `AnnAssign` without direct values.
@sobolevn Yep, I'll do it. Assign on me, please.
@sobolevn well, I've got a problem. I added one more `_scope_predicate` to `BlockVariableVisitor` which returns `True` if node is `AnnAssign` and there is no `value` in it. But the problem is in code to test
```python
def context():
    conn: Connection
    with db.get_conn() as conn:
        ...
```
`with` block has no parent, so [here](https://github.com/wemake-services/wemake-python-styleguide/blob/master/wemake_python_styleguide/visitors/ast/blocks.py#L155) node is `None` and obviously node is not `AnnAssign`.

`BlockVariableVisitor` is fragile and complex and I'm not sure how to fix it.
@AlwxSin you are interested in this method: https://github.com/wemake-services/wemake-python-styleguide/blob/master/wemake_python_styleguide/visitors/ast/blocks.py#L161

Here's how the flow works:
1. We start with `visit_locals`
2. Then we fall into `_scope` with `node: ast.AnnAssign`
3. Then we check `ignored_name` in `_naming_predicates`: https://github.com/wemake-services/wemake-python-styleguide/blob/master/wemake_python_styleguide/visitors/ast/blocks.py#L194-L197

That's where you need to add a new check: if it is a `AnnAssign` without a value, then the name is ignored.

By the way, if you find this code complex and unreadable we should at least write some docs about it. Do you want to help with it? Basically, all the same things I wrote here.
> if you find this code complex and unreadable

I just copied docstring which said that it fragile and complex 😏

Resolving patch:
diff --git a/wemake_python_styleguide/logic/scope_predicates.py b/wemake_python_styleguide/logic/scope_predicates.py
new file mode 100644
--- /dev/null
+++ b/wemake_python_styleguide/logic/scope_predicates.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+
+import ast
+from typing import Set
+
+from wemake_python_styleguide.compat.aliases import AssignNodes, FunctionNodes
+from wemake_python_styleguide.logic.source import node_to_string
+
+#: That's what we expect from `@overload` decorator:
+_overload_exceptions = frozenset(('overload', 'typing.overload'))
+
+#: That's what we expect from `@property` decorator:
+_property_exceptions = frozenset(('property', '.setter'))
+
+
+def is_function_overload(node: ast.AST) -> bool:
+    """Check that function decorated with `typing.overload`."""
+    if isinstance(node, FunctionNodes):
+        for decorator in node.decorator_list:
+            if node_to_string(decorator) in _overload_exceptions:
+                return True
+    return False
+
+
+def is_property_setter(node: ast.AST, _=None):
+    """Check that function decorated with `property.setter`."""
+    if isinstance(node, FunctionNodes):
+        for decorator in node.decorator_list:
+            if node_to_string(decorator) in _property_exceptions:
+                return True
+    return False
+
+
+def is_same_value_reuse(node: ast.AST, names: Set[str]) -> bool:
+    """Checks if the given names are reused by the given node."""
+    if isinstance(node, AssignNodes) and node.value:
+        used_names = {
+            name_node.id
+            for name_node in ast.walk(node.value)
+            if isinstance(name_node, ast.Name)
+        }
+        if not names.difference(used_names):
+            return True
+    return False
+
+
+def is_no_value_annotation(node: ast.AST, _=None) -> bool:
+    """Check that variable has annotation without value."""
+    return isinstance(node, ast.AnnAssign) and not node.value
diff --git a/wemake_python_styleguide/logic/scopes.py b/wemake_python_styleguide/logic/scopes.py
--- a/wemake_python_styleguide/logic/scopes.py
+++ b/wemake_python_styleguide/logic/scopes.py
@@ -6,21 +6,13 @@
 
 from typing_extensions import final
 
-from wemake_python_styleguide.compat.aliases import AssignNodes, FunctionNodes
 from wemake_python_styleguide.logic.naming import access, name_nodes
 from wemake_python_styleguide.logic.nodes import get_context
-from wemake_python_styleguide.logic.source import node_to_string
 from wemake_python_styleguide.types import ContextNodes
 
 #: That's how we represent scopes that are bound to contexts.
 _ContextStore = DefaultDict[ContextNodes, Set[str]]
 
-#: That's what we expect from `@overload` decorator:
-_overload_exceptions = frozenset(('overload', 'typing.overload'))
-
-#: That's what we expect from `@property` decorator:
-_property_exceptions = frozenset(('property', '.setter'))
-
 
 class _BaseScope(object):
     """Base class for scope operations."""
@@ -137,34 +129,3 @@ def _build_outer_context(self) -> Set[str]:
 def extract_names(node: ast.AST) -> Set[str]:
     """Extracts unique set of names from a given node."""
     return set(name_nodes.get_variables_from_node(node))
-
-
-def is_function_overload(node: ast.AST) -> bool:
-    """Check that function decorated with `typing.overload`."""
-    if isinstance(node, FunctionNodes):
-        for decorator in node.decorator_list:
-            if node_to_string(decorator) in _overload_exceptions:
-                return True
-    return False
-
-
-def is_property_setter(node: ast.AST, _=None):
-    """Check that function decorated with `property.setter`."""
-    if isinstance(node, FunctionNodes):
-        for decorator in node.decorator_list:
-            if node_to_string(decorator) in _property_exceptions:
-                return True
-    return False
-
-
-def is_same_value_reuse(node: ast.AST, names: Set[str]) -> bool:
-    """Checks if the given names are reused by the given node."""
-    if isinstance(node, AssignNodes) and node.value:
-        used_names = {
-            name_node.id
-            for name_node in ast.walk(node.value)
-            if isinstance(name_node, ast.Name)
-        }
-        if not names.difference(used_names):
-            return True
-    return False
diff --git a/wemake_python_styleguide/visitors/ast/blocks.py b/wemake_python_styleguide/visitors/ast/blocks.py
--- a/wemake_python_styleguide/visitors/ast/blocks.py
+++ b/wemake_python_styleguide/visitors/ast/blocks.py
@@ -20,13 +20,16 @@
     flat_variable_names,
 )
 from wemake_python_styleguide.logic.nodes import get_context, get_parent
+from wemake_python_styleguide.logic.scope_predicates import (
+    is_function_overload,
+    is_no_value_annotation,
+    is_property_setter,
+    is_same_value_reuse,
+)
 from wemake_python_styleguide.logic.scopes import (
     BlockScope,
     OuterScope,
     extract_names,
-    is_function_overload,
-    is_property_setter,
-    is_same_value_reuse,
 )
 from wemake_python_styleguide.logic.walk import is_contained_by
 from wemake_python_styleguide.types import (
@@ -92,6 +95,7 @@ class BlockVariableVisitor(base.BaseNodeVisitor):
     _naming_predicates: Tuple[_NamePredicate, ...] = (
         is_function_overload,
         is_property_setter,
+        is_no_value_annotation,
     )
 
     _scope_predicates: Tuple[_ScopePredicate, ...] = (


Test patch:
diff --git a/tests/test_visitors/test_ast/test_blocks/conftest.py b/tests/test_visitors/test_ast/test_blocks/conftest.py
--- a/tests/test_visitors/test_ast/test_blocks/conftest.py
+++ b/tests/test_visitors/test_ast/test_blocks/conftest.py
@@ -7,19 +7,28 @@
 simple_assign = '{0} = 1'
 multiple_assign = '{0} = unmatched_assign = 1'
 annotated_assign1 = '{0}: type = 1'
-annotated_assign2 = '{0}: type'
+simple_annotation = '{0}: type'
 unpacking_assign1 = '{0}, unmatched_assign = (1, 2)'
 unpacking_assign2 = 'unmatched_assign, *{0} = (1, 2)'
 
-
-@pytest.fixture(params=[
+_assigned_statements = [
     simple_assign,
     multiple_assign,
     annotated_assign1,
-    annotated_assign2,
     unpacking_assign1,
     unpacking_assign2,
-])
+]
+
+_assigned_and_annotation_statements = _assigned_statements + [simple_annotation]
+
+
+@pytest.fixture(params=_assigned_statements)
 def assign_statement(request):
     """Parametrized fixture that contains all possible assign templates."""
     return request.param
+
+
+@pytest.fixture(params=_assigned_and_annotation_statements)
+def assign_and_annotation_statement(request):
+    """Parametrized fixture that contains all possible assign templates."""
+    return request.param
diff --git a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_blocks_overlap.py b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_blocks_overlap.py
--- a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_blocks_overlap.py
+++ b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_blocks_overlap.py
@@ -98,6 +98,13 @@ def context():
         ...
 """
 
+annotation_overlap = """
+def context():
+    conn: Connection
+    with db.get_conn() as conn:
+        ...
+"""
+
 
 @pytest.mark.parametrize('code', [
     import_and_class1,
@@ -131,15 +138,16 @@ def test_block_overlap(
     unused_variables_overlap1,
     unused_variables_overlap2,
     unused_variables_overlap3,
+    annotation_overlap,
 ])
-def test_block_unused_overlap(
+def test_block_correct_overlap(
     assert_errors,
     parse_ast_tree,
     default_options,
     code,
     mode,
 ):
-    """Testing that overlaps between unused vars are ok."""
+    """Testing that correct overlaps are ok."""
     tree = parse_ast_tree(mode(code))
 
     visitor = BlockVariableVisitor(default_options, tree=tree)
diff --git a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_class_block.py b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_class_block.py
--- a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_class_block.py
+++ b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_class_block.py
@@ -46,7 +46,7 @@ def test_class_block_overlap(
     assert_error_text,
     parse_ast_tree,
     class_statement,
-    assign_statement,
+    assign_and_annotation_statement,
     context,
     variable_name,
     default_options,
@@ -54,7 +54,7 @@ def test_class_block_overlap(
     """Ensures that overlaping variables exist."""
     code = context.format(
         class_statement.format(variable_name),
-        assign_statement.format(variable_name),
+        assign_and_annotation_statement.format(variable_name),
     )
     tree = parse_ast_tree(code)
 
@@ -149,7 +149,7 @@ def test_class_block_correct(
     assert_errors,
     parse_ast_tree,
     class_statement,
-    assign_statement,
+    assign_and_annotation_statement,
     context,
     first_name,
     second_name,
@@ -158,7 +158,7 @@ def test_class_block_correct(
     """Ensures that different variables do not overlap."""
     code = context.format(
         class_statement.format(first_name),
-        assign_statement.format(second_name),
+        assign_and_annotation_statement.format(second_name),
     )
     tree = parse_ast_tree(code)
 
@@ -184,7 +184,7 @@ def test_class_internal_block_correct(
     assert_errors,
     parse_ast_tree,
     class_statement,
-    assign_statement,
+    assign_and_annotation_statement,
     context,
     first_name,
     second_name,
@@ -193,7 +193,7 @@ def test_class_internal_block_correct(
     """Ensures that different variables do not overlap."""
     code = context.format(
         class_statement.format('SomeUniqueClassName'),
-        assign_statement.format(second_name),
+        assign_and_annotation_statement.format(second_name),
         first_name,
     )
     tree = parse_ast_tree(code)
diff --git a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_function_block.py b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_function_block.py
--- a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_function_block.py
+++ b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_function_block.py
@@ -95,7 +95,7 @@ def test_method_block_overlap(
     assert_errors,
     parse_ast_tree,
     function_statement,
-    assign_statement,
+    assign_and_annotation_statement,
     context,
     variable_name,
     default_options,
@@ -104,7 +104,7 @@ def test_method_block_overlap(
     """Ensures that overlaping variables exist."""
     code = context.format(
         function_statement.format(variable_name),
-        assign_statement.format(variable_name),
+        assign_and_annotation_statement.format(variable_name),
     )
     tree = parse_ast_tree(mode(code))
 
@@ -165,7 +165,7 @@ def test_function_block_correct(
     assert_errors,
     parse_ast_tree,
     function_statement,
-    assign_statement,
+    assign_and_annotation_statement,
     context,
     first_name,
     second_name,
@@ -175,7 +175,7 @@ def test_function_block_correct(
     """Ensures that different variables do not overlap."""
     code = context.format(
         function_statement.format(first_name),
-        assign_statement.format(second_name),
+        assign_and_annotation_statement.format(second_name),
     )
     tree = parse_ast_tree(mode(code))
 
diff --git a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_import_block.py b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_import_block.py
--- a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_import_block.py
+++ b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_import_block.py
@@ -52,7 +52,7 @@ def test_import_block_overlap(
     assert_error_text,
     parse_ast_tree,
     import_statement,
-    assign_statement,
+    assign_and_annotation_statement,
     context,
     variable_name,
     default_options,
@@ -61,7 +61,7 @@ def test_import_block_overlap(
     """Ensures that overlaping variables exist."""
     code = context.format(
         import_statement.format(variable_name),
-        assign_statement.format(variable_name),
+        assign_and_annotation_statement.format(variable_name),
     )
     tree = parse_ast_tree(mode(code))
 
@@ -127,7 +127,7 @@ def test_import_block_correct(
     assert_errors,
     parse_ast_tree,
     import_statement,
-    assign_statement,
+    assign_and_annotation_statement,
     context,
     first_name,
     second_name,
@@ -137,7 +137,7 @@ def test_import_block_correct(
     """Ensures that different variables do not overlap."""
     code = context.format(
         import_statement.format(first_name),
-        assign_statement.format(second_name),
+        assign_and_annotation_statement.format(second_name),
     )
     tree = parse_ast_tree(mode(code))
 
diff --git a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_loop_blocks.py b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_loop_blocks.py
--- a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_loop_blocks.py
+++ b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_loop_blocks.py
@@ -63,7 +63,7 @@ def test_for_block_overlap(
     assert_error_text,
     parse_ast_tree,
     for_statement,
-    assign_statement,
+    assign_and_annotation_statement,
     context,
     variable_name,
     default_options,
@@ -72,7 +72,7 @@ def test_for_block_overlap(
     """Ensures that overlaping variables exist."""
     code = context.format(
         for_statement.format(variable_name),
-        assign_statement.format(variable_name),
+        assign_and_annotation_statement.format(variable_name),
     )
     tree = parse_ast_tree(mode(code))
 
@@ -138,7 +138,7 @@ def test_for_block_correct(
     assert_errors,
     parse_ast_tree,
     for_statement,
-    assign_statement,
+    assign_and_annotation_statement,
     context,
     first_name,
     second_name,
@@ -148,7 +148,7 @@ def test_for_block_correct(
     """Ensures that different variables do not overlap."""
     code = context.format(
         for_statement.format(first_name),
-        assign_statement.format(second_name),
+        assign_and_annotation_statement.format(second_name),
     )
     tree = parse_ast_tree(mode(code))
 
diff --git a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_try_block.py b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_try_block.py
--- a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_try_block.py
+++ b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_try_block.py
@@ -196,7 +196,7 @@ def test_except_block_correct(
     assert_errors,
     parse_ast_tree,
     except_statement,
-    assign_statement,
+    assign_and_annotation_statement,
     context,
     first_name,
     second_name,
@@ -206,7 +206,7 @@ def test_except_block_correct(
     """Ensures that different variables do not overlap."""
     code = context.format(
         except_statement.format(first_name),
-        assign_statement.format(second_name),
+        assign_and_annotation_statement.format(second_name),
     )
     tree = parse_ast_tree(mode(code))
 
diff --git a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_with_block.py b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_with_block.py
--- a/tests/test_visitors/test_ast/test_blocks/test_overlap/test_with_block.py
+++ b/tests/test_visitors/test_ast/test_blocks/test_overlap/test_with_block.py
@@ -67,7 +67,7 @@ def test_with_block_overlap(
     assert_error_text,
     parse_ast_tree,
     with_statement,
-    assign_statement,
+    assign_and_annotation_statement,
     context,
     variable_name,
     default_options,
@@ -76,7 +76,7 @@ def test_with_block_overlap(
     """Ensures that overlaping variables exist."""
     code = context.format(
         with_statement.format(variable_name),
-        assign_statement.format(variable_name),
+        assign_and_annotation_statement.format(variable_name),
     )
     tree = parse_ast_tree(mode(code))
 
@@ -146,7 +146,7 @@ def test_with_block_correct(
     assert_errors,
     parse_ast_tree,
     with_statement,
-    assign_statement,
+    assign_and_annotation_statement,
     context,
     first_name,
     second_name,
@@ -156,7 +156,7 @@ def test_with_block_correct(
     """Ensures that different variables do not overlap."""
     code = context.format(
         with_statement.format(first_name),
-        assign_statement.format(second_name),
+        assign_and_annotation_statement.format(second_name),
     )
     tree = parse_ast_tree(mode(code))