Repository: wemake-services/wemake-python-styleguide

Issue:
WPS000 on several code fragments
### What's wrong

If I run WPS on such code fragments, then the error WPS000 appears, although the code is correct.

**Fragment №1:**
```python
number = int(input())
if number == 1:
    print("1")
else:
    if number == 2: print("2")
```

**Output №1:**
```
Traceback (most recent call last):
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/checker.py", line 154, in run
    visitor.run()
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/visitors/base.py", line 285, in run
    self.visit(token)
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/visitors/base.py", line 279, in visit
    method(token)
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/visitors/tokenize/conditions.py", line 56, in visit_name
    self._check_implicit_elif(token)
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/visitors/tokenize/conditions.py", line 135, in _check_implicit_elif
    self._check_complex_else(next_tokens, next_token, index)
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/visitors/tokenize/conditions.py", line 110, in _check_complex_else
    complex_else = self._if_has_code_below(tokens[index + 1:])
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/visitors/tokenize/conditions.py", line 89, in _if_has_code_below
    while remaining_tokens[index - 1].exact_type != tokenize.INDENT:
IndexError: list index out of range

../df_test.py:0:1: WPS000 Internal error happened, see log. Please, take some time to report it
../df_test.py:1:1: D100 Missing docstring in public module
../df_test.py:1:14: WPS421 Found wrong function call: input
../df_test.py:3:5: WPS421 Found wrong function call: print
../df_test.py:3:11: Q000 Remove bad quotes
../df_test.py:5:19: E701 multiple statements on one line (colon)
../df_test.py:5:21: WPS421 Found wrong function call: print
../df_test.py:5:27: Q000 Remove bad quotes
```

**Fragment №2:**
```python
def is_prime(number: int):
    for i in range(2, number):
        if number % i == 0:
            return False
    else:
        if number != 1:
            return True
    return False
```

**Output №2:**
```
Traceback (most recent call last):
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/checker.py", line 154, in run
    visitor.run()
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/visitors/base.py", line 191, in run
    self.visit(self.tree)
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/visitors/base.py", line 186, in visit
    return route_visit(self, tree)
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/compat/routing.py", line 36, in route_visit
    return getattr(
  File "/usr/lib/python3.8/ast.py", line 379, in generic_visit
    self.visit(item)
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/visitors/base.py", line 186, in visit
    return route_visit(self, tree)
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/compat/routing.py", line 36, in route_visit
    return getattr(
  File "/usr/lib/python3.8/ast.py", line 379, in generic_visit
    self.visit(item)
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/visitors/base.py", line 186, in visit
    return route_visit(self, tree)
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/compat/routing.py", line 36, in route_visit
    return getattr(
  File "/usr/lib/python3.8/ast.py", line 379, in generic_visit
    self.visit(item)
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/visitors/base.py", line 186, in visit
    return route_visit(self, tree)
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/compat/routing.py", line 36, in route_visit
    return getattr(
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/visitors/ast/conditions.py", line 77, in visit_any_if
    self._check_simplifiable_returning_if(node)
  File "/home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/wemake_python_styleguide/visitors/ast/conditions.py", line 115, in _check_simplifiable_returning_if
    next_index_in_parent = body.index(node) + 1
ValueError: <_ast.If object at 0x7fa0e1305f40> is not in list

../df_test.py:0:1: WPS000 Internal error happened, see log. Please, take some time to report it
../df_test.py:1:1: D100 Missing docstring in public module
../df_test.py:1:1: D103 Missing docstring in public function
../df_test.py:2:5: WPS500 Found `else` in a loop without `break`
../df_test.py:2:5: WPS503 Found useless returning `else` statement
../df_test.py:2:9: WPS111 Found too short name: i < 2
```

I also checked this problem occurs on version 0.15.3 and in wps-light-0.15.2.

### How it should be

I expect there should be no WPS000 error on these fragments. :slightly_smiling_face:

### Flake8 version and plugins

```json
{
  "dependencies": [],
  "platform": {
    "python_implementation": "CPython",
    "python_version": "3.8.10",
    "system": "Linux"
  },
  "plugins": [
    {
      "is_local": false,
      "plugin": "flake8-bandit",
      "version": "2.1.2"
    },
    {
      "is_local": false,
      "plugin": "flake8-broken-line",
      "version": "0.3.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-bugbear",
      "version": "20.11.1"
    },
    {
      "is_local": false,
      "plugin": "flake8-comprehensions",
      "version": "3.5.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-darglint",
      "version": "1.8.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-debugger",
      "version": "4.0.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-docstrings",
      "version": "1.6.0, pydocstyle: 6.1.1"
    },
    {
      "is_local": false,
      "plugin": "flake8-eradicate",
      "version": "1.1.0"
    },
    {
      "is_local": false,
      "plugin": "flake8-string-format",
      "version": "0.3.0"
    },
    {
      "is_local": false,
      "plugin": "flake8_commas",
      "version": "2.0.0"
    },
    {
      "is_local": false,
      "plugin": "flake8_isort",
      "version": "4.0.0"
    },
    {
      "is_local": false,
      "plugin": "flake8_quotes",
      "version": "3.2.0"
    },
    {
      "is_local": false,
      "plugin": "mccabe",
      "version": "0.6.1"
    },
    {
      "is_local": false,
      "plugin": "naming",
      "version": "0.11.1"
    },
    {
      "is_local": false,
      "plugin": "pycodestyle",
      "version": "2.7.0"
    },
    {
      "is_local": false,
      "plugin": "pyflakes",
      "version": "2.3.1"
    },
    {
      "is_local": false,
      "plugin": "rst-docstrings",
      "version": "0.0.14"
    },
    {
      "is_local": false,
      "plugin": "wemake_python_styleguide",
      "version": "0.15.2"
    }
  ],
  "version": "3.9.2"
}
```

