Repository: wemake-services/wemake-python-styleguide

Issue:
Check that `self.generic_visit(node)` is on the last line
# Rule request

<!-- Thanks for coming up with a proposal! -->

## Thesis
We should check that `self.generic_visit(node)` is always on the last line of the method.

This should not be allowed:
```
def visit_JoinedStr(self, node: ast.JoinedStr) -> None:
        """Doc."""
        self.generic_visit(node)
        self._check_complex_formatted_string(node)
```

## Reasoning
Opening this issue as per request from @sobolevn.



Discussion:
Yes, thank you! We can probably go for https://github.com/hchasestevens/bellybutton check.
This is only required for `ast`-based checks. Do you want to take this over? 
Can I take this over?
Thanks! 😊 
XPath expression to find all of the occurences for this violation:
```xpath
//ClassDef[contains(bases, Name[@id="BaseNodeVisitor"])]/body/FunctionDef[re:match("visit_.*", @name) 
and not(child::body/Expr[last()]/value/Call/func/Attribute[@attr="generic_visit"])]
```

The problem is that `bellybutton` depends on `astpath==0.6.1`, which does not support the features needed for that expression (namely, `re:match`). To run that expression you need `astpath>=0.9.0` at least.

Three ways to proceed further:
1) Add a dependency on `astpath==0.9.0` and build a simple script around it.
2) Utilise LibCST that is already included and build a script with visitor around it.
3) Rewrite XPath without RE by copy-pasting it and creating separate `bellybutton` rules for each `visit_*` method.

What do you think is the best way to do this?
We need this only for dev, so we can add any dependencies that are required.
I personally like `astpath` and `libcst` solutions.

Resolving patch:
diff --git a/scripts/check_generic_visit.py b/scripts/check_generic_visit.py
new file mode 100644
--- /dev/null
+++ b/scripts/check_generic_visit.py
@@ -0,0 +1,46 @@
+import sys
+
+import astpath
+from pygments import highlight
+from pygments.formatters.terminal256 import Terminal256Formatter
+from pygments.lexers.python import PythonLexer
+
+FAIL_CODE = 255
+
+OK_CODE = 0
+
+PATTERN = '''
+//ClassDef[contains(bases, Name[@id='BaseNodeVisitor'])]/body
+/FunctionDef[re:match('visit_.*', @name)
+and not(child::body/Expr[last()]/value/Call/func
+/Attribute[@attr='generic_visit'])]
+'''  # noqa: Q001
+
+# This is needed to stop linter from spewing WPS421 errors.
+my_print = print
+
+
+if __name__ == '__main__':
+    if len(sys.argv) == 1:
+        my_print('Please provide path to search in!')
+
+    matches = astpath.search(sys.argv[1], PATTERN, print_matches=False)
+
+    if not len(matches):
+        exit(OK_CODE)  # noqa: WPS421
+
+    my_print()
+    my_print('"self.generic_visit(node)" should be last statement here:')
+
+    for fn, line in matches:
+        with open(fn, 'r') as fp:
+            source = fp.read()
+            lines = source.splitlines()
+            highlighted = highlight(
+                lines[line - 1],
+                PythonLexer(),
+                Terminal256Formatter(),
+            )
+            my_print('\t{0}:{1}\n\t{2}'.format(fn, line, highlighted))
+
+    exit(FAIL_CODE)  # noqa: WPS421
diff --git a/wemake_python_styleguide/visitors/ast/complexity/counts.py b/wemake_python_styleguide/visitors/ast/complexity/counts.py
--- a/wemake_python_styleguide/visitors/ast/complexity/counts.py
+++ b/wemake_python_styleguide/visitors/ast/complexity/counts.py
@@ -276,3 +276,5 @@ def visit_Assign(self, node: ast.Assign) -> None:
                         baseline=self.options.max_tuple_unpack_length,
                     ),
                 )
+
+        self.generic_visit(node)
diff --git a/wemake_python_styleguide/visitors/ast/complexity/imports.py b/wemake_python_styleguide/visitors/ast/complexity/imports.py
--- a/wemake_python_styleguide/visitors/ast/complexity/imports.py
+++ b/wemake_python_styleguide/visitors/ast/complexity/imports.py
@@ -59,6 +59,7 @@ def visit_Import(self, node: ast.Import) -> None:
 
         """
         self._visit_any_import(node)
+        self.generic_visit(node)
 
     def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
         """
@@ -72,6 +73,7 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
         """
         self._import_from_members_validator.validate(node)
         self._visit_any_import(node)
+        self.generic_visit(node)
 
     def _visit_any_import(self, node: AnyImport) -> None:
         self._imports_count += 1


Test patch: