Repository: wemake-services/wemake-python-styleguide

Issue:
Knowing the violation code it is quite hard to find the violation description
**Use Case.** I am testing wemake-python-styleguide on my project.

```
...
  22:5     WPS337 Found multiline conditions
  if (
  ^

Full list of violations and explanations:
https://wemake-python-stylegui.de/en/0.13.4/pages/usage/violations/
make: *** [infra/make/lint.mk:3: lint] Error 1
```

To get more information on what `WPS337` is for, I have to do several steps:

* Open https://wemake-python-stylegui.de/en/0.13.4/pages/usage/violations/
* Realize that WPS337 is probably behind the link titled `WPS300 - WPS399`
* And now I am stuck; I can `Ctrl + F` → `337`, but that won't help; so I try to find the word `multiline`
* and finally I reach the description I wanted: https://wemake-python-stylegui.de/en/0.13.4/pages/usage/violations/consistency.html#wemake_python_styleguide.violations.consistency.MultilineConditionsViolation

This takes time, especially when the title of the violation on this page does not exactly correspond to the name the tool prints on command line.

**Ideally,** I would see a link to the violation description right in the console, like this:

```
...
  22:5     WPS337 Found multiline conditions https://wps.it/0.13.4/WPS337/
  if (
  ^
...
```

Regrettably, wps.to domain is registered already, but wps.it is there.

**If that is not possible,** it would help if the docs pages at least included the violation codes, to make the page searchable via `Ctrl + F`.


Discussion:
That's good idea, my teammates also complain on the lack of some website, where all codes can be easily discovered and investigated, so this will be really useful
I had this idea too. But, it is kind of useless without all codes that come from other plugins and `flake8`. AFAIK there's no way of getting the violation docs from the code.

So, we would have to maintain docs for ALL violations we use in this project. 
Do this sound scary enough 😄 ?
[`flakehell`](https://github.com/life4/flakehell) has nice cli option - `flakehell codes <plugin_name>`.
I haven't investigated the implementation of codes' extractors, but it is at least somehow possible, probably they have to handle several cases, because unfortunately `flake8` doesn't enforce plugin's developers to provide some standardized documentation of all codes used by their plugins :(
So, yeap, it sounds scary :D
@orsinium do you have any ideas how can we implement this?
> do you have any ideas how can we implement this?

Yes, I do. We can provide our own CLI.

```bash
python3 -m wemake_python_styleguide code WPS337
```

Also, you can even implement your own runner as a minimal alternative to flake8 ([like deal does](https://deal.readthedocs.io/linter.html)) and show a bit more information on it like URL, short description etc.

What about third-party plugins, it's non-trivial. As @skarzi said, FlakeHell does it. However, it's mostly about ad-hoc solutions, so it's better to not pollute WPS with such logic and leave it to FlakeHell.
It can be added at the end next to each errors when the combined error count is shown, much like eslint. Each of the major plugin has a documentation page for error codes. Even if pointing to the exact code is not possible, it could give the link to documentation.

Resolving patch:
diff --git a/wemake_python_styleguide/violations/base.py b/wemake_python_styleguide/violations/base.py
--- 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 @@ def message(self) -> str:
         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 @@ def node_items(self) -> Tuple[int, int, str]:
         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 @@ def _location(self) -> Tuple[int, int]:
         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


Test patch:
diff --git a/tests/test_violations/test_docs.py b/tests/test_violations/test_docs.py
--- 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
--- 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