Repository: wemake-services/wemake-python-styleguide

Issue:
Forbid incorrect assign pattern
# Rule request

<!-- Thanks for coming up with a proposal! -->

## Thesis

<!-- What exactly do you propose? Please, provide code examples. -->

We need to enforce:

```python
a += b
```

instead of:

```python
a = a + b
```

The same applies for all other operators: `-`, `&`, `**`, etc
https://rust-lang.github.io/rust-clippy/master/index.html#assign_op_pattern

## Reasoning

<!--
Why do you think this is a good idea?

Please, include some strong points and considerations.
-->

The first version is correct and short. This is a consistency rule.

<!--

  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:
I would like to work on this
You are assigned!

Resolving patch:
diff --git a/wemake_python_styleguide/presets/types/tree.py b/wemake_python_styleguide/presets/types/tree.py
--- a/wemake_python_styleguide/presets/types/tree.py
+++ b/wemake_python_styleguide/presets/types/tree.py
@@ -30,6 +30,7 @@
     statements.WrongParametersIndentationVisitor,
     statements.PointlessStarredVisitor,
     statements.WrongNamedKeywordVisitor,
+    statements.AssignmentPatternsVisitor,
 
     keywords.WrongRaiseVisitor,
     keywords.WrongKeywordVisitor,
diff --git a/wemake_python_styleguide/violations/consistency.py b/wemake_python_styleguide/violations/consistency.py
--- a/wemake_python_styleguide/violations/consistency.py
+++ b/wemake_python_styleguide/violations/consistency.py
@@ -75,6 +75,7 @@
    VagueImportViolation
    AdditionAssignmentOnListViolation
    RedundantSubscriptViolation
+   AugmentedAssignPatternViolation
 
 Consistency checks
 ------------------
@@ -129,6 +130,7 @@
 .. autoclass:: VagueImportViolation
 .. autoclass:: AdditionAssignmentOnListViolation
 .. autoclass:: RedundantSubscriptViolation
+.. autoclass:: AugmentedAssignPatternViolation
 
 """
 
@@ -1790,6 +1792,7 @@ class VagueImportViolation(ASTViolation):
     code = 347
 
 
+@final
 class AdditionAssignmentOnListViolation(ASTViolation):
     """
     Forbids usage of += with list arguments.
@@ -1838,3 +1841,28 @@ class RedundantSubscriptViolation(ASTViolation):
 
     error_template = 'Found redundant subscript slice: {0}'
     code = 349
+
+
+@final
+class AugmentedAssignPatternViolation(ASTViolation):
+    """
+    Enforce using augmented assign pattern.
+
+    Reasoning:
+        ``a += b`` is short and correct version of ``a = a + b``.
+        Why not using the short version?
+
+    Example::
+
+        # Correct:
+        a += b
+
+        # Wrong:
+        a = a + b
+
+    .. versionadded:: 0.13.0
+
+    """
+
+    error_template = 'Found usable augmented assign pattern'
+    code = 350
diff --git a/wemake_python_styleguide/visitors/ast/statements.py b/wemake_python_styleguide/visitors/ast/statements.py
--- a/wemake_python_styleguide/visitors/ast/statements.py
+++ b/wemake_python_styleguide/visitors/ast/statements.py
@@ -27,6 +27,7 @@
     WrongNamedKeywordViolation,
 )
 from wemake_python_styleguide.violations.consistency import (
+    AugmentedAssignPatternViolation,
     ParametersIndentationViolation,
     UselessNodeViolation,
 )
@@ -427,3 +428,32 @@ def _has_wrong_keys(self, node: ast.keyword) -> bool:
                 if not str.isidentifier(key_node.s):
                     return True
         return False
+
+
+@final
+class AssignmentPatternsVisitor(BaseNodeVisitor):
+    """Responsible for checking assignment patterns."""
+
+    def visit_Assign(self, node: ast.Assign) -> None:
+        """Checks assignment patterns."""
+        self._check_augmented_assign_pattern(node)
+        self.generic_visit(node)
+
+    def _check_augmented_assign_pattern(
+        self,
+        node: ast.Assign,
+    ) -> None:
+        if not isinstance(node.value, ast.BinOp):
+            return
+
+        is_checkable = (
+            len(node.targets) == 1 and
+            isinstance(node.value.right, ast.Name) and
+            isinstance(node.value.left, ast.Name)
+        )
+
+        if not is_checkable:
+            return
+
+        if name_nodes.is_same_variable(node.targets[0], node.value.left):
+            self.add_violation(AugmentedAssignPatternViolation(node))


Test patch:
diff --git a/tests/fixtures/noqa.py b/tests/fixtures/noqa.py
--- a/tests/fixtures/noqa.py
+++ b/tests/fixtures/noqa.py
@@ -591,3 +591,5 @@ def nested_annotation_return_func() -> Union[str, Union[int, float]]: # noqa: WP
 swap_b = swap_a  # noqa: WPS448
 
 print(constant[0:7])  # noqa: WPS349
+
+var_a = var_a + var_b  # noqa: WPS350
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
@@ -123,6 +123,7 @@
     'WPS347': 3,
     'WPS348': 1,
     'WPS349': 1,
+    'WPS350': 1,
 
     'WPS400': 0,
     'WPS401': 0,
diff --git a/tests/test_visitors/test_ast/test_statements/test_augmented_assign.py b/tests/test_visitors/test_ast/test_statements/test_augmented_assign.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_statements/test_augmented_assign.py
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from wemake_python_styleguide.violations.consistency import (
+    AugmentedAssignPatternViolation,
+)
+from wemake_python_styleguide.visitors.ast.statements import (
+    AssignmentPatternsVisitor,
+)
+
+OPERATIONS = frozenset((
+    '+',
+    '-',
+    '*',
+    '/',
+    '%',
+    '**',
+    '&',
+    '|',
+    '^',
+    '>>',
+    '<<',
+))
+
+
+@pytest.mark.parametrize('code', [
+    'a {0}= b',
+])
+@pytest.mark.parametrize('operation', [
+    *OPERATIONS,
+])
+def test_augmented_assign(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    operation,
+    default_options,
+    mode,
+):
+    """Ensures that augemented assignment are not violated."""
+    tree = parse_ast_tree(mode(code.format(operation)))
+
+    visitor = AssignmentPatternsVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize('code', [
+    'a = a {0} b',
+])
+@pytest.mark.parametrize('operation', [
+    *OPERATIONS,
+])
+def test_no_augmented_assign(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    operation,
+    default_options,
+    mode,
+):
+    """Ensures force augmented assignments."""
+    tree = parse_ast_tree(mode(code.format(operation)))
+
+    visitor = AssignmentPatternsVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [AugmentedAssignPatternViolation])
+
+
+@pytest.mark.parametrize('code', [
+    'a = a {0} b + c',
+    'a = b {0} a + c',
+    'a = (a {0} b) - c',
+    'a = b {0} c',
+    'a = b {0} a',
+])
+@pytest.mark.parametrize('operation', [
+    *OPERATIONS,
+])
+def test_no_checkable_assign(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    operation,
+    default_options,
+    mode,
+):
+    """Ensures that complex expressions are not checked."""
+    tree = parse_ast_tree(mode(code.format(operation)))
+
+    visitor = AssignmentPatternsVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])