Repository: wemake-services/wemake-python-styleguide

Issue:
Restrict unicode names
# Rule request

## Thesis

Currently we can do something like this:

```python
никита = 'sobolev'
```

We can use unicode to create variables!
We can even do bizarre things like `Ø: Set[str] = set()`.

## Reasoning

This should be forbidden for sanity and readability.



Discussion:
I can work on this :)
@riyasyash awesome!

Resolving patch:
diff --git a/wemake_python_styleguide/logics/naming/logical.py b/wemake_python_styleguide/logics/naming/logical.py
--- a/wemake_python_styleguide/logics/naming/logical.py
+++ b/wemake_python_styleguide/logics/naming/logical.py
@@ -95,6 +95,32 @@ def is_too_short_name(
     return name != constants.UNUSED_VARIABLE and len(name) < min_length
 
 
+def is_too_long_name(
+    name: str,
+    max_length: int = defaults.MAX_NAME_LENGTH,
+) -> bool:
+    """
+    Checks for too long variable names.
+
+    >>> is_too_long_name('test')
+    False
+
+    >>> is_too_long_name('this_is_twentynine_characters')
+    False
+
+    >>> is_too_long_name('this_should_fail', max_length=10)
+    True
+
+    >>> is_too_long_name('this_is_thirty_characters_long', max_length=30)
+    False
+
+    >>> is_too_long_name('this_is_thirty_characters_long', max_length=29)
+    True
+
+    """
+    return len(name) > max_length
+
+
 def does_contain_underscored_number(name: str) -> bool:
     """
     Checks for variable names with underscored number.
@@ -160,29 +186,3 @@ def does_contain_consecutive_underscores(name: str) -> bool:
         return True
 
     return False
-
-
-def is_too_long_name(
-    name: str,
-    max_length: int = defaults.MAX_NAME_LENGTH,
-) -> bool:
-    """
-    Checks for too long variable names.
-
-    >>> is_too_long_name('test')
-    False
-
-    >>> is_too_long_name('this_is_twentynine_characters')
-    False
-
-    >>> is_too_long_name('this_should_fail', max_length=10)
-    True
-
-    >>> is_too_long_name('this_is_thirty_characters_long', max_length=30)
-    False
-
-    >>> is_too_long_name('this_is_thirty_characters_long', max_length=29)
-    True
-
-    """
-    return len(name) > max_length
diff --git a/wemake_python_styleguide/violations/naming.py b/wemake_python_styleguide/violations/naming.py
--- a/wemake_python_styleguide/violations/naming.py
+++ b/wemake_python_styleguide/violations/naming.py
@@ -122,6 +122,7 @@
    ConsecutiveUnderscoresInNameViolation
    ReservedArgumentNameViolation
    TooLongNameViolation
+   UnicodeNameViolation
 
 Module names
 ------------
@@ -142,6 +143,7 @@
 .. autoclass:: ConsecutiveUnderscoresInNameViolation
 .. autoclass:: ReservedArgumentNameViolation
 .. autoclass:: TooLongNameViolation
+.. autoclass:: UnicodeNameViolation
 
 """
 
@@ -530,7 +532,7 @@ class TooLongNameViolation(MaybeASTViolation):
         maybe it may require some documentation.
 
     Solution:
-        Think of another name. Give more context to it.
+        Think of another name. Give less context to it.
 
     This rule checks: modules, variables, attributes,
     functions, methods, and classes.
@@ -550,10 +552,37 @@ class TooLongNameViolation(MaybeASTViolation):
         Default:
         :str:`wemake_python_styleguide.options.defaults.MAX_NAME_LENGTH`
 
-    .. versionadded:: 0.1.0
-    .. versionchanged:: 0.4.0
+    .. versionadded:: 0.5.0
 
     """
 
     error_template = 'Found too long name: {0}'
     code = 118
+
+
+@final
+class UnicodeNameViolation(ASTViolation):
+    """
+    Restrict unicode names.
+
+    Reasoning:
+        This should be forbidden for sanity, readability, and writability.
+
+    Solution:
+        Rename your entities so that they contain only ASCII symbols.
+
+    Example::
+
+        # Correct:
+        some_variable = 'Text with russian: русский язык'
+
+        # Wrong:
+        переменная = 42
+        some_變量 = ''
+
+    .. versionadded:: 0.5.0
+
+    """
+
+    error_template = 'Found unicode name: {0}'
+    code = 119
diff --git a/wemake_python_styleguide/visitors/ast/naming.py b/wemake_python_styleguide/visitors/ast/naming.py
--- a/wemake_python_styleguide/visitors/ast/naming.py
+++ b/wemake_python_styleguide/visitors/ast/naming.py
@@ -85,6 +85,12 @@ def check_name(
                 self._error_callback(
                     naming.ReservedArgumentNameViolation(node, text=name),
                 )
+        try:
+            name.encode('ascii')
+        except UnicodeEncodeError:
+            self._error_callback(
+                naming.UnicodeNameViolation(node, text=name),
+            )
 
         self._ensure_length(node, name)
         self._ensure_underscores(node, name)
@@ -145,9 +151,11 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None:
 
         Raises:
             UpperCaseAttributeViolation
+            UnicodeNameViolation
 
         """
         self._validator.check_attribute_name(node)
+        self._validator.check_name(node, node.name)
         self.generic_visit(node)
 
     def visit_any_function(self, node: AnyFunctionDef) -> None:
@@ -159,6 +167,7 @@ def visit_any_function(self, node: AnyFunctionDef) -> None:
             TooShortNameViolation
             PrivateNameViolation
             TooLongNameViolation
+            UnicodeNameViolation
 
         """
         self._validator.check_name(node, node.name)
@@ -205,6 +214,7 @@ def visit_variable(self, node: VariableDef) -> None:
             TooShortNameViolation
             PrivateNameViolation
             TooLongNameViolation
+            UnicodeNameViolation
 
         """
         variable_name = name_nodes.get_assigned_name(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
@@ -38,6 +38,7 @@ def nested():  # noqa: Z430
 cls = 5  # noqa: Z117
 __author__ = 'Nikita Sobolev'  # noqa: Z410
 extremely_long_name_that_needs_to_be_shortened_to_work_fine = 2  # noqa: Z118
+привет_по_русски = 'Hello, world!'  # noqa: Z119
 
 some._execute()  # noqa: Z441
 
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
@@ -42,6 +42,7 @@ def test_noqa_fixture_disabled(absolute_path, all_violations):
         'Z116': 1,
         'Z117': 1,
         'Z118': 1,
+        'Z119': 1,
 
         'Z200': 0,
         'Z201': 0,
diff --git a/tests/test_visitors/test_ast/test_naming/test_naming.py b/tests/test_visitors/test_ast/test_naming/test_naming.py
deleted file mode 100644
--- a/tests/test_visitors/test_ast/test_naming/test_naming.py
+++ /dev/null
@@ -1,162 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import pytest
-
-from wemake_python_styleguide.constants import VARIABLE_NAMES_BLACKLIST
-from wemake_python_styleguide.violations.naming import (
-    ConsecutiveUnderscoresInNameViolation,
-    PrivateNameViolation,
-    TooLongNameViolation,
-    TooShortNameViolation,
-    UnderscoredNumberNameViolation,
-    WrongVariableNameViolation,
-)
-from wemake_python_styleguide.visitors.ast.naming import WrongNameVisitor
-
-
-@pytest.mark.parametrize('wrong_name', VARIABLE_NAMES_BLACKLIST)
-def test_wrong_variable_name(
-    assert_errors,
-    assert_error_text,
-    parse_ast_tree,
-    naming_template,
-    default_options,
-    mode,
-    wrong_name,
-):
-    """Ensures that wrong names are not allowed."""
-    tree = parse_ast_tree(mode(naming_template.format(wrong_name)))
-
-    visitor = WrongNameVisitor(default_options, tree=tree)
-    visitor.run()
-
-    assert_errors(visitor, [WrongVariableNameViolation])
-    assert_error_text(visitor, wrong_name)
-
-
-def test_short_variable_name(
-    assert_errors,
-    assert_error_text,
-    parse_ast_tree,
-    naming_template,
-    default_options,
-    mode,
-):
-    """Ensures that short names are not allowed."""
-    short_name = 'y'
-    tree = parse_ast_tree(mode(naming_template.format(short_name)))
-
-    visitor = WrongNameVisitor(default_options, tree=tree)
-    visitor.run()
-
-    assert_errors(visitor, [TooShortNameViolation])
-    assert_error_text(visitor, short_name)
-
-
-def test_private_variable_name(
-    assert_errors,
-    assert_error_text,
-    parse_ast_tree,
-    naming_template,
-    default_options,
-    mode,
-):
-    """Ensures that private names are not allowed."""
-    private_name = '__private'
-    tree = parse_ast_tree(mode(naming_template.format(private_name)))
-
-    visitor = WrongNameVisitor(default_options, tree=tree)
-    visitor.run()
-
-    assert_errors(visitor, [PrivateNameViolation])
-    assert_error_text(visitor, private_name)
-
-
-@pytest.mark.parametrize('underscored_name', [
-    'with__underscore',
-    'mutliple__under__score',
-    'triple___underscore',
-])
-def test_underscored_variable_name(
-    assert_errors,
-    assert_error_text,
-    parse_ast_tree,
-    naming_template,
-    default_options,
-    mode,
-    underscored_name,
-):
-    """Ensures that underscored names are not allowed."""
-    tree = parse_ast_tree(mode(naming_template.format(underscored_name)))
-
-    visitor = WrongNameVisitor(default_options, tree=tree)
-    visitor.run()
-
-    assert_errors(visitor, [ConsecutiveUnderscoresInNameViolation])
-    assert_error_text(visitor, underscored_name)
-
-
-@pytest.mark.parametrize('number_suffix', [
-    'number_5',
-    'between_45_letters',
-    'with_multiple_groups_4_5',
-])
-def test_number_prefix_variable_name(
-    assert_errors,
-    assert_error_text,
-    parse_ast_tree,
-    naming_template,
-    default_options,
-    mode,
-    number_suffix,
-):
-    """Ensures that number suffix names are not allowed."""
-    tree = parse_ast_tree(mode(naming_template.format(number_suffix)))
-
-    visitor = WrongNameVisitor(default_options, tree=tree)
-    visitor.run()
-
-    assert_errors(visitor, [UnderscoredNumberNameViolation])
-    assert_error_text(visitor, number_suffix)
-
-
-@pytest.mark.parametrize('correct_name', [
-    'snake_case',
-    '_protected_or_unused',
-    'with_number5',
-    'xy',
-])
-def test_naming_correct(
-    assert_errors,
-    parse_ast_tree,
-    naming_template,
-    default_options,
-    mode,
-    correct_name,
-):
-    """Ensures that correct names are allowed."""
-    tree = parse_ast_tree(mode(naming_template.format(correct_name)))
-
-    visitor = WrongNameVisitor(default_options, tree=tree)
-    visitor.run()
-
-    assert_errors(visitor, [])
-
-
-def test_long_variable_name(
-    assert_errors,
-    assert_error_text,
-    parse_ast_tree,
-    naming_template,
-    default_options,
-    mode,
-):
-    """Ensures that short names are not allowed."""
-    long_name = 'incredibly_long_name_that_should_not_pass_the_long_name_test'
-    tree = parse_ast_tree(mode(naming_template.format(long_name)))
-
-    visitor = WrongNameVisitor(default_options, tree=tree)
-    visitor.run()
-
-    assert_errors(visitor, [TooLongNameViolation])
-    assert_error_text(visitor, long_name)
diff --git a/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_long.py b/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_long.py
--- a/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_long.py
+++ b/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_long.py
@@ -12,7 +12,7 @@ def test_long_variable_name(
     default_options,
     mode,
 ):
-    """Ensures that short names are not allowed."""
+    """Ensures that long names are not allowed."""
     long_name = 'incredibly_long_name_that_should_not_pass_the_long_name_test'
     tree = parse_ast_tree(mode(naming_template.format(long_name)))
 
@@ -21,3 +21,22 @@ def test_long_variable_name(
 
     assert_errors(visitor, [TooLongNameViolation])
     assert_error_text(visitor, long_name)
+
+
+def test_long_variable_name_config(
+    assert_errors,
+    assert_error_text,
+    parse_ast_tree,
+    naming_template,
+    options,
+    mode,
+):
+    """Ensures that it is possible to configure `max_name_length`."""
+    long_name = 'incredibly_long_name_that_should_not_pass_the_long_name_test'
+    tree = parse_ast_tree(mode(naming_template.format(long_name)))
+
+    option_values = options(max_name_length=len(long_name) + 1)
+    visitor = WrongNameVisitor(option_values, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])
diff --git a/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_unicode.py b/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_unicode.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_naming/test_naming_rules/test_unicode.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from wemake_python_styleguide.violations.naming import UnicodeNameViolation
+from wemake_python_styleguide.visitors.ast.naming import WrongNameVisitor
+
+
+@pytest.mark.parametrize('wrong_name', [
+    'тестовое_имя',
+    'test_имя',
+    'сос',  # written with identical to ASCII russian chars
+    'some_變量',
+    'имя2',
+])
+def test_wrong_unicode(
+    assert_errors,
+    assert_error_text,
+    parse_ast_tree,
+    naming_template,
+    default_options,
+    mode,
+    wrong_name,
+):
+    """Test names with unicode."""
+    tree = parse_ast_tree(mode(naming_template.format(wrong_name)))
+
+    visitor = WrongNameVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [UnicodeNameViolation])
+    assert_error_text(visitor, wrong_name)