Repository: wemake-services/wemake-python-styleguide

Issue:
Restrict multiple `in` in comparisons 
# Rule request

## Thesis

We should restrict comparisons like:


```
>>> '6' in '6' in '6'
True

>>> '6' in '6' in [True]
False

>>> '6' in '5' in '65'
False

>>> '6' in '66' in '666'
True
```

## Reasoning

This is unreadable. Use different comparisons for it.
Restrict multiple `in` in comparisons 
# Rule request

## Thesis

We should restrict comparisons like:


```
>>> '6' in '6' in '6'
True

>>> '6' in '6' in [True]
False

>>> '6' in '5' in '65'
False

>>> '6' in '66' in '666'
True
```

## Reasoning

This is unreadable. Use different comparisons for it.


Discussion:

Resolving patch:
diff --git a/wemake_python_styleguide/violations/consistency.py b/wemake_python_styleguide/violations/consistency.py
--- a/wemake_python_styleguide/violations/consistency.py
+++ b/wemake_python_styleguide/violations/consistency.py
@@ -42,6 +42,7 @@
    ConstantComparisonViolation
    BadNumberSuffixViolation
    ComparisonOrderViolation
+   MultipleInComparisonViolation
 
 Consistency checks
 ------------------
@@ -57,6 +58,7 @@
 .. autoclass:: ConstantComparisonViolation
 .. autoclass:: BadNumberSuffixViolation
 .. autoclass:: ComparisonOrderViolation
+.. autoclass:: MultipleInComparisonViolation
 
 """
 
@@ -425,3 +427,36 @@ class BadNumberSuffixViolation(TokenizeViolation):
     #: Error message shown to the user.
     error_template = 'Found underscored number: {0}'
     code = 310
+
+
+class MultipleInComparisonViolation(ASTViolation):
+    """
+    Forbids comparision where multiple 'in's are userd in a statement.
+
+    Reasoning:
+        This is unreadable. Use different comparisons for it.
+        Bring a consistency to the comparison!
+
+    Solution:
+        Refactor your comparison expression to use several ``and`` conditions
+        or separate ``if`` statements in case it is appropriate.
+
+    Example::
+
+        # Correct:
+        if item in bucket and bucket in master_list_of_buckets:
+        if x_coord in line and line in square:
+
+        # Wrong:
+        if item in bucket in master_list_of_buckets:
+        if x_cord in line in square:
+
+    Note:
+        Returns Z311 as error code
+
+    """
+
+    should_use_text = False
+    #: Error message shown to the user.
+    error_template = 'Found multiple in comparisons'
+    code = 311
diff --git a/wemake_python_styleguide/visitors/ast/comparisons.py b/wemake_python_styleguide/visitors/ast/comparisons.py
--- a/wemake_python_styleguide/visitors/ast/comparisons.py
+++ b/wemake_python_styleguide/visitors/ast/comparisons.py
@@ -7,6 +7,7 @@
 from wemake_python_styleguide.violations.consistency import (
     ComparisonOrderViolation,
     ConstantComparisonViolation,
+    MultipleInComparisonViolation,
 )
 from wemake_python_styleguide.visitors.base import BaseNodeVisitor
 
@@ -123,3 +124,30 @@ def visit_Compare(self, node: ast.Compare) -> None:
         """
         self._check_ordering(node)
         self.generic_visit(node)
+
+
+# TODO(@sobolevn): refactor to be a single visitor
+class MultipleInVisitor(BaseNodeVisitor):
+    """Restricts comparision where multiple `in`s are used."""
+
+    def _has_multiple_in_comparisons(self, node: ast.Compare) -> bool:
+        count = 0
+        for op in node.ops:
+            if isinstance(op, ast.In):
+                count += 1
+        return count > 1
+
+    def _count_in_comparisons(self, node: ast.Compare) -> None:
+        if self._has_multiple_in_comparisons(node):
+            self.add_violation(MultipleInComparisonViolation(node))
+
+    def visit_Compare(self, node: ast.Compare) -> None:
+        """
+        Forbids comparisons including multiple 'in's in a statement.
+
+        Raise:
+            MultipleInComparisonViolation
+
+        """
+        self._count_in_comparisons(node)
+        self.generic_visit(node)
diff --git a/wemake_python_styleguide/visitors/presets/general.py b/wemake_python_styleguide/visitors/presets/general.py
--- a/wemake_python_styleguide/visitors/presets/general.py
+++ b/wemake_python_styleguide/visitors/presets/general.py
@@ -38,6 +38,7 @@
 
     comparisons.ConstantComparisonVisitor,
     comparisons.WrongOrderVisitor,
+    comparisons.MultipleInVisitor,
 
     # Classes:
     WrongClassVisitor,


Test patch:
diff --git a/tests/test_visitors/test_ast/test_comparisons/test_multiple_in.py b/tests/test_visitors/test_ast/test_comparisons/test_multiple_in.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_comparisons/test_multiple_in.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from wemake_python_styleguide.violations.consistency import (
+    MultipleInComparisonViolation,
+)
+from wemake_python_styleguide.visitors.ast.comparisons import MultipleInVisitor
+
+if_with_multiple_in_comaprisons = 'if {0} in {1} in {2}: ...'
+if_without_multiple_in_comparisons = 'if {0} in {1} : ...'
+
+ternary = 'ternary = 0 if {0} in {1} else 1'
+ternary_with_multiple_in = 'ternary = 0 if {0} in {1} in {2} else 1'
+while_construct = 'while {0} in {1}: ...'
+while_with_multiple_in = 'while {0} in {1} in {2}: ...'
+if_with_refactored_in = 'if x_coord in line and line in square: ...'
+
+
+@pytest.mark.parametrize('code', [
+    if_without_multiple_in_comparisons,
+    ternary,
+    while_construct,
+    if_with_refactored_in,
+])
+@pytest.mark.parametrize('comparators', [
+    ('x_coord', '6'),
+    ('status', [True]),
+    ('letter', ['a', 'b']),
+])
+def test_comparison_with_in(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    comparators,
+    default_options,
+):
+    """Comparisons work well for single `in`."""
+    tree = parse_ast_tree(code.format(*comparators))
+
+    visitor = MultipleInVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize('code', [
+    if_with_multiple_in_comaprisons,
+    ternary_with_multiple_in,
+    while_with_multiple_in,
+])
+@pytest.mark.parametrize('comparators', [
+    ('line', 'sqaure', 'shape'),
+    ('output', 'status', [True]),
+    ('letter', 'line', 'book'),
+])
+def test_comparison_with_multiple_in(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    comparators,
+    default_options,
+):
+    """Comparisons raise for multiple `in`s."""
+    tree = parse_ast_tree(code.format(*comparators))
+
+    visitor = MultipleInVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [MultipleInComparisonViolation])