Repository: wemake-services/wemake-python-styleguide

Issue:
Allow arguments in calls inside `f` strings
This code should be accepted: `f'{_bold("Header")}'`

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/wemake-services/wemake-python-styleguide/pull/3161?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`d8275b6`)](https://app.codecov.io/gh/wemake-services/wemake-python-styleguide/commit/d8275b675fb124ee28589a7d7da037f8a04f907c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services) to head [(`8023ef1`)](https://app.codecov.io/gh/wemake-services/wemake-python-styleguide/commit/8023ef14d3c96bf5b53db23a05c540b803696e8f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##            master     #3161   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          119       119           
  Lines         5785      5775   -10     
  Branches       801       800    -1     
=========================================
- Hits          5785      5775   -10     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/wemake-services/wemake-python-styleguide/pull/3161?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wemake-services).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 38ee5ae8..32229b2b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -79,6 +79,9 @@ Semantic versioning in our case means:
 
 ### Bugfixes
 
+- Fixes `WPS217` to allow simple calls in `f` strings, #3150
+- Fixes `WPS217` not to raise on empty `f` strings,
+  because `ruff check` handles that now for us
 - Fixes `OverusedStringViolation` not to include `'...'` string
 - Removes `astor` package in favour of `ast.unparse`
 - Fixes `WPS210` to not count nested local variables in nested scopes #3108
diff --git a/wemake_python_styleguide/violations/complexity.py b/wemake_python_styleguide/violations/complexity.py
index 2b0afefc..b5bae8f2 100644
--- a/wemake_python_styleguide/violations/complexity.py
+++ b/wemake_python_styleguide/violations/complexity.py
@@ -1209,9 +1209,7 @@ class TooComplexFormattedStringViolation(ASTViolation):
     - the value of a variable
     - the value of a collection through lookup with a variable, number, or
       string as the key
-    - the return value of a procedure call without arguments
-
-    Related to :class:`~FormattedStringViolation`.
+    - the return value of a function / method call with 3 arguments maximum
 
     Reasoning:
         Complex ``f`` strings are often difficult to understand,
@@ -1227,9 +1225,11 @@ class TooComplexFormattedStringViolation(ASTViolation):
 
         # Correct:
         f'smth {user.get_full_name()}'
+        f'smth {math_func(1, 2, 3)}'
 
         # Wrong:
-        f'{reverse("url-name")}?{"&".join("user="+uid for uid in user_ids)}'
+        f'{reverse("url-name")}?{"&".join("user=" + uid for uid in user_ids)}'
+        f'smth {math_func(1, 2, 3, 4)}'
 
     .. versionadded:: 0.15.0
 
diff --git a/wemake_python_styleguide/visitors/ast/builtins.py b/wemake_python_styleguide/visitors/ast/builtins.py
index 34b3e25d..90d72d14 100644
--- a/wemake_python_styleguide/visitors/ast/builtins.py
+++ b/wemake_python_styleguide/visitors/ast/builtins.py
@@ -110,30 +110,16 @@ class WrongFormatStringVisitor(base.BaseNodeVisitor):
         self.generic_visit(node)
 
     def _check_complex_formatted_string(self, node: ast.JoinedStr) -> None:
-        """
-        Whitelists all simple uses of f strings.
-
-        Checks if list, dict, function call with no parameters or variable.
-        """
-        has_formatted_components = any(
-            isinstance(comp, ast.FormattedValue) for comp in node.values
-        )
-        if not has_formatted_components:
-            self.add_violation(  # If no formatted values
-                complexity.TooComplexFormattedStringViolation(node),
-            )
-            return
-
+        """Allows all simple uses of `f` strings."""
         for string_component in node.values:
             if isinstance(string_component, ast.FormattedValue):
                 # Test if possible chaining is invalid
-                format_value = string_component.value
-                if self._is_valid_formatted_value(format_value):
+                if self._is_valid_formatted_value(string_component.value):
                     continue
                 self.add_violation(  # Everything else is too complex:
                     complexity.TooComplexFormattedStringViolation(node),
                 )
