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:
codecov[bot]: # [Codecov](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649?src=pr&el=h1) Report
> Merging [#1649](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649?src=pr&el=desc) into [master](https://codecov.io/gh/wemake-services/wemake-python-styleguide/commit/9879a18acafc691db215afb7c42dceb9a365a604?el=desc) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649/graphs/tree.svg?width=650&height=150&src=pr&token=yMZCKw0D02)](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##            master     #1649   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          105       105           
  Lines         5410      5392   -18     
  Branches      1189      1186    -3     
=========================================
- Hits          5410      5392   -18     
```


| [Impacted Files](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [...hon\_styleguide/visitors/ast/complexity/overuses.py](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649/diff?src=pr&el=tree#diff-d2VtYWtlX3B5dGhvbl9zdHlsZWd1aWRlL3Zpc2l0b3JzL2FzdC9jb21wbGV4aXR5L292ZXJ1c2VzLnB5) | `100.00% <100.00%> (ø)` | |
| [wemake\_python\_styleguide/compat/nodes.py](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649/diff?src=pr&el=tree#diff-d2VtYWtlX3B5dGhvbl9zdHlsZWd1aWRlL2NvbXBhdC9ub2Rlcy5weQ==) | `100.00% <0.00%> (ø)` | |
| [wemake\_python\_styleguide/logic/walrus.py](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649/diff?src=pr&el=tree#diff-d2VtYWtlX3B5dGhvbl9zdHlsZWd1aWRlL2xvZ2ljL3dhbHJ1cy5weQ==) | `100.00% <0.00%> (ø)` | |
| [wemake\_python\_styleguide/compat/routing.py](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649/diff?src=pr&el=tree#diff-d2VtYWtlX3B5dGhvbl9zdHlsZWd1aWRlL2NvbXBhdC9yb3V0aW5nLnB5) | `100.00% <0.00%> (ø)` | |
| [wemake\_python\_styleguide/logic/safe\_eval.py](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649/diff?src=pr&el=tree#diff-d2VtYWtlX3B5dGhvbl9zdHlsZWd1aWRlL2xvZ2ljL3NhZmVfZXZhbC5weQ==) | `100.00% <0.00%> (ø)` | |
| [wemake\_python\_styleguide/compat/packaging.py](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649/diff?src=pr&el=tree#diff-d2VtYWtlX3B5dGhvbl9zdHlsZWd1aWRlL2NvbXBhdC9wYWNrYWdpbmcucHk=) | `100.00% <0.00%> (ø)` | |
| [wemake\_python\_styleguide/visitors/ast/functions.py](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649/diff?src=pr&el=tree#diff-d2VtYWtlX3B5dGhvbl9zdHlsZWd1aWRlL3Zpc2l0b3JzL2FzdC9mdW5jdGlvbnMucHk=) | `100.00% <0.00%> (ø)` | |
| [wemake\_python\_styleguide/visitors/ast/operators.py](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649/diff?src=pr&el=tree#diff-d2VtYWtlX3B5dGhvbl9zdHlsZWd1aWRlL3Zpc2l0b3JzL2FzdC9vcGVyYXRvcnMucHk=) | `100.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649?src=pr&el=footer). Last update [9879a18...92f86a1](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1649?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b37b5fea..c54bb4d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ Semantic versioning in our case means:
 - Forbids using non-trivial expressions as an argument to `except`
 - Forbids using too many variables in a tuple unpacking
 - Allow `__call__` method to be asynchronous
+- Allows common strings not to be counted against string constant overuse limit
 
 ### Bugfixes
 
diff --git a/wemake_python_styleguide/visitors/ast/complexity/overuses.py b/wemake_python_styleguide/visitors/ast/complexity/overuses.py
index 568cf81c..e7e668cb 100644
--- a/wemake_python_styleguide/visitors/ast/complexity/overuses.py
+++ b/wemake_python_styleguide/visitors/ast/complexity/overuses.py
@@ -22,7 +22,25 @@ _FunctionExpressions = DefaultDict[ast.AST, _Expressions]
     '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 @@ class StringOveruseVisitor(base.BaseNodeVisitor):
         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
index 79d2d632..06d1b79a 100644
--- 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 @@ first = {0}
 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, [])