Repository: wemake-services/wemake-python-styleguide

Issue:
Forbid to use `&` and `|` with booleans
# Rule request

<!-- Thanks for coming up with a proposal! -->

## Thesis

<!-- What exactly do you propose? Please, provide code examples. -->

Yesterday I saw this code: 

```python
my_result = (first > 0 & second != 0) | other is None
```

And I was 😮 

So, we need to check all operations around `&` and `|` and detect any booleans there:
- Explicit `True` and `False`
- Compares with `is` or `==` or `!=`
- Compares with `>`, `>=`, etc
- Logical operations `not`, `or`, `and`

If we can detect at least one of these operations, then we should raise a violation. 
Let's call it `BinaryAndBooleanMixupViolation`.

Note: other binary operations are still fine!
So, these lines should not raise:
- `first & second`
- `first | 10`
- etc

## Reasoning

This case indicates that a person confused `&` with `and` and `|` with `or`.
This can be the case if a person is comming from another language. 



Discussion:
Please assign to me thank you
@TohDeKai I would love to! Can you please share your ideas about possible solution? This mees like a complicated task to me! 
Hi, I would like to try working on this issue. This would be my first issue, so I'm not sure of a solution yet as I'm still going through the contribution tutorials. I'll update once I have an idea for a solution, but any suggestion is welcome too! 

Also would calling & and | as bitwise operator be more appropriate? so the error would be BitwiseAndBooleanMixupViolation
LGTM, @thammaji! Thanks a lot!
Hi, was a bit busy over the past few weeks, but I've now gone through all of the contribution tutorials and internal docs; am a little intimidated, but I think I mostly understand. 

I just wanted to make sure I'm going in the right direction here: 

For this rule, I would likely have to create a new AST-based visitor, probably inside compares.py or conditions.py, then I would add a violation BitwiseAndBooleanMixupViolation to best_practices.py where it describes the violation, and the visitor should have logic to raise that violation. Also I'm not sure if I missed this in the tutorial but how is the code for the violation determined?
Looks like it! 👍 

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
@@ -77,6 +77,7 @@
     operators.UselessOperatorsVisitor,
     operators.WrongMathOperatorVisitor,
     operators.WalrusVisitor,
+    operators.BitwiseOpVisitor,
 
     compares.WrongConditionalVisitor,
     compares.CompareSanityVisitor,
@@ -86,6 +87,7 @@
     compares.InCompareSanityVisitor,
     compares.WrongFloatComplexCompareVisitor,
 
+
     conditions.IfStatementVisitor,
     conditions.BooleanConditionVisitor,
     conditions.ImplicitBoolPatternsVisitor,
diff --git a/wemake_python_styleguide/violations/best_practices.py b/wemake_python_styleguide/violations/best_practices.py
--- a/wemake_python_styleguide/violations/best_practices.py
+++ b/wemake_python_styleguide/violations/best_practices.py
@@ -81,6 +81,7 @@
    WrongMultilineStringUseViolation
    GetterWithoutReturnViolation
    EmptyCommentViolation
+   BitwiseAndBooleanMixupViolation
 
 Best practices
 --------------
@@ -150,6 +151,7 @@
 .. autoclass:: WrongMultilineStringUseViolation
 .. autoclass:: GetterWithoutReturnViolation
 .. autoclass:: EmptyCommentViolation
