Repository: wemake-services/wemake-python-styleguide

Issue:
Add Exceptions for String Constant Overuse
# Rule request

I feel it would be good to be able to declare exceptions for WPS226

## Thesis / Reasoning

In a file on a current project, I tend to use `" ".join(iterable)` a lot.
It feels unnatural to declare a CONSTANT for that.
Also, I don't think it is good in this case to disable WPS226 for the entire file as there could be other common string constants.

Alternative: Excluse `" "`, `""`, "`\n"` & friends from this by default.


Discussion:
I'm for the last proposed solution 👍 No harm in using these strings in-place. They can be excluded onthe wps level by default.
+1. Had a similar case with `dict.get(field, '')` used for several fields.

Looks like exclusions to the magic numbers (WPS432), which are reasonable, too.
Hi, I would like to help.
Hi, @sumeshpradhan! Thanks a lot!
Can I take this over?

Resolving patch:
diff --git a/wemake_python_styleguide/visitors/ast/complexity/overuses.py b/wemake_python_styleguide/visitors/ast/complexity/overuses.py
--- a/wemake_python_styleguide/visitors/ast/complexity/overuses.py
+++ b/wemake_python_styleguide/visitors/ast/complexity/overuses.py
@@ -22,7 +22,25 @@
     'visit_Bytes',
 ))
 class StringOveruseVisitor(base.BaseNodeVisitor):
-    """Restricts several string usages."""
+    """
+    Restricts repeated usage of the same string constant.
+
+    NB: Some short strings are ignored, as their use is very common and
+    forcing assignment would not make much sense (i.e. newlines or "").
+    """
+
+    _ignored_string_constants = frozenset((
+        ' ',
+        '',
+        '\n',
+        '\r\n',
+        '\t',
+        b' ',
+        b'',
+        b'\n',
+        b'\r\n',
+        b'\t',
+    ))
 
     def __init__(self, *args, **kwargs) -> None:
         """Inits the counter for constants."""
@@ -46,6 +64,11 @@ def _check_string_constant(self, node: AnyText) -> None:
         if overuses.is_annotation(node):
             return
 
+        # Some strings are so common, that it makes no sense to check if
+        # they are overused.
+        if node.s in self._ignored_string_constants:
+            return
+
         self._string_constants[node.s] += 1
 
     def _post_visit(self) -> None:


Test patch:
diff --git a/tests/test_visitors/test_ast/test_complexity/test_overuses/test_overused_string.py b/tests/test_visitors/test_ast/test_complexity/test_overuses/test_overused_string.py
--- a/tests/test_visitors/test_ast/test_complexity/test_overuses/test_overused_string.py
+++ b/tests/test_visitors/test_ast/test_complexity/test_overuses/test_overused_string.py
@@ -12,6 +12,7 @@
 second({0})
 third[{0}]
 'new' + {0}
+{0}.join("1", "2", "3")
 """
 
 string_function_type_annotations1 = """
@@ -121,7 +122,7 @@ def test_string_overuse_settings(
     """Ensures that settings for string over-use work."""
     tree = parse_ast_tree(mode(strings.format(string_value)))
 
-    option_values = options(max_string_usages=4)
+    option_values = options(max_string_usages=5)
     visitor = StringOveruseVisitor(option_values, tree=tree)
     visitor.run()
 
@@ -134,8 +135,6 @@ def test_string_overuse_settings(
 @pytest.mark.parametrize('string_value', [
     '"same-string"',
     '"GenericType[int, str]"',
-    "''",
-    '""',
 ])
 @pytest.mark.parametrize('prefix', [
     'b',
@@ -195,3 +194,30 @@ def test_string_type_annotations(
     visitor.run()
 
     assert_errors(visitor, [])
+
+
+@pytest.mark.parametrize('string_value', [
+    r'"\t"',
+    r'"\n"',
+    r'""',
+])
+@pytest.mark.parametrize('prefix', [
+    'b',
+    'u',
+    '',
+])
+def test_string_overuse_exceptions(
+    assert_errors,
+    parse_ast_tree,
+    default_options,
+    prefix,
+    string_value,
+):
+    """Ensures that over-used strings raise violations."""
+    snippet = string_actions.format(prefix + string_value)
+    tree = parse_ast_tree(snippet)
+
+    visitor = StringOveruseVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [])