Repository: wemake-services/wemake-python-styleguide

Issue:
[RFE] Include actual WPS code in docs
New to wemake here, great stuff.

I noticed that if I google WPS326 no good results are found, I suspect this is because in the docs, only '326' can be found.

I don't know if it's possible, but including a WPS326 somewhere might make it easier to go directly from the error I'm seeing to the docs where it is described if the WPSXXX code got picked up by a search engine.

It could go inline or even in the table up top.

Just a thought & suggestion.

Thanks for all this btw, my code is better now.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0cd64aea..9230e3e0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,15 @@ Semantic versioning in our case means:
   But, in the future we might change the configuration names/logic,
   change the client facing API, change code conventions significantly, etc.
 
+## 0.17.1
+
+### Features
+- Adds `__init_subclass__` in the beginning of accepted methods order as per WPS338
+
+## 0.16.2
+
+### Misc
+- Adds full violation codes to docs and `BaseViolation.full_code` #2409 
 
 ## 0.16.1
 
diff --git a/wemake_python_styleguide/violations/base.py b/wemake_python_styleguide/violations/base.py
index b64fb3d5..358d2233 100644
--- a/wemake_python_styleguide/violations/base.py
+++ b/wemake_python_styleguide/violations/base.py
@@ -76,7 +76,8 @@ class ViolationPostfixes(enum.Enum):
     less_than = ' < {0}'
 
 
-class BaseViolation(object, metaclass=abc.ABCMeta):
+# TODO: remove `noqa` after a new release:
+class BaseViolation(object, metaclass=abc.ABCMeta):  # noqa: WPS338
     """
     Abstract base class for all style violations.
 
@@ -99,11 +100,34 @@ class BaseViolation(object, metaclass=abc.ABCMeta):
     previous_codes: ClassVar[Set[int]]
     deprecated: ClassVar[bool] = False
 
+    # assigned in __init_subclass__
+    full_code: ClassVar[str]
+    summary: ClassVar[str]
+
     # We use this code to show base metrics and thresholds mostly:
     postfix_template: ClassVar[ViolationPostfixes] = (
         ViolationPostfixes.bigger_than
     )
 
+    def __init_subclass__(cls, **kwargs) -> None:
+        """Sets additional values for subclasses."""
+        super().__init_subclass__(**kwargs)
+        violation_code = getattr(cls, 'code', None)
+        if violation_code is None:
+            return
+        if cls.__doc__ is None:
+            raise TypeError(
+                'Please include a docstring documenting {0}'.format(cls),
+            )
+        # this is mostly done for docs to display the full code,
+        # allowing its indexing in search engines and better discoverability
+        cls.full_code = cls._full_code()
+        cls.summary = cls.__doc__.lstrip().split('\n', maxsplit=1)[0]
+        # this hack adds full code to summary table in the docs
+        cls.__doc__ = _prepend_skipping_whitespaces(
+            '{0} — '.format(cls.full_code), cls.__doc__,
+        )
+
     def __init__(
         self,
         node: ErrorNode,
@@ -131,7 +155,7 @@ class BaseViolation(object, metaclass=abc.ABCMeta):
         Conditionally formats the ``error_template`` if it is required.
         """
         return '{0} {1}{2}'.format(
-            self._full_code(),
+            self.full_code,
             self.error_template.format(self._text),
             self._postfix_information(),
         )
@@ -142,14 +166,15 @@ class BaseViolation(object, metaclass=abc.ABCMeta):
         return (*self._location(), self.message())
 
     @final
-    def _full_code(self) -> str:
+    @classmethod
+    def _full_code(cls) -> str:
         """
         Returns fully formatted code.
 
         Adds violation letter to the numbers.
         Also ensures that codes like ``3`` will be represented as ``WPS003``.
         """
-        return 'WPS{0}'.format(str(self.code).zfill(3))
+        return 'WPS{0}'.format(str(cls.code).zfill(3))
 
     @final
     def _postfix_information(self) -> str:
@@ -236,3 +261,9 @@ class SimpleViolation(BaseViolation, metaclass=abc.ABCMeta):
         Cannot be ignored by inline ``noqa`` comments.
         """
         return 0, 0
+
+
+def _prepend_skipping_whitespaces(prefix: str, text: str) -> str:
+    lstripped_text = text.lstrip()
+    leading_whitespaces = text[:len(text) - len(lstripped_text)]
+    return leading_whitespaces + prefix + lstripped_text
diff --git a/wemake_python_styleguide/violations/consistency.py b/wemake_python_styleguide/violations/consistency.py
index 594bdd67..78020975 100644
--- a/wemake_python_styleguide/violations/consistency.py
+++ b/wemake_python_styleguide/violations/consistency.py
@@ -1564,6 +1564,7 @@ class WrongMethodOrderViolation(ASTViolation):
 
     We follow the same ordering:
 
+    - ``__init_subclass__``
     - ``__new__``
     - ``__init__``
     - ``__call__``
diff --git a/wemake_python_styleguide/visitors/ast/classes.py b/wemake_python_styleguide/visitors/ast/classes.py
index 1a08021b..4ff53cab 100644
--- a/wemake_python_styleguide/visitors/ast/classes.py
+++ b/wemake_python_styleguide/visitors/ast/classes.py
@@ -395,7 +395,8 @@ class ClassMethodOrderVisitor(base.BaseNodeVisitor):
 
     def _ideal_order(self, first: str) -> int:
         base_methods_order = {
-            '__new__': 6,  # highest priority
+            '__init_subclass__': 7,  # highest priority
+            '__new__': 6,
             '__init__': 5,
             '__call__': 4,
             '__await__': 3,


Test patch:
diff --git a/tests/test_violations/test_docs.py b/tests/test_violations/test_docs.py
index 96ec874e..e25358d8 100644
--- a/tests/test_violations/test_docs.py
+++ b/tests/test_violations/test_docs.py
@@ -60,3 +60,9 @@ def test_configuration(all_violations):
 
     for option_item, is_listed in option_listed.items():
         assert is_listed, option_item
+
+
+def test_all_violations_doc_start_with_full_code(all_violations):
+    """Ensures that all violations have `versionadded` tag."""
+    for violation in all_violations:
+        assert violation.__doc__.lstrip().startswith(violation.full_code)
diff --git a/tests/test_violations/test_implementation.py b/tests/test_violations/test_implementation.py
index b32dc06e..e43b00bf 100644
--- a/tests/test_violations/test_implementation.py
+++ b/tests/test_violations/test_implementation.py
@@ -1,11 +1,41 @@
 import ast
+import re
+
+import pytest
 
 from wemake_python_styleguide.violations.base import ASTViolation
 
 
+class NewViolation(ASTViolation):
+    """
+    Yells at cloud.
+
+    Yay, I'm a docstring!
+    """
+
+    code = 1
+    error_template = '{0}'
+
+
 def test_visitor_returns_location():
     """Ensures that `BaseNodeVisitor` return correct violation message."""
-    visitor = ASTViolation(node=ast.parse(''), text='violation')
-    visitor.error_template = '{0}'
-    visitor.code = 1
+    assert NewViolation.full_code == 'WPS001'
+    assert NewViolation.summary == 'Yells at cloud.'
+
+    visitor = NewViolation(node=ast.parse(''), text='violation')
     assert visitor.node_items() == (0, 0, 'WPS001 violation')
+
+
+def test_violation_must_have_docstring():
+    """Ensures that `BaseNodeVisitor` return correct violation message."""
+    with pytest.raises(
+        TypeError,
+        match=re.escape(
+            'Please include a docstring documenting ' +
+            "<class 'test_implementation.test_violation_must_have_docstring." +
+            "<locals>.IShallNotPass'>",
+        ),
+    ):
+
+        class IShallNotPass(ASTViolation):  # noqa: WPS431
+            code = 123
diff --git a/tests/test_visitors/test_ast/test_classes/test_method_order.py b/tests/test_visitors/test_ast/test_classes/test_method_order.py
index d2303ad9..7969a2bb 100644
--- a/tests/test_visitors/test_ast/test_classes/test_method_order.py
+++ b/tests/test_visitors/test_ast/test_classes/test_method_order.py
@@ -9,6 +9,9 @@ from wemake_python_styleguide.visitors.ast.classes import (
 
 correct_method_order = """
 class Test(object):
+    def __init_subclass__(cls):
+        ...
+
     def __new__(self):
         ...
 
@@ -82,21 +85,25 @@ def test_correct_method_order(
 
 
 @pytest.mark.parametrize(('first', 'second'), [
+    ('__new__', '__init_subclass__'),
     ('__init__', '__new__'),
     ('__call__', '__init__'),
     ('__call__', '__new__'),
     ('__await__', '__call__'),
 
+    ('public', '__init_subclass__'),
     ('public', '__new__'),
     ('public', '__init__'),
     ('public', '__call__'),
     ('public', '__await__'),
 
+    ('__magic__', '__init_subclass__'),
     ('__magic__', '__new__'),
     ('__magic__', '__init__'),
     ('__magic__', '__call__'),
     ('__magic__', '__await__'),
 
+    ('_protected', '__init_subclass__'),
     ('_protected', '__new__'),
     ('_protected', '__init__'),
     ('_protected', '__call__'),
@@ -104,6 +111,7 @@ def test_correct_method_order(
     ('_protected', 'public'),
     ('_protected', '__magic__'),
 
+    ('__private', '__init_subclass__'),
     ('__private', '__new__'),
     ('__private', '__init__'),
     ('__private', '__call__'),