Repository: wemake-services/wemake-python-styleguide

Issue:
Forbid to use alphabet as a string
# Rule request

<!-- Thanks for coming up with a proposal! -->

## Thesis

<!-- What exactly do you propose? Please, provide code examples. -->

We should disallow strings with **full** alphabet inside it:

```python
GUESS_MY_NAME = "abcde...WXYZ"
UPPERCASE_ALPH = "ABCD...WXYZ"
LOWERCASE_ALPH = "abcd...wxyz"
```

Instead we should use:

```python
import string
UPPERCASE_ALPH = string.ascii_uppercase
LOWERCASE_ALPH = string.ascii_lowercase
```

## Reasoning

<!--
Why do you think this is a good idea?

Please, include some strong points and considerations.
-->

This is a best practice, similar to #787 
Original idea: https://github.com/thg-consulting/inspectortiger/blob/6b9736b22528493cd41d0bf92f9fdcdd6c7cd129/inspectortiger/plugins/upgradeable.py#L191

<!--

  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:

Resolving patch:
diff --git a/wemake_python_styleguide/violations/best_practices.py b/wemake_python_styleguide/violations/best_practices.py
--- a/wemake_python_styleguide/violations/best_practices.py
+++ b/wemake_python_styleguide/violations/best_practices.py
@@ -66,6 +66,7 @@
    WrongKeywordConditionViolation
    WrongNamedKeywordViolation
    ApproximateConstantViolation
+   StringConstantRedefinedViolation
 
 Best practices
 --------------
@@ -117,6 +118,7 @@
 .. autoclass:: WrongKeywordConditionViolation
 .. autoclass:: WrongNamedKeywordViolation
 .. autoclass:: ApproximateConstantViolation
+.. autoclass:: StringConstantRedefinedViolation
 
 """
 
@@ -1819,3 +1821,37 @@ class ApproximateConstantViolation(ASTViolation):
 
     code = 446
     error_template = 'Found approximate constant: {0}'
+
+
+@final
+class StringConstantRedefinedViolation(ASTViolation):
+    """
+    Forbid to use alphabet as a string.
+
+    Reasoning:
+        Some constants are already defined.
+        No need to write them again, use existing values.
+        We just compare strings and raise this violation
+        when they have exactly the same chars.
+
+    Solution:
+        Use pre-defined constants.
+
+    Example::
+
+        # Correct:
+        import string
+        UPPERCASE_ALPH = string.ascii_uppercase
+        LOWERCASE_ALPH = string.ascii_lowercase
+
+        # Wrong:
+        GUESS_MY_NAME = "abcde...WXYZ"
+        UPPERCASE_ALPH = "ABCD...WXYZ"
+        LOWERCASE_ALPH = "abcd...wxyz"
+
+    .. versionadded:: 0.13.0
+
+    """
+
+    error_template = 'Found alphabet as strings: {0}'
+    code = 447
diff --git a/wemake_python_styleguide/visitors/ast/builtins.py b/wemake_python_styleguide/visitors/ast/builtins.py
--- a/wemake_python_styleguide/visitors/ast/builtins.py
+++ b/wemake_python_styleguide/visitors/ast/builtins.py
@@ -1,11 +1,13 @@
 # -*- coding: utf-8 -*-
 
 import ast
+import string
 from collections import Counter, Hashable, defaultdict
 from contextlib import suppress
 from typing import (
     ClassVar,
     DefaultDict,
+    FrozenSet,
     Iterable,
     List,
     Optional,
@@ -31,6 +33,7 @@
     MagicNumberViolation,
     MultipleAssignmentsViolation,
     NonUniqueItemsInHashViolation,
+    StringConstantRedefinedViolation,
     UnhashableTypeInHashViolation,
     WrongUnpackingViolation,
 )
@@ -41,6 +44,20 @@
 class WrongStringVisitor(base.BaseNodeVisitor):
     """Restricts several string usages."""
 
+    _string_constants: FrozenSet[str] = frozenset((
+        string.ascii_letters,
+        string.ascii_lowercase,
+        string.ascii_uppercase,
+
+        string.digits,
+        string.octdigits,
+        string.hexdigits,
+
+        string.printable,
+        string.whitespace,
+        string.punctuation,
+    ))
+
     def visit_JoinedStr(self, node: ast.JoinedStr) -> None:
         """
         Restricts to use ``f`` strings.
@@ -52,6 +69,23 @@ def visit_JoinedStr(self, node: ast.JoinedStr) -> None:
         self.add_violation(consistency.FormattedStringViolation(node))
         self.generic_visit(node)
 
+    def visit_Str(self, node: ast.Str) -> None:
+        """
+        Forbid to use alphabet as a string.
+
+        Raises:
+            StringConstantRedefinedViolation
+
+        """
+        self._check_is_alphatbet(node)
+        self.generic_visit(node)
+
+    def _check_is_alphatbet(self, node: ast.Str) -> None:
+        if node.s in self._string_constants:
+            self.add_violation(
+                StringConstantRedefinedViolation(node, text=node.s),
+            )
+
 
 @final
 class WrongNumberVisitor(base.BaseNodeVisitor):


Test patch:
diff --git a/tests/fixtures/noqa.py b/tests/fixtures/noqa.py
--- a/tests/fixtures/noqa.py
+++ b/tests/fixtures/noqa.py
@@ -571,3 +571,4 @@ async def async_gen(self):
 
 print(constant[0:7])  # noqa: WPS349
 var_a = var_a + var_b  # noqa: WPS350
+LOWERCASE_ALPH = "abcdefghijklmnopqrstuvwxyz" # noqa: WPS447
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
@@ -172,6 +172,7 @@
     'WPS444': 1,
     'WPS445': 1,
     'WPS446': 1,
+    'WPS447': 1,
 
     'WPS500': 1,
     'WPS501': 1,
diff --git a/tests/test_visitors/test_ast/test_builtins/test_strings/test_alphabet_string.py b/tests/test_visitors/test_ast/test_builtins/test_strings/test_alphabet_string.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_builtins/test_strings/test_alphabet_string.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+
+import string
+
+import pytest
+
+from wemake_python_styleguide.violations.best_practices import (
+    StringConstantRedefinedViolation,
+)
+from wemake_python_styleguide.visitors.ast.builtins import WrongStringVisitor
+
+
+@pytest.mark.parametrize('code', [
+    # We don't test all values here, because of strage parsing issues.
+    string.digits,
+    string.hexdigits,
+    string.octdigits,
+
+    string.ascii_uppercase,
+    string.ascii_lowercase,
+    string.ascii_letters,
+])
+def test_alphabet_as_string_violation(
+    assert_errors,
+    assert_error_text,
+    parse_ast_tree,
+    code,
+    default_options,
+):
+    """Testing that the strings violate the rules."""
+    tree = parse_ast_tree('"{0}"'.format(code))
+
+    visitor = WrongStringVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [StringConstantRedefinedViolation])
+    assert_error_text(visitor, code)
+
+
+@pytest.mark.parametrize('code', [
+    'ABCDE',
+    '1234',
+    'random text',
+    r'\n',
+    '!@#',
+    '',
+])
+def test_alphabet_as_string_no_violation(
+    assert_errors,
+    parse_ast_tree,
+    code,
+    default_options,
+):
+    """Testing that regular strings work well."""
+    tree = parse_ast_tree('"{0}"'.format(code))
+
+    visitor = WrongStringVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])