Repository: wemake-services/wemake-python-styleguide

Issue:
False positive for WPS504
# Bug report

<!--
Hi, thanks for submitting a bug. We appreciate that.

But, we will need some information about what's wrong to help you.
-->

## What's wrong

This code produces a false positive violation:

```python
def test():
    delta = due - since
    if not delta.days:
        raise ValidationError(FormatErrorMessages.empty_duration)
    elif delta.days > max_days:
        raise ValidationError(FormatErrorMessages.too_long_duration)
    return due
```

Output:

```
361   51:5     WPS504 Found negated condition
362   if not delta.days:
363   ^
```

## How is that should be

We don't have an `else` clause here. I cannot switch this condition.
So, when `else` is missing, we should ignore this violation.

<!-- Describe how it should work. -->

<!--

  Love wemake-python-styleguide? ❤️
  Do you want your bugs to be solved faster?

  Please consider supporting our collective:
👉  https://opencollective.com/wemake-python-styleguide/donate

-->

## System information

This actually happens on `0.12.5`

```
» flake8 --version
3.7.9 (flake8-annotations-complexity: 0.0.2, flake8-bandit: 2.1.2, flake8-broken-line: 0.1.1, flake8-bugbear: 19.8.0, flake8-comprehensions: <cached_property.cached_property object at 0x103968438>, flake8-debugger: 3.2.1, flake8-django: 0.0.3, flake8-docstrings: 1.5.0, pydocstyle: 4.0.1, flake8-eradicate: 0.2.3, flake8-executable: 2.0.3, flake8-print: 3.1.4, flake8-pytest: 1.3, flake8-string-format: 0.2.3, flake8_builtins: 1.4.1, flake8_coding: 1.3.2, flake8_commas: 2.0.0, flake8_isort: 2.3, flake8_pep3101: 1.2.1, flake8_quotes: 2.1.1, logging-format: 0.6.0, mccabe: 0.6.1, naming: 0.8.2, pycodestyle: 2.5.0, pyflakes: 2.1.1, radon: 2.4.0, rst-docstrings: 0.0.11, wemake-python-styleguide: 0.12.5) CPython 3.7.3 on Darwin
```



Discussion:

Resolving patch:
diff --git a/wemake_python_styleguide/logic/ifs.py b/wemake_python_styleguide/logic/ifs.py
--- a/wemake_python_styleguide/logic/ifs.py
+++ b/wemake_python_styleguide/logic/ifs.py
@@ -13,6 +13,12 @@ def has_elif(node: ast.If) -> bool:
     return getattr(node, 'wps_if_chain', False)  # noqa: WPS425
 
 
+def has_else(node: ast.If) -> bool:
+    """Tells if this node or ``if`` chain ends with an ``else`` expression."""
+    last_elem = tuple(chain(node))[-1]
+    return bool(last_elem)
+
+
 def root_if(node: ast.If) -> Optional[ast.If]:
     """Returns the previous ``if`` node in the chain if it exists."""
     return getattr(node, 'wps_if_chained', None)
diff --git a/wemake_python_styleguide/violations/refactoring.py b/wemake_python_styleguide/violations/refactoring.py
--- a/wemake_python_styleguide/violations/refactoring.py
+++ b/wemake_python_styleguide/violations/refactoring.py
@@ -276,6 +276,11 @@ class NegatedConditionsViolation(ASTViolation):
         if not some:
              ...
 
+        if not some:
+            ...
+        elif other:
+            ...
+
         # Wrong:
         if not some:
              ...
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
@@ -103,7 +103,7 @@ def visit_IfExp(self, node: ast.IfExp) -> None:
         self.generic_visit(node)
 
     def _check_negated_conditions(self, node: ast.If) -> None:
-        if not node.orelse:
+        if not ifs.has_else(node):
             return
 
         if isinstance(node.test, ast.UnaryOp):


Test patch:
diff --git a/tests/test_visitors/test_ast/test_conditions/test_negated_conditions.py b/tests/test_visitors/test_ast/test_conditions/test_negated_conditions.py
--- a/tests/test_visitors/test_ast/test_conditions/test_negated_conditions.py
+++ b/tests/test_visitors/test_ast/test_conditions/test_negated_conditions.py
@@ -12,6 +12,13 @@
     ...
 """
 
+complex_elif_conditions = """
+if {0}:
+    ...
+elif ...:
+    ...
+"""
+
 complex_conditions = """
 if {0}:
     ...
@@ -19,6 +26,15 @@
     ...
 """
 
+complex_elif_else_conditions = """
+if {0}:
+    ...
+elif ...:
+    ...
+else:
+    ...
+"""
+
 
 @pytest.mark.parametrize('code', [
     'not some',
@@ -44,6 +60,34 @@ def test_negated_simple_conditions(
     assert_errors(visitor, [])
 
 
+@pytest.mark.parametrize('code', [
+    'not some',
+    '-some',
+    'some != 1',
+    'some',
+    'some == 0',
+    'some != other',
+    'some > 1',
+])
+def test_negated_complex_elif_conditions(
+    code,
+    assert_errors,
+    parse_ast_tree,
+    default_options,
+):
+    """Testing complex conditions without else expression."""
+    tree = parse_ast_tree(complex_elif_conditions.format(code))
+
+    visitor = IfStatementVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize('template', [
+    complex_conditions,
+    complex_elif_else_conditions,
+])
 @pytest.mark.parametrize('code', [
     'not some',
     'some != 1',
@@ -51,12 +95,13 @@ def test_negated_simple_conditions(
 ])
 def test_wrong_negated_complex_conditions(
     code,
+    template,
     assert_errors,
     parse_ast_tree,
     default_options,
 ):
     """Testing complex conditions with nagated ``if`` condition."""
-    tree = parse_ast_tree(complex_conditions.format(code))
+    tree = parse_ast_tree(template.format(code))
 
     visitor = IfStatementVisitor(default_options, tree=tree)
     visitor.run()
@@ -64,6 +109,10 @@ def test_wrong_negated_complex_conditions(
     assert_errors(visitor, [NegatedConditionsViolation])
 
 
+@pytest.mark.parametrize('template', [
+    complex_conditions,
+    complex_elif_else_conditions,
+])
 @pytest.mark.parametrize('code', [
     'some',
     '-some',
@@ -74,12 +123,13 @@ def test_wrong_negated_complex_conditions(
 ])
 def test_correctly_negated_complex_conditions(
     code,
+    template,
     assert_errors,
     parse_ast_tree,
     default_options,
 ):
     """Testing correctly negated complex conditions."""
-    tree = parse_ast_tree(complex_conditions.format(code))
+    tree = parse_ast_tree(template.format(code))
 
     visitor = IfStatementVisitor(default_options, tree=tree)
     visitor.run()