Repository: wemake-services/wemake-python-styleguide

Issue:
false positive for new union types
# Bug report

## What's wrong

[PEP604](https://www.python.org/dev/peps/pep-0604/) allows `|` to be used for union types. you can now write `str | int` instead of `Union[str, int]` or `str | None` instead of `Optional[str]` and even `isinstance(foo, str | int)` will work. This feature is accepted for Python 3.10, but you can use it already with mypy>=0.800 and `from __future__ import annotations` (which will become the default in Python 3.10 as well).

However, currently this syntax triggers [WPS465](https://wemake-python-stylegui.de/en/0.15.1/pages/usage/violations/best_practices.html#wemake_python_styleguide.violations.best_practices.BitwiseAndBooleanMixupViolation).

A workaround is to write type declarations as strings like `"str | None"` or `"str | int"`.

## How it should be

The `|` operator will be used a lot in future Python versions (e.g. [Structural Pattern Matching](https://www.python.org/dev/peps/pep-0636/#or-patterns) uses it to define or-patterns). So I think this rule should not cause a violation for these special cases.

## System information

OS: Ubuntu 18.04.5 LTS x86_64
Python: 3.7.9
pip: 21.0.1

### flake8 information

```
{
  "dependencies": [],
  "platform": {
    "python_implementation": "CPython",
    "python_version": "3.7.9",
    "system": "Linux"
  },
  "plugins": [
    {
      "is_local": false,
      "plugin": "flake8-bandit",
      "version": "2.1.2"
    },
    {
      "is_local": false,
      "plugin": "flake8-broken-line",
      "version": "0.3.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-bugbear",
      "version": "20.11.1"
    },
    {
      "is_local": false,
      "plugin": "flake8-comprehensions",
      "version": "3.3.1"
    },
    {
      "is_local": false,
      "plugin": "flake8-darglint",
      "version": "1.6.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-debugger",
      "version": "4.0.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-docstrings",
      "version": "1.5.0, pydocstyle: 5.1.1"
    },
    {
      "is_local": false,
      "plugin": "flake8-eradicate",
      "version": "1.0.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-pytest-style",
      "version": "1.3.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-string-format",
      "version": "0.3.0"
    },
    {
      "is_local": false,
      "plugin": "flake8_commas",
      "version": "2.0.0"
    },
    {
      "is_local": false,
      "plugin": "flake8_isort",
      "version": "4.0.0"
    },
    {
      "is_local": false,
      "plugin": "flake8_quotes",
      "version": "3.2.0"
    },
    {
      "is_local": false,
      "plugin": "logging-format",
      "version": "0.6.0"
    },
    {
      "is_local": false,
      "plugin": "mccabe",
      "version": "0.6.1"
    },
    {
      "is_local": false,
      "plugin": "naming",
      "version": "0.11.1"
    },
    {
      "is_local": false,
      "plugin": "pycodestyle",
      "version": "2.6.0"
    },
    {
      "is_local": false,
      "plugin": "pyflakes",
      "version": "2.2.0"
    },
    {
      "is_local": false,
      "plugin": "rst-docstrings",
      "version": "0.0.14"
    },
    {
      "is_local": false,
      "plugin": "wemake_python_styleguide",
      "version": "0.15.0"
    }
  ],
  "version": "3.8.4"
}
```

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7ba411b..5f96bc5b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,11 @@ Semantic versioning in our case means:
   But, in the future we might change the configuration names / logic,
   change the client facing API, change code conventions signigicantly, etc.
 
+## 0.15.2
+
+### Bugfixes
+
+- Fixes `BitwiseAndBooleanMixupViolation` work with PEP 604 union types #1884
 
 ## 0.15.1
 
diff --git a/wemake_python_styleguide/visitors/ast/operators.py b/wemake_python_styleguide/visitors/ast/operators.py
index 1f5995bf..ca0e80d9 100644
--- a/wemake_python_styleguide/visitors/ast/operators.py
+++ b/wemake_python_styleguide/visitors/ast/operators.py
@@ -6,6 +6,7 @@ from typing_extensions import final
 from wemake_python_styleguide.compat.aliases import TextNodes
 from wemake_python_styleguide.compat.nodes import NamedExpr
 from wemake_python_styleguide.logic import walk
+from wemake_python_styleguide.logic.nodes import get_parent
 from wemake_python_styleguide.logic.tree.operators import (
     count_unary_operator,
     unwrap_unary_node,
@@ -297,9 +298,24 @@ class BitwiseOpVisitor(base.BaseNodeVisitor):
         if not isinstance(node.op, (ast.BitOr, ast.BitAnd)):
             return
 
+        if self._is_type_annotation(node):
+            return
+
         if self._is_bool_like(node.left) or self._is_bool_like(node.right):
             self.add_violation(BitwiseAndBooleanMixupViolation(node))
 
     # checks either side of the Bitwise operation invalid usage
     def _is_bool_like(self, node: ast.expr) -> bool:
         return isinstance(node, self._invalid_nodes)
+
+    # checks whether bitwise operator is used in a type annotation
+    def _is_type_annotation(self, node: ast.BinOp) -> bool:
+        parent_node = get_parent(node)
+
+        if isinstance(parent_node, (ast.AnnAssign, ast.arg)):
+            return node is parent_node.annotation
+
+        if isinstance(parent_node, ast.FunctionDef):
+            return node is parent_node.returns
+
+        return False


Test patch:
diff --git a/tests/test_visitors/test_ast/test_operators/test_bitwise_boolean_mixup.py b/tests/test_visitors/test_ast/test_operators/test_bitwise_boolean_mixup.py
index 967eb8f5..567fc6c3 100644
--- a/tests/test_visitors/test_ast/test_operators/test_bitwise_boolean_mixup.py
+++ b/tests/test_visitors/test_ast/test_operators/test_bitwise_boolean_mixup.py
@@ -58,3 +58,30 @@ def test_correct_binary(
     visitor.run()
 
     assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize('context', [
+    'foo: {0}',
+    'def fun(foo) -> {0}: ...',
+    'def fun(foo: {0}): ...',
+])
+@pytest.mark.parametrize('expression', [
+    'str | int',
+    'int | None',
+    'First | None',
+    'First | Second',
+])
+def test_union_type(
+    assert_errors,
+    parse_ast_tree,
+    context,
+    expression,
+    default_options,
+):
+    """Testing PEP 604 union types."""
+    tree = parse_ast_tree(context.format(expression))
+
+    visitor = BitwiseOpVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])