Repository: wemake-services/wemake-python-styleguide

Issue:
Forbid to use `continue` in `finally`
# Rule request

<!-- Thanks for coming up with a proposal! -->

## Thesis

This code is now allowed:

```python
for some in range(10):
    try:
        func(some)
    finally:
        continue
```

Why would you do that? 
Putting any control statements in `finally` is a terrible practice.
We should not allow it.

It was added in `python3.8`: https://www.python.org/downloads/release/python-381/


Discussion:
Hi, could I take the issue?
Sure! Feel free to ask for help.
Hi! I am trying to run and write tests and have an error: 
```ModuleNotFoundError: No module named 'plugins' ```
I could not find any information about such module in the internet and have no idea how to deal with the problem.
Could you help me, please?
Sure! It looks like a problem with your environment.

Can you please post how exactly you run your tests?
You can also take a look at how we run our tests here: https://github.com/wemake-services/wemake-python-styleguide/blob/master/.travis.yml#L19-L20

I tried to run tests from the project root by ```make test``` and ```pytest```. Each time there was the same error: ```ModuleNotFoundError: No module named 'plugins' ```
Then I changed ```plugins.violation``` to ```test.plugins.violation``` in conftest.py and it helped.

Two questions:
1. How does your installation process looks like? Because it is proven to work on CI and locally
2. What is your OS?
I was going the following way:
```
pyenv update
pyenv local 3.6.7
pip3 install poetry
poetry install
poetry shell
make test
```
OS: Ubuntu 18.04
Looks strange. This should do the trick.

Tow comments, however:
1. Use should use `3.8.0`, because this feature is only possible there
2. You can try to `activate` env instead of using `poetry shell`. Try `source $(poetry env info -p)/bin/activate`

Resolving patch:
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
@@ -71,6 +71,7 @@
    FloatKeyViolation
    ProtectedModuleMemberViolation
    PositionalOnlyArgumentsViolation
+   LoopControlFinallyViolation
 
 Best practices
 --------------
@@ -127,6 +128,7 @@
 .. autoclass:: FloatKeyViolation
 .. autoclass:: ProtectedModuleMemberViolation
 .. autoclass:: PositionalOnlyArgumentsViolation
+.. autoclass:: LoopControlFinallyViolation
 
 """
 
@@ -2020,3 +2022,46 @@ def my_function(first, /, second):
 
     error_template = 'Found positional-only argument'
     code = 451
+
+
+class LoopControlFinallyViolation(ASTViolation):
+    """
+    Forbids to use ``break`` and ``continue`` in ``finally`` case.
+
+    Related to :class:`~TryExceptMultipleReturnPathViolation`.
+
+    Reasoning:
+        Putting any control statements in `finally` is a
+        terrible practice, because `finally` is implicitly
+        called and can cause damage to your logic with
+        its implicitness.
+        We should not allow it.
+
+    Solution:
+        Remove ``break`` and ``continue`` from ``finally`` blocks.
+
+    Example::
+
+        # Correct:
+        try:
+            ...
+        finally:
+            ...
+
+        # Wrong:
+        try:
+            ...
+        finally:
+            break
+
+        try:
+            ...
+        finally:
+            continue
+
+    .. versionadded:: 0.14.0
+
+    """
+
+    error_template = 'Found `break` or `continue` in `finally` block'
+    code = 452
diff --git a/wemake_python_styleguide/visitors/ast/exceptions.py b/wemake_python_styleguide/visitors/ast/exceptions.py
--- a/wemake_python_styleguide/visitors/ast/exceptions.py
+++ b/wemake_python_styleguide/visitors/ast/exceptions.py
@@ -15,6 +15,7 @@
     BaseExceptionViolation,
     DuplicateExceptionViolation,
     IncorrectExceptOrderViolation,
+    LoopControlFinallyViolation,
     TryExceptMultipleReturnPathViolation,
 )
 from wemake_python_styleguide.violations.consistency import (
@@ -69,12 +70,14 @@ def visit_Try(self, node: ast.Try) -> None:
             DuplicateExceptionViolation
             TryExceptMultipleReturnPathViolation
             IncorrectExceptOrderViolation
+            LoopControlFinallyViolation
 
         """
         self._check_if_needs_except(node)
         self._check_duplicate_exceptions(node)
         self._check_return_path(node)
         self._check_exception_order(node)
+        self._check_break_or_continue_in_finally(node)
         self.generic_visit(node)
 
     def _check_if_needs_except(self, node: ast.Try) -> None:
@@ -123,6 +126,15 @@ def _check_exception_order(self, node: ast.Try) -> None:
                 else:
                     seen.add(exception)
 