### pip information

```
pip 20.0.2 from /home/ilya/Desktop/venv-wps-15.2/lib/python3.8/site-packages/pip (python 3.8)
astor==0.8.1
attrs==21.2.0
bandit==1.7.0
darglint==1.8.0
docutils==0.17.1
eradicate==2.0.0
flake8==3.9.2
flake8-bandit==2.1.2
flake8-broken-line==0.3.0
flake8-bugbear==20.11.1
flake8-commas==2.0.0
flake8-comprehensions==3.5.0
flake8-debugger==4.0.0
flake8-docstrings==1.6.0
flake8-eradicate==1.1.0
flake8-isort==4.0.0
flake8-polyfill==1.0.2
flake8-quotes==3.2.0
flake8-rst-docstrings==0.0.14
flake8-string-format==0.3.0
gitdb==4.0.7
GitPython==3.1.18
isort==5.9.3
mccabe==0.6.1
pbr==5.6.0
pep8-naming==0.11.1
pycodestyle==2.7.0
pydocstyle==6.1.1
pyflakes==2.3.1
Pygments==2.9.0
PyYAML==5.4.1
restructuredtext-lint==1.3.2
six==1.16.0
smmap==4.0.0
snowballstemmer==2.1.0
stevedore==3.3.0
testfixtures==6.18.0
typing-extensions==3.10.0.0
wemake-python-styleguide==0.15.2
```

### OS information

```
NAME="Ubuntu"
VERSION="20.04.2 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.2 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal
```


Discussion:
Hi! I'm new here, but I would like to take a look at this issue. Would it be okay if I try to solve it?
@danielboghean sure! You can start with the very first code sample: copy it to some file, run `flake8` on it.
It will reproduce the bug. Then, solve it and write a regression test case. Done! 

Resolving patch:
diff --git a/wemake_python_styleguide/logic/tree/keywords.py b/wemake_python_styleguide/logic/tree/keywords.py
--- a/wemake_python_styleguide/logic/tree/keywords.py
+++ b/wemake_python_styleguide/logic/tree/keywords.py
@@ -20,25 +20,24 @@ def returning_nodes(
     return returns, has_values
 
 
-def node_returns_bool_const(node: ast.stmt) -> bool:
-    """Checks if a Return node would return a boolean constant."""
-    is_return = isinstance(node, ast.Return)
-    if is_return:
-        return_value = getattr(node, 'value', None)
-        if isinstance(return_value, ast.NameConstant):
-            return isinstance(return_value.value, bool)
-    return False
-
-
 def is_simple_return(body: Sequence[ast.stmt]) -> bool:
     """Check if a statement only returns a boolean constant."""
     if len(body) != 1:
         return False
-    return node_returns_bool_const(body[0])
+    return _node_returns_bool_const(body[0])
 
 
 def next_node_returns_bool(body: Sequence[ast.stmt], index: int) -> bool:
     """Check if the node after exiting the context returns a boolean const."""
     if len(body) < index + 1:
         return False
-    return node_returns_bool_const(body[index])
+    return _node_returns_bool_const(body[index])
+
+
+def _node_returns_bool_const(node: ast.stmt) -> bool:
+    """Checks if a Return node would return a boolean constant."""
+    return (
+        isinstance(node, ast.Return) and
+        isinstance(node.value, ast.NameConstant) and
+        isinstance(node.value.value, bool)
+    )
diff --git a/wemake_python_styleguide/visitors/ast/conditions.py b/wemake_python_styleguide/visitors/ast/conditions.py
--- a/wemake_python_styleguide/visitors/ast/conditions.py
+++ b/wemake_python_styleguide/visitors/ast/conditions.py
@@ -3,9 +3,11 @@
 from functools import reduce
 from typing import ClassVar, DefaultDict, List, Mapping, Set, Type
 
-from typing_extensions import final
+from typing_extensions import Final, final
 
+from wemake_python_styleguide.compat.aliases import ForNodes
 from wemake_python_styleguide.logic import source, walk
+from wemake_python_styleguide.logic.nodes import get_parent
 from wemake_python_styleguide.logic.tree import ifs, keywords, operators
 from wemake_python_styleguide.logic.tree.compares import CompareBounds
 from wemake_python_styleguide.logic.tree.functions import given_function_called
@@ -29,6 +31,7 @@
 from wemake_python_styleguide.visitors.decorators import alias
 
 _OperatorPairs = Mapping[Type[ast.boolop], Type[ast.cmpop]]
+_ELSE_NODES: Final = (*ForNodes, ast.While, ast.Try)
 
 
 # TODO: move to logic
@@ -68,7 +71,7 @@ def _get_duplicate_names(variables: List[Set[str]]) -> Set[str]:
 class IfStatementVisitor(BaseNodeVisitor):
     """Checks single and consecutive ``if`` statement nodes."""
 
-    def visit_any_if(self, node: ast.If) -> None:
+    def visit_any_if(self, node: AnyIf) -> None:
         """Checks ``if`` nodes and expressions."""
         self._check_negated_conditions(node)
         self._check_useless_len(node)
@@ -88,6 +91,11 @@ def _check_negated_conditions(self, node: AnyIf) -> None:
             if any(isinstance(elem, ast.NotEq) for elem in node.test.ops):
                 self.add_violation(NegatedConditionsViolation(node))
 
+    def _check_useless_len(self, node: AnyIf) -> None:
+        if isinstance(node.test, ast.Call):
+            if given_function_called(node.test, {'len'}):
+                self.add_violation(UselessLenCompareViolation(node))
+
     def _check_multiline_conditions(self, node: ast.If) -> None:
         """Checks multiline conditions ``if`` statement nodes."""
         start_lineno = getattr(node, 'lineno', None)
@@ -97,11 +105,6 @@ def _check_multiline_conditions(self, node: ast.If) -> None:
                 self.add_violation(MultilineConditionsViolation(node))
                 break
 
-    def _check_useless_len(self, node: AnyIf) -> None:
-        if isinstance(node.test, ast.Call):
-            if given_function_called(node.test, {'len'}):
-                self.add_violation(UselessLenCompareViolation(node))
-
     def _check_simplifiable_returning_if(self, node: ast.If) -> None:
         body = node.body
         simple_if_and_root = not (ifs.has_elif(node) or ifs.is_elif(node))
@@ -111,10 +114,19 @@ def _check_simplifiable_returning_if(self, node: ast.If) -> None:
                 if keywords.is_simple_return(else_body):
                     self.add_violation(SimplifiableReturningIfViolation(node))
                 return
-            body = getattr(node, 'wps_parent').body  # TODO: refactor
-            next_index_in_parent = body.index(node) + 1
-            if keywords.next_node_returns_bool(body, next_index_in_parent):
-                self.add_violation(SimplifiableReturningIfViolation(node))
+
+            self._check_simplifiable_returning_parent(node)
+
+    def _check_simplifiable_returning_parent(self, node: ast.If) -> None:
+        parent = get_parent(node)
+        if isinstance(parent, _ELSE_NODES):
+            body = parent.body + parent.orelse
+        else:
+            body = getattr(parent, 'body', [node])
+
+        next_index_in_parent = body.index(node) + 1
+        if keywords.next_node_returns_bool(body, next_index_in_parent):
+            self.add_violation(SimplifiableReturningIfViolation(node))
 
 
 @final


Test patch:
diff --git a/tests/test_visitors/test_ast/test_loops/test_loops/test_for_else.py b/tests/test_visitors/test_ast/test_loops/test_loops/test_for_else.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_loops/test_loops/test_for_else.py
@@ -0,0 +1,58 @@
+import pytest
+
+from wemake_python_styleguide.visitors.ast.conditions import IfStatementVisitor
+
+code_that_breaks = """
+def is_prime(number: int):
+    for i in range(2, number):
+        if number % i == 0:
+            return False
+    else:
+        if number != 1:
+            return True
+    return False
+"""
+
+code_that_works = """
+def is_prime(number: int):
+    for i in range(2, number):
+        if number % i == 0:
+            return False
+"""
+
+code_with_complex_if = """
+def _has_same_args(
+    node,
+    call,
+):
+    for num in node:
+        if num:
+            if isinstance(num, int):
+                return False
+        elif isinstance(num, str):
+            if num > 2:
+                return False
+        else:
+            return False
+    return True
+"""
+
+
+@pytest.mark.parametrize('code', [
+    code_that_breaks,
+    code_that_works,
+    code_with_complex_if,
+])
+def test_regression_for_else(
+    assert_errors,
+    default_options,
+    parse_ast_tree,
+    code,
+):
+    """Tests for nested ``if`` statement in ``for else`` statement."""
+    tree = parse_ast_tree(code)
+
+    visitor = IfStatementVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])