Repository: wemake-services/wemake-python-styleguide

Issue:
Warn about possibly infinite `while`
# Rule request

Check that `while True` block has `raise` or `return`.

**UPD**: and `break` too. 

<!-- Thanks for coming up with a proposal! -->

## Thesis

It's OK to not have them for CLI entry point, but in other cases, it can be an error. So, let's ask users explicitly `noqa` such cases to be sure that they really know what they're doing.

<!-- What exactly do you propose? Please, provide code examples. -->

## Reasoning

<!--
Why do you think this is a good idea?

Please, include some strong points and considerations.
-->

<!--

  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:
`break` is also an option. I like this one!
Hi, I've been looking to contribute to this project and would like to try and implement this rule if nobody is on it!
Sure, I would be happy to answer your questions! Thanks!
Hi there! After perusing through the source code and the tutorial documentation, I was thinking that this rule would best fit in wemake_python_styleguide\visitors\ast\loops.py. Does this seem like a good place to start?
Yes, it does! Do you want to take this one?

Feel free to ask for help! 👍 
Sorry about the delayed responses on this issue! I'll work on this implementation tomorrow. Thanks!
I was able to successfully call "make test" without any errors when I first cloned the repo, but after implemented a few changes, I go the following error complaining that "no shebang is present:"

![no_shebang](https://user-images.githubusercontent.com/38593243/70352737-d0974b00-1839-11ea-8e99-4ebe08160c94.JPG)

I thought that this may have been caused by the changed I introduced to the source code, but after recloning the repo, the same error messages persist.

I could not find much specific help on this issue online and was wondering if you have run into this? I am running on Ubuntu 18.04 by the way. Thanks!
This warning means that you file has executable rights, but does not have a sebang `#!/...` in its source code. And since these files should be executable at all, I guess the problem is inside your machine.

Check file rights with `ls -alh`, they should not have `+x` for all categories.

You can also send me `git status` output, it might help.
Ah yes, chmod did the trick to fix the file rights. Thanks!
I'm a bit confused by the error "WPS214 Found too many methods: 9" error that is printed out when flake8 runs in wemake_python_styleguide\visitors\ast\loops.py. It seems like you've talked about this in a previous issue https://github.com/wemake-services/wemake-python-styleguide/pull/678.

So, do I have to reduce the number of methods that I have added to the WrongLoopVisitor class? I added two functions to this class, one for parsing a node to check if it violates the rule and a helper function. However, this error was still thrown when I did not have a helper function (the number of methods was 8).
I created a new pull request just in case you need access to see the specific code I am referring to. Thanks!
It seem like these functions in  WrongLoopVisitor class are cluttering things up and need to be reused or moved.

![trouble_functions](https://user-images.githubusercontent.com/38593243/70453799-60760880-1a77-11ea-8782-2a6ef14b4513.JPG)

However, I cannot figure out how to change/move them without breaking other things in the code.
Move them to `logic/`
Great thanks!

I moved those functions to logic and everything seems to be working, but there is an issue right now with running pytest. The rule, as defined, raises the violation in any "while True:" loop that does not contain a break, return, or raise, but there are instances of these loops in some of the other test files like the one below (wemake-python-styleguide/tests/test_visitors/test_ast/test_loops/test_loops/test_lambda_in_loop.py).

![Screen Shot 2019-12-09 at 8 48 38 PM](https://user-images.githubusercontent.com/38593243/70488058-71e60180-1ac5-11ea-820a-ce9140ec663c.png)

It seems like there are two options:
1) add a break to these tests
2) change the way the infinite while loop rule is defined so that these instances don't fail

I'm leaning towards (1) because it seems like the while loops in these test cases would be incorrect because they don't necessarily exit. Does this seem like the best approach?
Also, what exactly would an integration test to add to tests/fixtures/noqa.py look like? I'm a bit confused on what I should add here. Like I probably shouldn't just add "while True:" because it might hang forever.
I think I figured it out. I removed those lines from test_lambda_in_loop.py because they are actual violations of the infinite loop rule. All of the requirements have been satisfied except for the adding of an integration test in noqa.py because I could not figure out how to include an infinite loop without pytest failing when running this file. I'll submit another pull request right now, but please let me know if it is possible to add an integration test for this. Thanks!
@ragohin may I ask how you fixed the `EXE002` error exactly? I have just forked the project and get this error the first time i try to run `make test`. 

I run Ubuntu 16.04 but we get the same error in Ubuntu 18.04.
@pandaseal this is related to your folder structure. Run `ls -alh` and then fix file permissions if you have any. That should solve it.
@sobolevn should I do it manually for every file that fails? I do not really know how to go about all of it... Any tips?
It really dependends on your problem and your OS. I don't think that I can help without any further information. You can also just ignore this issue for now. It works on CI correctly.
@sobolevn We fixed it. Thanks for your help.

**If anyone else has [this](https://github.com/wemake-services/wemake-python-styleguide/issues/1007#issuecomment-562719702) problem, this was the solution:**

- System: Ubuntu 16.04 WSL on Windows 10
- Problem: Windows overwrote the permissions of the WSL when the repo was located in the Windows partition (`/mnt`), changing the permissions did not work. [Here](https://github.com/Microsoft/WSL/issues/81) is why changing permissions wouldn't work.
- Solution: Clone the repo _directly onto the home_ (ie. do `cd` and then clone the repo) of the WSL.
@pandaseal you can submit this to `CONTRIBUTING.md`
@sobolevn should I create an issue where we can discuss this further? For example where the information should be added in `CONTRIBUTING.md` and on which level it should be described? (I mean, at this point is not really relevant for _this_ issue)

Resolving patch:
diff --git a/wemake_python_styleguide/logic/tree/loops.py b/wemake_python_styleguide/logic/tree/loops.py
--- a/wemake_python_styleguide/logic/tree/loops.py
+++ b/wemake_python_styleguide/logic/tree/loops.py
@@ -1,23 +1,46 @@
 import ast
+from typing import Optional
 
 from wemake_python_styleguide.compat.aliases import ForNodes
-from wemake_python_styleguide.logic import walk
-from wemake_python_styleguide.types import AnyLoop
+from wemake_python_styleguide.types import AnyLoop, AnyNodes
 
 
-def _is_nested_loop(node: AnyLoop, sub: ast.AST) -> bool:
-    return isinstance(sub, (*ForNodes, ast.While)) and sub is not node
+def _does_loop_contain_node(
+    loop: Optional[AnyLoop],
+    to_check: ast.AST,
+) -> bool:
+    """
+    Helper function to check for break statement in a nested loop.
 
+    If a loop contains a loop with a break, this ensures that
+    we don't count the outside loop as having a break.
+    """
+    if loop is None:
+        return False
 
-def has_break(node: AnyLoop) -> bool:
-    """Tells whether or not given loop has ``break`` keyword in its body."""
+    for inner_node in ast.walk(loop):
+        # We are checking this specific node, not just any `break`:
+        if to_check is inner_node:
+            return True
+    return False
+
+
+def has_break(
+    node: AnyLoop,
+    *,
+    break_nodes: AnyNodes,
+) -> bool:
+    """Checks whether loop contains a break statement."""
     closest_loop = None
 
-    for sub in ast.walk(node):
-        if _is_nested_loop(node, sub):
-            closest_loop = sub
+    for subnode in ast.walk(node):
+        if isinstance(subnode, (*ForNodes, ast.While)) and subnode is not node:
+            closest_loop = subnode
 
-        if isinstance(sub, ast.Break):
-            if not closest_loop or not walk.is_contained_by(sub, closest_loop):
+        if isinstance(subnode, break_nodes):
+            is_nested_break = _does_loop_contain_node(
+                closest_loop, subnode,
+            )
+            if not is_nested_break:
                 return True
     return False
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
@@ -73,6 +73,7 @@
    BaseExceptionRaiseViolation
    NonTrivialExceptViolation
    FloatingNanViolation
+   InfiniteWhileLoopViolation
 
 Best practices
 --------------
@@ -134,6 +135,7 @@
 .. autoclass:: BaseExceptionRaiseViolation
 .. autoclass:: NonTrivialExceptViolation
 .. autoclass:: FloatingNanViolation
+.. autoclass:: InfiniteWhileLoopViolation
 
 """
 
@@ -2224,3 +2226,33 @@ class FloatingNanViolation(ASTViolation):
 
     error_template = 'Found "NaN" as argument to float()'
     code = 456
+
+
+@final
+class InfiniteWhileLoopViolation(ASTViolation):
+    """
+    Forbids use of infinite ``while True:`` loops.
+
+    Reasoning:
+        Infinite loops will cause bugs in code.
+
+    Solution:
+        Add either a return, raise, or break to handle the infinite loop.
+
+    Example::
+
+        # Correct:
+        while True:
+            print('forever')
+            break
+
+        # Wrong:
+        while True:
+            print('forever')
+
+    .. versionadded:: 0.15.0
+
+    """
+
+    error_template = 'Found an infinite while loop'
+    code = 457
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
@@ -18,6 +18,7 @@
     AnyNodes,
 )
 from wemake_python_styleguide.violations.best_practices import (
+    InfiniteWhileLoopViolation,
     LambdaInsideLoopViolation,
     LoopVariableDefinitionViolation,
     YieldInComprehensionViolation,
@@ -122,6 +123,12 @@ def _post_visit(self) -> None:
 class WrongLoopVisitor(base.BaseNodeVisitor):
     """Responsible for examining loops."""
 
+    _breaks: ClassVar[AnyNodes] = (
+        ast.Break,
+        ast.Return,
+        ast.Raise,
+    )
+
     _containers: ClassVar[_ContainerSpec] = {
         ast.ListComp: ['elt'],
         ast.SetComp: ['elt'],
@@ -152,16 +159,18 @@ def visit_any_loop(self, node: AnyLoop) -> None:
             UselessLoopElseViolation
             LambdaInsideLoopViolation
             MultilineLoopViolation
+            InfiniteWhileLoopViolation
 
         """
         self._check_loop_needs_else(node)
         self._check_lambda_inside_loop(node)
         self._check_useless_continue(node)
         self._check_multiline_loop(node)
+        self._check_infinite_while_loop(node)
         self.generic_visit(node)
 
     def _check_loop_needs_else(self, node: AnyLoop) -> None:
-        if node.orelse and not loops.has_break(node):
+        if node.orelse and not loops.has_break(node, break_nodes=(ast.Break,)):
             self.add_violation(UselessLoopElseViolation(node))
 
     def _check_lambda_inside_loop(
@@ -203,6 +212,15 @@ def _check_multiline_loop(self, node: AnyLoop) -> None:
                 self.add_violation(MultilineLoopViolation(node))
                 break
 
+    def _check_infinite_while_loop(self, node: AnyLoop) -> None:
+        if not isinstance(node, ast.While):
+            return
+
+        real_node = operators.unwrap_unary_node(node.test)
+        if isinstance(real_node, ast.NameConstant) and real_node.value is True:
+            if not loops.has_break(node, break_nodes=self._breaks):
+                self.add_violation(InfiniteWhileLoopViolation(node))
+
 
 @final
 @decorators.alias('visit_any_for', (


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
@@ -707,6 +707,7 @@ def consecutive_yields():
 
     'wrong',
 ]
+
 *numbers, = [4, 7]  # noqa: WPS356
 [first_number, second_number] = [4, 7]  # noqa: WPS359
 
@@ -732,3 +733,7 @@ def raise_bad_exception():
 
 if float("NaN") < number:  # noqa: WPS456
     my_print("Greater than... what?")
+
+def infinite_loop():
+    while True:  # noqa: WPS457
+        my_print('forever')
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
@@ -226,6 +226,7 @@
     'WPS454': 1,
     'WPS455': 1,
     'WPS456': 1,
+    'WPS457': 1,
 
     'WPS500': 1,
     'WPS501': 1,
diff --git a/tests/test_visitors/test_ast/test_loops/test_loops/test_infinite_while_loops.py b/tests/test_visitors/test_ast/test_loops/test_loops/test_infinite_while_loops.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_loops/test_loops/test_infinite_while_loops.py
@@ -0,0 +1,206 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from wemake_python_styleguide.violations.best_practices import (
+    InfiniteWhileLoopViolation,
+)
+from wemake_python_styleguide.visitors.ast.loops import WrongLoopVisitor
+
+template_simple = """
+while True:
+    {0}
+"""
+
+template_nested_while1 = """
+while True:
+    while other:
+        {0}
+    {0}
+"""
+
+template_nested_while2 = """
+while other:
+    while True:
+        {0}
+    {0}
+"""
+
+template_nested_if = """
+while True:
+    if some:
+        {0}
+"""
+
+template_function = """
+def wrapper():
+    while True:
+        {0}
+"""
+
+template_other = """
+while other:
+    {0}
+"""
+
+# Double:
+
+template_double_while = """
+while True:
+    while True:
+        {0}
+    {1}
+"""
+
+
+@pytest.mark.parametrize('template', [
+    template_simple,
+    template_nested_while1,
+    template_nested_while2,
+    template_nested_if,
+    template_function,
+    template_other,
+])
+@pytest.mark.parametrize('keyword', [
+    'break',
+    'raise Some',
+    'raise Some()',
+    'raise',
+])
+def test_correct_while_loops(
+    assert_errors,
+    parse_ast_tree,
+    keyword,
+    template,
+    default_options,
+):
+    """Testing while loops with correct code."""
+    tree = parse_ast_tree(template.format(keyword))
+
+    visitor = WrongLoopVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize('template', [
+    template_function,
+])
+@pytest.mark.parametrize('keyword', [
+    'return',
+    'return some',
+])
+def test_correct_while_loops_function(
+    assert_errors,
+    parse_ast_tree,
+    keyword,
+    template,
+    default_options,
+    mode,
+):
+    """Testing while loops with ``return`` statements."""
+    tree = parse_ast_tree(mode(template.format(keyword)))
+
+    visitor = WrongLoopVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize('template', [
+    template_other,
+])
+@pytest.mark.parametrize('keyword', [
+    'print(some)',
+    'attr.method()',
+    'a = 1',
+])
+def test_other_while_loops(
+    assert_errors,
+    parse_ast_tree,
+    keyword,
+    template,
+    default_options,
+):
+    """Testing other while loops with regular code."""
+    tree = parse_ast_tree(template.format(keyword))
+
+    visitor = WrongLoopVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize('template', [
+    template_simple,
+    template_nested_while1,
+    template_nested_while2,
+    template_nested_if,
+    template_function,
+])
+@pytest.mark.parametrize('keyword', [
+    'print(some)',
+    'attr.method()',
+    'a = 1',
+])
+def test_wrong_while_loops(
+    assert_errors,
+    parse_ast_tree,
+    keyword,
+    template,
+    default_options,
+):
+    """Testing while loops with wrong code."""
+    tree = parse_ast_tree(template.format(keyword))
+
+    visitor = WrongLoopVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [InfiniteWhileLoopViolation])
+
+
+@pytest.mark.parametrize('template', [
+    template_double_while,
+])
+@pytest.mark.parametrize('keyword', [
+    'break',
+    'raise ValueError',
+])
+def test_double_while_correct_loops(
+    assert_errors,
+    parse_ast_tree,
+    keyword,
+    template,
+    default_options,
+):
+    """Testing while loops with wrong code."""
+    tree = parse_ast_tree(template.format(keyword, keyword))
+
+    visitor = WrongLoopVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize('template', [
+    template_double_while,
+])
+@pytest.mark.parametrize(('keyword1', 'keyword2'), [
+    ('print()', 'break'),
+    ('break', 'other.attr = 1'),
+])
+def test_double_while_wrong_loops(
+    assert_errors,
+    parse_ast_tree,
+    keyword1,
+    keyword2,
+    template,
+    default_options,
+):
+    """Testing while loops with wrong code."""
+    tree = parse_ast_tree(template.format(keyword1, keyword2))
+
+    visitor = WrongLoopVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [InfiniteWhileLoopViolation])
diff --git a/tests/test_visitors/test_ast/test_loops/test_loops/test_lambda_in_loop.py b/tests/test_visitors/test_ast/test_loops/test_loops/test_lambda_in_loop.py
--- a/tests/test_visitors/test_ast/test_loops/test_loops/test_lambda_in_loop.py
+++ b/tests/test_visitors/test_ast/test_loops/test_loops/test_lambda_in_loop.py
@@ -30,12 +30,14 @@ def wrapper():
 lambda_inside_while_loop = """
 while True:
     print(lambda: index)
+    break
 """
 
 nested_lambda_inside_while_loop = """
 while True:
     if some:
         print(lambda: index)
+    break
 """
 
 lambda_inside_set = """
diff --git a/tests/test_visitors/test_ast/test_loops/test_loops/test_useless_conitue.py b/tests/test_visitors/test_ast/test_loops/test_loops/test_useless_conitue.py
--- a/tests/test_visitors/test_ast/test_loops/test_loops/test_useless_conitue.py
+++ b/tests/test_visitors/test_ast/test_loops/test_loops/test_useless_conitue.py
@@ -24,6 +24,7 @@ def wrapper():
         continue
 
     print(number)
+    break
 """
 
 # Wrong:
@@ -41,23 +42,10 @@ def wrapper():
             continue
 """
 
-wrong_while_loop = """
-while True:
-    continue
-"""
-
-wrong_nested_while_loop = """
-while True:
-    if number == 0:
-        continue
-"""
-
 
 @pytest.mark.parametrize('code', [
     wrong_for_loop,
     wrong_nested_for_loop,
-    wrong_while_loop,
-    wrong_nested_while_loop,
 ])
 def test_wrong_continue_in_loop(
     assert_errors,