+.. autoclass:: BitwiseAndBooleanMixupViolation
 
 """
 
@@ -2504,3 +2506,35 @@ class EmptyCommentViolation(TokenizeViolation):
 
     error_template = 'Found empty comment'
     code = 464
+
+
+@final
+class BitwiseAndBooleanMixupViolation(ASTViolation):
+    """
+    Forbid comparisons between bitwise and boolean expressions.
+
+    Empty comments are only allowed in between valid comments.
+
+    Reasoning:
+       This case indicates that a person confused & with and and | with or.
+       This can be the case if a person is comming from another language.
+
+    Solution:
+        Change bitwise operator to boolean operators.
+
+    Example::
+
+        # Correct:
+
+        first | 10
+
+        # Wrong:
+
+        result = ((first > 0) & False)
+
+    .. versionadded:: 0.15.0
+
+    """
+
+    error_template = 'Likely bitwise and boolean operation mixup'
+    code = 465
diff --git a/wemake_python_styleguide/visitors/ast/operators.py b/wemake_python_styleguide/visitors/ast/operators.py
--- a/wemake_python_styleguide/visitors/ast/operators.py
+++ b/wemake_python_styleguide/visitors/ast/operators.py
@@ -13,6 +13,7 @@
 from wemake_python_styleguide.types import AnyNodes
 from wemake_python_styleguide.violations import consistency
 from wemake_python_styleguide.violations.best_practices import (
+    BitwiseAndBooleanMixupViolation,
     ListMultiplyViolation,
 )
 from wemake_python_styleguide.visitors import base, decorators
@@ -268,3 +269,37 @@ def visit_NamedExpr(
         """
         self.add_violation(consistency.WalrusViolation(node))
         self.generic_visit(node)
+
+
+@final
+class BitwiseOpVisitor(base.BaseNodeVisitor):
+    """Checks bitwise operations are used correctly."""
+
+    _invalid_nodes: ClassVar[AnyNodes] = (
+        ast.BoolOp,
+        ast.UnaryOp,
+        ast.NameConstant,
+        ast.Compare,
+    )
+
+    def visit_BinOp(self, node: ast.BinOp) -> None:
+        """
+        Finds bad usage of bitwise operation with binary operation.
+
+        Raises:
+            BitwiseAndBooleanMixupViolation
+
+        """
+        self._check_logical_bitwise_operator(node)
+        self.generic_visit(node)
+
+    def _check_logical_bitwise_operator(self, node: ast.BinOp) -> None:
+        if not isinstance(node.op, (ast.BitOr, ast.BitAnd)):
+            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)


Test patch:
diff --git a/tests/fixtures/noqa/noqa.py b/tests/fixtures/noqa/noqa.py
--- a/tests/fixtures/noqa/noqa.py
+++ b/tests/fixtures/noqa/noqa.py
@@ -765,3 +765,5 @@ def many_raises_function(parameter):  # noqa: WPS238
 
 def get_item():  # noqa: WPS463
     return  # noqa: WPS324
+
+bad_bitwise = True | True # noqa: WPS465
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
@@ -237,6 +237,7 @@
     'WPS462': 1,
     'WPS463': 1,
     'WPS464': 0,  # logically unacceptable.
+    'WPS465': 1,
 
     'WPS500': 1,
     'WPS501': 1,
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
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_operators/test_bitwise_boolean_mixup.py
@@ -0,0 +1,60 @@
+import pytest
+
+from wemake_python_styleguide.violations.best_practices import (
+    BitwiseAndBooleanMixupViolation,
+)
+from wemake_python_styleguide.visitors.ast.operators import BitwiseOpVisitor
+
+# add more cases for more usages of mixup
+
+
+@pytest.mark.parametrize('expression', [
+    'True | False',
+    '(x >= y) & True',
+    'first | (10 == second)',
+    '(11 != first) | (not False)',
+    '(1 or first) & (second and first)',
+    '(first > 0 & second != 0) | (other is None)',
+    '(5234 + 2345) & True',
+    'True | True',
+    'False & False',
+    'None & None',
+    'None | 1246',
+])
+def test_bitwise_boolean_mixup(
+    assert_errors,
+    parse_ast_tree,
+    expression,
+    default_options,
+):
+    """Testing for forbidden comparison between bitwise and boolean operator."""
+    tree = parse_ast_tree(expression)
+
+    visitor = BitwiseOpVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [BitwiseAndBooleanMixupViolation])
+
+
+@pytest.mark.parametrize('expression', [
+    'x | y',
+    'y & x',
+    'first & second',
+    '5 | 10',
+    '5 | x',
+    '(5 + 324 + 35) & y',
+    '(2 & 2 + 4 -5) | 5 - 3 + 3 & 3',
+])
+def test_correct_binary(
+    assert_errors,
+    parse_ast_tree,
+    expression,
+    default_options,
+):
+    """Testing allowed bitwise comparisions."""
+    tree = parse_ast_tree(expression)
+
+    visitor = BitwiseOpVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])