+    def _check_break_or_continue_in_finally(self, node: ast.Try) -> None:
+        has_wrong_nodes = any(
+            is_contained(line, (ast.Break, ast.Continue))
+            for line in node.finalbody
+        )
+
+        if has_wrong_nodes:
+            self.add_violation(LoopControlFinallyViolation(node))
+
 
 @final
 class NestedTryBlocksVisitor(BaseNodeVisitor):


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
@@ -692,3 +692,14 @@ def consecutive_yields():
     'wrong',
 ]
 *numbers, = [4, 7]  # noqa: WPS356
+
+for element in range(10):
+    try:  # noqa: WPS452
+        my_print(1)
+    except AnyError:
+        my_print('nope')
+    finally:
+        # See:
+        # https://github.com/wemake-services/wemake-python-styleguide/issues/1082
+        break
+    my_print(4)
diff --git a/tests/fixtures/noqa/noqa38.py b/tests/fixtures/noqa/noqa38.py
--- a/tests/fixtures/noqa/noqa38.py
+++ b/tests/fixtures/noqa/noqa38.py
@@ -49,3 +49,15 @@ def wrong_comprehension2():
 
 def positional_only(first, /, second):  # noqa: WPS451
     anti_wps428 = 1
+
+
+for element in range(10):
+    try:  # noqa: WPS452
+        my_print(1)
+    except AnyError:
+        my_print('nope')
+    finally:
+        # See:
+        # https://github.com/wemake-services/wemake-python-styleguide/issues/1082
+        continue
+    my_print(4)
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
@@ -47,6 +47,7 @@
 
     'WPS416': int(not PY38),  # only works for `< python3.8`
     'WPS451': int(PY38),  # only works for `>= python3.8`
+    'WPS452': int(PY38),  # only works for `>= python3.8`
 
     'WPS602': 2,
 })
@@ -217,6 +218,7 @@
     'WPS449': 1,
     'WPS450': 1,
     'WPS451': 0,  # defined in version specific table.
+    'WPS452': 1,  # also defined in version specific table.
 
     'WPS500': 1,
     'WPS501': 1,
diff --git a/tests/test_visitors/test_ast/test_exceptions/test_finally_in_loops.py b/tests/test_visitors/test_ast/test_exceptions/test_finally_in_loops.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_exceptions/test_finally_in_loops.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from wemake_python_styleguide.compat.constants import PY38
+from wemake_python_styleguide.violations.best_practices import (
+    LoopControlFinallyViolation,
+)
+from wemake_python_styleguide.visitors.ast.exceptions import (
+    WrongTryExceptVisitor,
+)
+
+right_try_example_with_for = """
+def function():
+    for element in range(10):
+        try:
+            ...
+        except:
+            ...
+        finally:
+            ...
+"""
+
+right_try_example_with_while = """
+def function():
+    while first_element < second_element:
+        try:
+            ...
+        except:
+            ...
+        finally:
+            ...
+"""
+
+right_example_with_for = """
+def function():
+    def some():
+        for elem in range(10):
+            if elem > 5:
+                {0}
+"""
+
+right_example_with_while = """
+def function():
+    def some():
+        while a < b:
+            if a == 5:
+                {0}
+"""
+
+wrong_try_example = """
+def function():
+    for element in range(10):
+        try:
+            ...
+        except:
+            ...
+        finally:
+            {0}
+"""
+
+wrong_try_example_with_while = """
+def function():
+    while first_element < second_element:
+        try:
+            ...
+        except:
+            ...
+        finally:
+            {0}
+"""
+
+
+@pytest.mark.parametrize('statement', [
+    'break',
+    'continue',
+])
+@pytest.mark.parametrize('code', [
+    right_example_with_for,
+    right_example_with_while,
+    right_try_example_with_for,
+    right_try_example_with_while,
+])
+def test_right_finally(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    statement,
+    default_options,
+    mode,
+):
+    """Testing that regular loops and loops with `try` are allowed."""
+    tree = parse_ast_tree(mode(code.format(statement)))
+
+    visitor = WrongTryExceptVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize('statement', [
+    'break',
+])
+@pytest.mark.parametrize('code', [
+    wrong_try_example,
+    wrong_try_example_with_while,
+])
+def test_finally_with_break(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    statement,
+    default_options,
+    mode,
+):
+    """Testing that `break` keyword is not allowed in `finally`."""
+    tree = parse_ast_tree(mode(code.format(statement)))
+
+    visitor = WrongTryExceptVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [LoopControlFinallyViolation])
+
+
+@pytest.mark.skipif(
+    not PY38,
+    reason='continue in finally works only since python3.8',
+)
+@pytest.mark.parametrize('statement', [
+    'continue',
+])
+@pytest.mark.parametrize('code', [
+    wrong_try_example,
+    wrong_try_example_with_while,
+])
+def test_finally_with_continue(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    statement,
+    default_options,
+    mode,
+):
+    """Testing that `continue` keyword is not allowed in `finally`."""
+    tree = parse_ast_tree(mode(code.format(statement)))
+
+    visitor = WrongTryExceptVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [LoopControlFinallyViolation])
+
+
+@pytest.mark.skipif(
+    PY38,
+    reason='continue in finally does not work till since python3.8',
+)
+@pytest.mark.parametrize('statement', [
+    'continue',
+])
+@pytest.mark.parametrize('code', [
+    wrong_try_example,
+    wrong_try_example_with_while,
+])
+def test_finally_with_continue_and_exception(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    statement,
+    default_options,
+    mode,
+):
+    """Testing using `continue` keyword in python < 3.8 raises an exception."""
+    with pytest.raises(SyntaxError):
+        parse_ast_tree(mode(code.format(statement)))
diff --git a/tests/test_visitors/test_ast/test_exceptions/test_finally_return_path.py b/tests/test_visitors/test_ast/test_exceptions/test_finally_return_path.py
--- a/tests/test_visitors/test_ast/test_exceptions/test_finally_return_path.py
+++ b/tests/test_visitors/test_ast/test_exceptions/test_finally_return_path.py
@@ -16,7 +16,7 @@
 
 right_outside1 = """
 def function():  # we need function to use ``return``
-    for _ in range(10):  # we need ``for`` to use ``break``
+    for _ in range(10):
         try:
             ...
         except:
@@ -363,7 +363,6 @@ def function():
     'return',
     'return None',
     'return 1',
-    'break',
     'raise ValueError',
     'raise ValueError()',
     'raise TypeError(1)',
@@ -408,7 +407,6 @@ def test_wrong_return_in_else_or_finally(
     'return',
     'return None',
     'return 1',
-    'break',
     'raise ValueError',
     'raise ValueError()',
     'raise TypeError(1)',
@@ -455,33 +453,33 @@ def test_correct_return_path_in_try_except(
     ('raise ValueError(1)', '...', '...', 'return 1', '...'),
     ('raise ValueError(1)', '...', '...', 'break', '...'),
 
-    ('...', '...', '...', 'raise ValueError', 'break'),
+    ('...', '...', '...', 'raise ValueError', 'return 0'),
     ('...', '...', '...', 'raise ValueError', 'return None'),
     ('...', '...', '...', 'break', 'return'),
     ('...', '...', '...', 'break', 'raise ValueError'),
-    ('...', '...', '...', 'return', 'break'),
+    ('...', '...', '...', 'return', 'return 1'),
     ('...', '...', '...', 'return', 'raise ValueError()'),
 
     ('break', '...', '...', '...', 'raise ValueError'),
     ('break', '...', '...', '...', 'return'),
-    ('return', '...', '...', '...', 'break'),
+    ('return', '...', '...', '...', 'raise ValueError(1)'),
     ('return 0', '...', '...', '...', 'raise ValueError'),
     ('raise ValueError(1)', '...', '...', '...', 'return 1'),
-    ('raise ValueError(1)', '...', '...', '...', 'break'),
+    ('raise ValueError(1)', '...', '...', '...', 'return'),
 
     ('...', 'break', '...', '...', 'raise ValueError'),
     ('...', 'break', '...', '...', 'return'),
-    ('...', 'return', '...', '...', 'break'),
+    ('...', 'return', '...', '...', 'raise ValueError(1)'),
     ('...', 'return 0', '...', '...', 'raise ValueError'),
     ('...', 'raise ValueError(1)', '...', '...', 'return 1'),
-    ('...', 'raise ValueError(1)', '...', '...', 'break'),
+    ('...', 'raise ValueError(1)', '...', '...', 'return 0'),
 
     ('...', '...', 'break', '...', 'raise ValueError'),
     ('...', '...', 'break', '...', 'return'),
-    ('...', '...', 'return', '...', 'break'),
+    ('...', '...', 'return', '...', 'raise ValueError'),
     ('...', '...', 'return 0', '...', 'raise ValueError'),
     ('...', '...', 'raise ValueError(1)', '...', 'return 1'),
-    ('...', '...', 'raise ValueError(1)', '...', 'break'),
+    ('...', '...', 'raise ValueError(1)', '...', 'return 0'),
 ])
 def test_different_nodes_trigger_violation(
     assert_errors,