Repository: wemake-services/wemake-python-styleguide

Issue:
Detect bare `raise` outside of `except` block
# Rule request

<!-- Thanks for coming up with a proposal! -->

## Thesis

Report `raise` statements outside of `except` block

```python
# bad
def smth():
    raise

# good
def smth():
    try:
        ...
    except:
        raise
```


## Reasoning

It causes runtime error:

```
>>> smth()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in smth
RuntimeError: No active exception to reraise
```

It is possible that the function `smth` is meant to be called only from `except` block and so it won't fail but I'd say it is quite obscure and is a bad practice.


Discussion:
Ok, we used to detect this one, I can surely remember it. But for some reason, it is now gone.
Hi. We are first-time contributors (@bhargavi247) and were wondering if we could take a look at this issue if possible? Do let us know if we can work on it. Thank you!
Sure, @amansr02! Thanks a lot for your help!
Hello @sobolevn ! @amansr02 and I are making good progress! Just a quick question - we added a new violation that is called with an illegal bare raise. Are we supposed to add a new violation or use an existing violation?
Thank you!
Please, add a new one.
@sobolevn I'm taking a look at this issue and was able to add a new violation and visitor, but I have a few clarifying questions.

Is the intent to disallow any calls to `raise` unless it is from an `except` block? What about an `if` block? Something like 
```
if x < 0: 
    raise Exception("Sorry, no numbers below zero")
```

Are there any other blocks that `raise` should be allow to be called from?

Second, I ran my first implementation of the visitor, and there are a few files within the repo that are returning a bare raise violation. For example, it's finding this function as a violation since the raise is being called outside of an except or if clause:

```
def _convert_num(node: Optional[ast.AST]):
    if isinstance(node, Constant):  # pragma: py-lt-38
        if isinstance(node.value, (int, float, complex)):
            return node.value
    elif isinstance(node, ast.Num):  # pragma: py-gte-38
        return node.n
    # That's what is modified from the original
    elif isinstance(node, ast.Name):
        # We return string names as is, see how we return strings:
        return node.id
    raise ValueError('malformed node or string: {0!r}'.format(node))
```

There are 3 files currently where this is happening. How do we want to deal with such cases?

Thanks!
Hi, @danielboghean 

We only need to forbid bare `raise` without any arguments outside of `except` 🙂 
It can be nested in `except`, for example:

```python
except Some:
    if other:
      raise
```
Ah, I understand now, I totally misread the 'bare' except. Thanks for clearing that up, that should help!

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
@@ -83,6 +83,7 @@
    EmptyCommentViolation
    BitwiseAndBooleanMixupViolation
    NewStyledDecoratorViolation
+   BareRaiseViolation
 
 Best practices
 --------------
@@ -154,6 +155,7 @@
 .. autoclass:: EmptyCommentViolation
 .. autoclass:: BitwiseAndBooleanMixupViolation
 .. autoclass:: NewStyledDecoratorViolation
+.. autoclass:: BareRaiseViolation
 
 """
 
@@ -2572,3 +2574,36 @@ def my_function(): ...
 
     error_template = 'Found new-styled decorator'
     code = 466
+
+
+@final
+class BareRaiseViolation(ASTViolation):
+    """
+    Forbid using a bare ``raise`` keyword outside of ``except``.
+
+    Reasoning:
+       Using a bare ``raise`` outside of an ``except`` block
+       causes a runtime error.
+
+    Solution:
+        Only use bare ``raise`` within an ``except`` block.
+
+    Example::
+
+        # Correct:
+        def smth():
+            try:
+                ...
+            except:
+                raise
+
+        # Wrong:
+        def smth():
+            raise
+
+    .. versionadded:: 0.16.0
+
+    """
+
+    error_template = 'Found bare raise keyword'
+    code = 467
diff --git a/wemake_python_styleguide/visitors/ast/keywords.py b/wemake_python_styleguide/visitors/ast/keywords.py
--- a/wemake_python_styleguide/visitors/ast/keywords.py
+++ b/wemake_python_styleguide/visitors/ast/keywords.py
@@ -18,6 +18,7 @@
 )
 from wemake_python_styleguide.types import AnyFunctionDef, AnyNodes, AnyWith
 from wemake_python_styleguide.violations.best_practices import (
+    BareRaiseViolation,
     BaseExceptionRaiseViolation,
     ContextManagerVariableDefinitionViolation,
     RaiseNotImplementedViolation,
@@ -54,6 +55,7 @@ class WrongRaiseVisitor(BaseNodeVisitor):
     def visit_Raise(self, node: ast.Raise) -> None:
         """Checks how ``raise`` keyword is used."""
         self._check_exception_type(node)
+        self._check_bare_raise(node)
         self.generic_visit(node)
 
     def _check_exception_type(self, node: ast.Raise) -> None:
@@ -65,6 +67,13 @@ def _check_exception_type(self, node: ast.Raise) -> None:
                 BaseExceptionRaiseViolation(node, text=exception_name),
             )
 
+    def _check_bare_raise(self, node: ast.Raise) -> None:
+        if node.exc is None:
+            parent_except = walk.get_closest_parent(node, ast.ExceptHandler)
+
+            if not parent_except:
+                self.add_violation(BareRaiseViolation(node))
+
 
 @final
 @alias('visit_any_function', (


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
@@ -788,3 +788,7 @@ def get_item():  # noqa: WPS463
    some(number) for numbers in matrix
    for number in numbers # noqa: WPS361
 ]
+
+
+def bare_raise_function():
+    raise # noqa: WPS467
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
@@ -246,6 +246,7 @@
     'WPS464': 0,  # logically unacceptable.
     'WPS465': 1,
     'WPS466': 0,  # defined in version specific table.
+    'WPS467': 1,
 
     'WPS500': 1,
     'WPS501': 1,
diff --git a/tests/test_visitors/test_ast/test_keywords/test_raise.py b/tests/test_visitors/test_ast/test_keywords/test_raise.py
--- a/tests/test_visitors/test_ast/test_keywords/test_raise.py
+++ b/tests/test_visitors/test_ast/test_keywords/test_raise.py
@@ -1,6 +1,7 @@
 import pytest
 
 from wemake_python_styleguide.violations.best_practices import (
+    BareRaiseViolation,
     BaseExceptionRaiseViolation,
     RaiseNotImplementedViolation,
 )
@@ -112,7 +113,7 @@ def test_raise_good_errors(
     assert_errors(visitor, [])
 
 
-def test_bare_raise(
+def test_bare_raise_wrong_visitor(
     assert_errors,
     parse_ast_tree,
     default_options,
@@ -130,3 +131,58 @@ def test_bare_raise(
     visitor.run()
 
     assert_errors(visitor, [])
+
+
+bare_raise_except_function = """
+def bare_raise_with_except():
+    try:
+        print('test')
+    except:
+        raise
+"""
+
+bare_raise_if_function = """
+def bare_raise_with_if():
+    try:
+        print('test')
+    except:
+        if 1 == 1:
+            raise
+"""
+
+
+def test_bare_raise(
+    assert_errors,
+    parse_ast_tree,
+    default_options,
+):
+    """Testing bare raise without except block."""
+    code = """
+    def bare_raise():
+        raise
+    """
+    tree = parse_ast_tree(code)
+
+    visitor = WrongRaiseVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [BareRaiseViolation])
+
+
+@pytest.mark.parametrize('code', [
+    bare_raise_except_function,
+    bare_raise_if_function,
+])
+def test_bare_raise_except(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+):
+    """Testing that bare `raise` is only allowed in except blocks."""
+    tree = parse_ast_tree(code)
+
+    visitor = WrongRaiseVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])