Repository: wemake-services/wemake-python-styleguide

Issue:
WPS426 triggers on safe lambdas
# Bug report

<!--
Hi, thanks for submitting a bug. We appreciate that.

But, we will need some information about what's wrong to help you.
-->

## What's wrong

<!-- Describe what is not working. Please, attach a traceback. -->

```python
def _show_titles(number_of_posts: int) -> Sequence[FutureResultE[str]]:
    return [
        # Notice how easily we compose async and sync functions:
        _fetch_post(post_id).map(lambda post: post['title'])
        # TODO: try `for post_id in (2, 1, 0):` to see how async errors work
        for post_id in range(1, number_of_posts + 1)
    ]
```

Raises:

```
  32:9     WPS426 Found `lambda` in loop's body
  _fetch_post(post_id).map(lambda post: post['title'])
  ^
```

But it should not!

## How is that should be

Why? Because this `lambda` does not use `post_id` in its body. It is safe to be used.
So, if `lambda` uses only its args - we can allow it.

<!-- Describe how it should work. -->

<!--

  Love wemake-python-styleguide? ❤️
  Do you want your bugs to be solved faster?

  Please consider supporting our collective:
👉  https://opencollective.com/wemake-python-styleguide/donate

-->

## System information

### flake8 information

Contents of `flake8 --bug-report`:

### pip information

Contents of `pip freeze`:


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1694?src=pr&el=h1) Report
> Merging [#1694](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1694?src=pr&el=desc) into [master](https://codecov.io/gh/wemake-services/wemake-python-styleguide/commit/ece40b94d7649cc30a3e8a896513719d28099aed?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/1694/graphs/tree.svg?width=650&height=150&src=pr&token=yMZCKw0D02)](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1694?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##            master     #1694   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          106       106           
  Lines         5757      5733   -24     
  Branches      1286      1282    -4     
=========================================
- Hits          5757      5733   -24     
```


| [Impacted Files](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1694?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [wemake\_python\_styleguide/visitors/ast/loops.py](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1694/diff?src=pr&el=tree#diff-d2VtYWtlX3B5dGhvbl9zdHlsZWd1aWRlL3Zpc2l0b3JzL2FzdC9sb29wcy5weQ==) | `100.00% <100.00%> (ø)` | |
| [wemake\_python\_styleguide/compat/nodes.py](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1694/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/1694/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/1694/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/1694/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/1694/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/1694/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/1694/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/1694?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/1694?src=pr&el=footer). Last update [ece40b9...4825098](https://codecov.io/gh/wemake-services/wemake-python-styleguide/pull/1694?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 ec4ed88a..5cf56332 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -46,6 +46,7 @@ Semantic versioning in our case means:
 - Allowed yield statements in call method
 - Allow to use `^` with `1`
 - Fixes false positives in WPS513 and WPS323
+- Fixes false positive WPS426 if `lambda` in loop uses only its arguments
 
 ### Misc
 
diff --git a/wemake_python_styleguide/visitors/ast/loops.py b/wemake_python_styleguide/visitors/ast/loops.py
index 298f0fd0..92a3c4ce 100644
--- a/wemake_python_styleguide/visitors/ast/loops.py
+++ b/wemake_python_styleguide/visitors/ast/loops.py
@@ -177,15 +177,17 @@ class WrongLoopVisitor(base.BaseNodeVisitor):
         self,
         node: Union[AnyLoop, AnyComprehension],
     ) -> None:
-        container_names = self._containers.get(type(node), ())
-        for container in container_names:
-            body = getattr(node, container, [])
-            if not isinstance(body, list):
-                body = [body]
-
-            for subnode in body:
-                if walk.is_contained(subnode, ast.Lambda):
-                    self.add_violation(LambdaInsideLoopViolation(node))
+        for lambda_node in walk.get_subnodes_by_type(node, ast.Lambda):
+            arguments = (
+                arg.arg for arg
+                in walk.get_subnodes_by_type(lambda_node, ast.arg)
+            )
+            body = (
+                subnode.id for subnode
+                in walk.get_subnodes_by_type(lambda_node.body, ast.Name)
+            )
+            if not all(symbol in arguments for symbol in body):
+                self.add_violation(LambdaInsideLoopViolation(node))
 
     def _check_useless_continue(self, node: AnyLoop) -> None:
         nodes_at_line: DefaultDict[int, List[ast.AST]] = defaultdict(list)


Test patch:
diff --git a/tests/test_visitors/test_ast/test_loops/test_loops/test_lambda_in_loop.py b/tests/test_visitors/test_ast/test_loops/test_loops/test_lambda_in_loop.py
index f00a135c..722f3325 100644
--- a/tests/test_visitors/test_ast/test_loops/test_loops/test_lambda_in_loop.py
+++ b/tests/test_visitors/test_ast/test_loops/test_loops/test_lambda_in_loop.py
@@ -17,14 +17,7 @@ nested_lambda_inside_for_loop = """
 def wrapper():
     for index in range(10):
         if some:
-            print(lambda: index)
-"""
-
-nested_argument_lambda_inside_for_loop = """
-def wrapper():
-    for index in range(10):
-        if some:
-            print(lambda index=inde: index)
+            print(lambda x: x + index)
 """
 
 lambda_inside_while_loop = """
@@ -42,17 +35,17 @@ while True:
 
 lambda_inside_set = """
 def wrapper():
-    return {lambda x=x: x for x in some()}
+    return {lambda: x for x in some()}
 """
 
 lambda_inside_list = """
 def wrapper():
-    return [lambda x: x for x in some()]
+    return [lambda: x for x in some()]
 """
 
 lambda_inside_gen = """
 def wrapper():
-    return (lambda x: x for x in some())
+    return (lambda: x for x in some())
 """
 
 lambda_inside_dict_key = """
@@ -68,6 +61,12 @@ def wrapper():
 # Correct:
 
 correct_lambda_inside_for = """
+def wrapper():
+    for index in range(10):
+        print(lambda x: x)
+"""
+
+correct_lambda_inside_map = """
 def wrapper():
     for some in map(lambda x: x * 2, items):
         ...
@@ -80,14 +79,20 @@ def wrapper():
 
 correct_lambda_inside_list = """
 def wrapper():
-    return [x for x in some() if callback(lambda: x)]
+    return [x for x in some() if callback(lambda x: x)]
+"""
+
+correct_lambda_nested_argument_for = """
+def wrapper():
+    for index in range(10):
+        if some:
+            print(lambda index=some: index)
 """
 
 
 @pytest.mark.parametrize('code', [
     lambda_inside_for_loop,
     nested_lambda_inside_for_loop,
-    nested_argument_lambda_inside_for_loop,
     lambda_inside_while_loop,
     nested_lambda_inside_while_loop,
     lambda_inside_list,
@@ -114,8 +119,10 @@ def test_lambda_body(
 
 @pytest.mark.parametrize('code', [
     correct_lambda_inside_for,
+    correct_lambda_inside_map,
     correct_lambda_inside_list,
     correct_lambda_inside_set,
+    correct_lambda_nested_argument_for,
 ])
 def test_correct_lambda_body(
     assert_errors,