-                break
+                return
 
     def _is_valid_formatted_value(self, format_value: ast.AST) -> bool:
         if isinstance(
@@ -146,7 +132,7 @@ class WrongFormatStringVisitor(base.BaseNodeVisitor):
     def _is_valid_final_value(self, format_value: ast.AST) -> bool:
         # Variable lookup is okay and a single attribute is okay
         if isinstance(format_value, ast.Name | ast.Attribute) or (
-            isinstance(format_value, ast.Call) and not format_value.args
+            isinstance(format_value, ast.Call) and len(format_value.args) <= 3
         ):
             return True
         # Named lookup, Index lookup & Dict key is okay


Test patch:
diff --git a/tests/test_visitors/test_ast/test_builtins/test_strings/test_formatted_string.py b/tests/test_visitors/test_ast/test_builtins/test_strings/test_formatted_string.py
index 14b40e1d..541b1239 100644
--- a/tests/test_visitors/test_ast/test_builtins/test_strings/test_formatted_string.py
+++ b/tests/test_visitors/test_ast/test_builtins/test_strings/test_formatted_string.py
@@ -29,16 +29,18 @@ f_attr_on_function = "f'{fcn().attr}'"
 f_true_index = "f'{array[True]}'"
 f_none_index = "f'{array[None]}'"
 f_byte_index = 'f\'{array[b"Hello"]}\''
+f_empty_string = "f''"
+f_function_with_single_arg = "f'smth {func(arg)}'"
+f_function_with_three_args = "f'{func(arg1, arg2, arg3)}'"
+f_method_with_three_args = "f'{obj.method(arg1, arg2, arg3)}'"
 
 # Disallowed
 f_string = "f'x + y = {2 + 2}'"
 f_double_indexing = "f'{list[0][1]}'"
 f_calling_returned_function = "f'{calling_returned_function()()}'"
-f_empty_string = "f''"
 f_complex_f_string = """
     f'{reverse(\"url-name\")}?{\"&\".join(\"user=\"+uid for uid in user_ids)}'
 """
-f_function_with_args = "f'smth {func(arg)}'"
 f_dict_lookup_function_empty_args = "f'smth {dict_value[func()]}'"
 f_list_slice_lookup = "f'smth {list[:]}'"
 f_attr_on_returned_value = "f'{some.call().attr}'"
@@ -49,6 +51,8 @@ f_triple_call = "f'{foo()()()}'"
 f_triple_lookup = "f'{arr[0][1][2]}'"
 f_double_call_arg = "f'{foo()(arg)}'"
 f_single_chained_functions = "f'{f1().f2()}'"
+f_function_with_four_args = "f'{func(arg1, arg2, arg3, arg4)}'"
+f_method_with_four_args = "f'{obj.meth(arg1, arg2, arg3, arg4)}-post'"
 
 # regression 1921
 f_string_comma_format = 'f"Count={count:,}"'
@@ -81,32 +85,10 @@ def test_string_normal(
     assert_errors(visitor, [])
 
 
-@pytest.mark.parametrize(
-    'code',
-    [
-        f_empty_string,
-    ],
-)
-def test_wrong_string(assert_errors, parse_ast_tree, code, default_options):
-    """Testing that violations are raised when reaching max value."""
-    tree = parse_ast_tree(code)
-
-    visitor = WrongFormatStringVisitor(default_options, tree=tree)
-    visitor.run()
-
-    assert_errors(
-        visitor,
-        [
-            TooComplexFormattedStringViolation,
-        ],
-    )
-
-
 @pytest.mark.parametrize(
     'code',
     [
         f_complex_f_string,
-        f_function_with_args,
         f_dict_lookup_function_empty_args,
         f_string,
         f_list_slice_lookup,
@@ -120,6 +102,7 @@ def test_wrong_string(assert_errors, parse_ast_tree, code, default_options):
         f_double_indexing,
         f_calling_returned_function,
         f_single_chained_functions,
+        f_function_with_four_args,
     ],
 )
 def test_complex_f_string(assert_errors, parse_ast_tree, code, default_options):
@@ -148,6 +131,10 @@ def test_complex_f_string(assert_errors, parse_ast_tree, code, default_options):
         f_none_index,
         f_byte_index,
         f_string_comma_format,
+        f_empty_string,
+        f_function_with_single_arg,
+        f_function_with_three_args,
+        f_method_with_three_args,
     ],
 )
 def test_simple_f_string(assert_errors, parse_ast_tree, code, default_options):