Repository: wemake-services/wemake-python-styleguide

Issue:
Forbid multi-line for and while loops
# Rule request


## Thesis

Code [like this](
https://github.com/sobolevn/itmo-2019/pull/24/files#diff-5ad37df7dcdec8d4aeb51aac0bde8387R51-R54) should be forbidden: 

```python
    for test_file in find_files(
        target_dir,
        '{0}*{1}'.format(TEST_PREFIX, PY_EXTENSION),
    ):
        module = import_module(test_file)
```

## Reasoning

`for`s should be simple and readable. The correct way to rewrite the same code is:

```python
    files = find_files(
        target_dir,
        '{0}*{1}'.format(TEST_PREFIX, PY_EXTENSION),
    )
    for test_file in files:
        module = import_module(test_file)
```

This is a consistency rule for `ast.For`, `ast.AsyncFor`, and `ast.While` nodes.

<!--

  Love wemake-python-styleguide? ❤️
  Do you want your features to be implemented faster?

  Please consider supporting our collective:
👉  https://opencollective.com/wemake-python-styleguide/donate

-->



Discussion:
I think I can handle it 
Assigned! Thanks a lot, @eternalgum! 
@abyss143 feel free to ping me in case you are still on it.
Happy to try this out :)
Thanks 👍 
@sobolevn Quick question. Should I be adding the check for this violation to the `WrongLoopVisitor`? It looks like that visitor will get the job done, but I wanted to make sure that is the correct one to use.
Yes, looks fine

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
@@ -77,6 +77,7 @@
    RedundantSubscriptViolation
    AugmentedAssignPatternViolation
    UnnecessaryLiteralsViolation
+   MultilineLoopViolation
 
 Consistency checks
 ------------------
@@ -133,6 +134,7 @@
 .. autoclass:: RedundantSubscriptViolation
 .. autoclass:: AugmentedAssignPatternViolation
 .. autoclass:: UnnecessaryLiteralsViolation
+.. autoclass:: MultilineLoopViolation
 
 """
 
@@ -1904,3 +1906,37 @@ class UnnecessaryLiteralsViolation(ASTViolation):
 
     error_template = 'Found unnecessary literals.'
     code = 351
+
+
+@final
+class MultilineLoopViolation(ASTViolation):
+    """
+    Forbids multiline loops.
+
+    Reasoning:
+        It decreased the readability of the code.
+
+    Solution:
+        Use single line loops and create new variables
+        in case you need to fit too many logic inside the loop definition.
+
+    Example::
+
+        # Correct
+
+        for num in some_function(arg1, arg2):
+            ...
+
+        # Wrong
+        for num in range(
+            arg1,
+            arg2,
+        ):
+            ...
+
+    .. versionadded:: 0.13.0
+
+    """
+
+    error_template = 'Forbids multiline loops'
+    code = 352
diff --git a/wemake_python_styleguide/visitors/ast/loops.py b/wemake_python_styleguide/visitors/ast/loops.py
--- a/wemake_python_styleguide/visitors/ast/loops.py
+++ b/wemake_python_styleguide/visitors/ast/loops.py
@@ -23,6 +23,7 @@
     TooManyForsInComprehensionViolation,
 )
 from wemake_python_styleguide.violations.consistency import (
+    MultilineLoopViolation,
     MultipleIfsInComprehensionViolation,
     UselessContinueViolation,
     WrongLoopIterTypeViolation,
@@ -122,11 +123,13 @@ def visit_any_loop(self, node: _AnyLoop) -> None:
         Raises:
             UselessLoopElseViolation
             LambdaInsideLoopViolation
+            MultilineLoopViolation
 
         """
         self._check_loop_needs_else(node)
         self._check_lambda_inside_loop(node)
         self._check_useless_continue(node)
+        self._check_multiline_loop(node)
         self.generic_visit(node)
 
     def _does_loop_contain_node(  # TODO: move, reuse in annotations.py
@@ -179,6 +182,20 @@ def _check_useless_continue(self, node: _AnyLoop) -> None:
         if any(isinstance(last, ast.Continue) for last in last_line):
             self.add_violation(UselessContinueViolation(node))
 
+    def _check_multiline_loop(self, node: _AnyLoop) -> None:
+        start_lineno = getattr(node, 'lineno', None)
+
+        if isinstance(node, ast.While):
+            node_to_check = node.test
+        else:
+            node_to_check = node.iter
+
+        for sub_node in ast.walk(node_to_check):
+            sub_lineno = getattr(sub_node, 'lineno', None)
+            if sub_lineno is not None and sub_lineno > start_lineno:
+                self.add_violation(MultilineLoopViolation(node))
+                break
+
 
 @final
 @decorators.alias('visit_any_for', (


Test patch:
diff --git a/tests/fixtures/noqa.py b/tests/fixtures/noqa.py
--- a/tests/fixtures/noqa.py
+++ b/tests/fixtures/noqa.py
@@ -580,5 +580,10 @@ def some_method(self):
 
 int()  # noqa: WPS351
 
+for wrong_loop in call(  # noqa: WPS352
+    1, 2, 3,
+):
+    print('bad loop')
+
 if a in {1}:  # noqa: WPS525
     print('bad!')
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
@@ -125,6 +125,7 @@
     'WPS349': 1,
     'WPS350': 1,
     'WPS351': 1,
+    'WPS352': 1,
 
     'WPS400': 0,
     'WPS401': 0,
diff --git a/tests/test_visitors/test_ast/test_loops/test_loops/test_multiline_loops.py b/tests/test_visitors/test_ast/test_loops/test_loops/test_multiline_loops.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_loops/test_loops/test_multiline_loops.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from wemake_python_styleguide.violations.consistency import (
+    MultilineLoopViolation,
+)
+from wemake_python_styleguide.visitors.ast.loops import WrongLoopVisitor
+
+incorrect_loop1 = """
+def wrapper():
+    for x in some_func(1,
+    3):
+        ...
+"""
+
+incorrect_loop2 = """
+def wrapper():
+    for x in (1,
+    2, 3, 4):
+        ...
+"""
+
+incorrect_loop3 = """
+while some_func(1,
+3):
+    ...
+"""
+
+correct_loop1 = """
+def wrapper():
+    for x in (1, 2, 3, 4):
+        ...
+"""
+
+correct_loop2 = """
+def wrapper():
+    for x in (1, 2, 3, 4):
+        ...
+    return
+"""
+
+correct_loop3 = """
+while some_func(1,3):
+    ...
+"""
+
+
+@pytest.mark.parametrize('code', [
+    incorrect_loop1,
+    incorrect_loop2,
+    incorrect_loop3,
+])
+def test_incorrect_multiline_loops(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+    mode,
+):
+    """Testing multiline loops."""
+    tree = parse_ast_tree(mode(code))
+
+    visitor = WrongLoopVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [MultilineLoopViolation])
+
+
+@pytest.mark.parametrize('code', [
+    correct_loop1,
+    correct_loop2,
+    correct_loop3,
+])
+def test_correct_multiline_loops(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+    mode,
+):
+    """Testing multiline loops."""
+    tree = parse_ast_tree(mode(code))
+
+    visitor = WrongLoopVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])