Repository: MartinThoma/flake8-simplify (2022-02-27 ~ 2022-03-28)

problem_statement: [Adjust Rule] SIM113 False Positive in Nested Loops
## Desired change
SIM113 False Positive in nested loops

In this case, we could using enumerate is rather tricky. How do you suggest we best do it in nested loops like so?
* **Rule(s)**: SIM113
* **Adjustment**: In which way should it/they be adjusted?
The result should not be triggered in nested loops

## Explanation

Why is the adjustment necessary / better?
Because the enumerate logic actually becomes MORE complicated in this nested loop context. You need to have some complex multiplication to get the proper number and that may not even be possible if ```bars``` is a generator that has variable length. In which case, counting manually here is actually for the best.


## Example

This is an example where the mentioned rule(s) would currently be suboptimal:

```python
count = 0
for foo in foos:
    for bar in bars:
        count +=1
```
In this second one, I could se the argument for epochs (assuming there is not complex increment logic, but it complains about number of iterations)
```python
epoch, i = np.load(resume_file)
while epochs < TOTAL_EPOCHS:
    for b in batches:
        i+=1
```
date: 2022-02-27
patch: diff --git a/flake8_simplify/__init__.py b/flake8_simplify/__init__.py
index 3686a57..db11afc 100644
--- a/flake8_simplify/__init__.py
+++ b/flake8_simplify/__init__.py
@@ -165,7 +165,7 @@ def add_meta(root: ast.AST, level: int = 0) -> None:
     previous_sibling = None
     for node in ast.iter_child_nodes(root):
         if level == 0:
-            node.parent = None  # type: ignore
+            node.parent = root  # type: ignore
         node.previous_sibling = previous_sibling  # type: ignore
         node.next_sibling = None  # type: ignore
         if previous_sibling:
diff --git a/flake8_simplify/rules/ast_for.py b/flake8_simplify/rules/ast_for.py
index 36ea38b..a8151f7 100644
--- a/flake8_simplify/rules/ast_for.py
+++ b/flake8_simplify/rules/ast_for.py
@@ -140,7 +140,7 @@ def get_sim113(node: ast.For) -> List[Tuple[int, int, str]]:
                 Expr(
                     value=Constant(value=Ellipsis, kind=None),
                 ),
-                AugAssign(
+                AugAssign( -- agument and assign, aka "+= 1"
                     target=Name(id='idx', ctx=Store()),
                     op=Add(),
                     value=Constant(value=1, kind=None),
@@ -150,11 +150,12 @@ def get_sim113(node: ast.For) -> List[Tuple[int, int, str]]:
             type_comment=None,
         ),
     """
-    RULE = "SIM113 Use enumerate instead of '{variable}'"
     errors: List[Tuple[int, int, str]] = []
     variable_candidates = []
     if body_contains_continue(node.body):
         return errors
+
+    # Find variables that might just count the iteration of the current loop
     for expression in node.body:
         if (
             isinstance(expression, ast.AugAssign)
@@ -162,13 +163,32 @@ def get_sim113(node: ast.For) -> List[Tuple[int, int, str]]:
             and isinstance(expression.target, ast.Name)
         ):
             variable_candidates.append(expression.target)
+    str_candidates = [to_source(x) for x in variable_candidates]
+
+    older_siblings = []
+    for older_sibling in node.parent.body:  # type: ignore
+        if older_sibling is node:
+            break
+        older_siblings.append(older_sibling)
+
+    matches = [
+        n.targets[0]
+        for n in older_siblings
+        if isinstance(n, ast.Assign)
+        and len(n.targets) == 1
+        and isinstance(n.targets[0], ast.Name)
+        and to_source(n.targets[0]) in str_candidates
+    ]
+    if len(matches) == 0:
+        return errors
 
-    for candidate in variable_candidates:
+    for match in matches:
+        variable = to_source(match)
         errors.append(
             (
-                candidate.lineno,
-                candidate.col_offset,
-                RULE.format(variable=to_source(candidate)),
+                match.lineno,
+                match.col_offset,
+                f"SIM113 Use enumerate instead of '{variable}'",
             )
         )
     return errors

test_patch: diff --git a/tests/test_100_rules.py b/tests/test_100_rules.py
index 30b2448..44ed391 100644
--- a/tests/test_100_rules.py
+++ b/tests/test_100_rules.py
@@ -276,36 +276,48 @@ def test_sim112_get_with_default():
 
 def test_sim113():
     ret = _results(
-        """for el in iterable:
+        """
+idx = 0
+for el in iterable:
     idx += 1"""
     )
-    assert ret == {"2:4 SIM113 Use enumerate instead of 'idx'"}
+    assert ret == {"2:0 SIM113 Use enumerate instead of 'idx'"}
 
 
-def test_sim113_false_positive():
-    ret = _results(
+@pytest.mark.parametrize(
+    "s",
+    (
         """for x in xs:
-    cm[x] += 1"""
-    )
-    assert ret == set()
-
-
-def test_sim113_false_positive_add_string():
-    ret = _results(
+    cm[x] += 1""",
         r"""for line in read_list(redis_conn, storage_key):
-    line += '\n'"""
-    )
-    assert ret == set()
-
-
-def test_sim113_false_positive_continue():
-    ret = _results(
+    line += '\n'""",
         """even_numbers = 0
 for el in range(100):
     if el % 2 == 1:
         continue
-    even_numbers += 1"""
-    )
+    even_numbers += 1""",
+        # The following two examples (double-loop and while-loop) were
+        # contributed by Skylion007 via
+        # https://github.com/MartinThoma/flake8-simplify/issues/25
+        """count = 0
+for foo in foos:
+    for bar in bars:
+        count +=1""",
+        """epoch, i = np.load(resume_file)
+while epochs < TOTAL_EPOCHS:
+    for b in batches:
+        i+=1""",
+    ),
+    ids=(
+        "augment-dict",
+        "add-string",
+        "continue",
+        "double-loop",
+        "while-loop",
+    ),
+)
+def test_sim113_false_positive(s):
+    ret = _results(s)
     assert ret == set()
 
 


----

problem_statement: [Adjust Rule] SIM102: 'if __name__ == "__main__"' does not count
## Desired change

* **Rule(s)**: SIM102
* **Adjustment**: Ignore `if __name__ == "__main__"`

## Explanation

the check `if __name__ == "__main__"` is special. People recognize it as a special part of a script. Although one can combine things, I think it's better not to do it.

## Example

The following should not trigger SIM102

```python
if __name__ == "__main__":
    if foo(): ...
```

date: 2022-02-27
patch: diff --git a/flake8_simplify/rules/ast_if.py b/flake8_simplify/rules/ast_if.py
index 1756458..4136a26 100644
--- a/flake8_simplify/rules/ast_if.py
+++ b/flake8_simplify/rules/ast_if.py
@@ -9,7 +9,7 @@ from flake8_simplify.utils import get_if_body_pairs, is_body_same, to_source
 
 def get_sim102(node: ast.If) -> List[Tuple[int, int, str]]:
     """Get a list of all nested if-statements without else-blocks."""
-    SIM102 = "SIM102 Use a single if-statement instead of nested if-statements"
+    RULE = "SIM102 Use a single if-statement instead of nested if-statements"
     errors: List[Tuple[int, int, str]] = []
 
     # ## Pattern 1
@@ -31,7 +31,17 @@ def get_sim102(node: ast.If) -> List[Tuple[int, int, str]]:
 
     if not is_pattern_1:
         return errors
-    errors.append((node.lineno, node.col_offset, SIM102))
+    is_main_check = (
+        isinstance(node.test, ast.Compare)
+        and isinstance(node.test.left, ast.Name)
+        and node.test.left.id == "__name__"
+        and len(node.test.comparators) == 1
+        and isinstance(node.test.comparators[0], ast.Str)
+        and node.test.comparators[0].s == "__main__"
+    )
+    if is_main_check:
+        return errors
+    errors.append((node.lineno, node.col_offset, RULE))
     return errors
 
 

test_patch: diff --git a/tests/test_100_rules.py b/tests/test_100_rules.py
index 44ed391..17f4cf7 100644
--- a/tests/test_100_rules.py
+++ b/tests/test_100_rules.py
@@ -54,13 +54,20 @@ def test_sim102_not_active1():
     assert ret == set()
 
 
-def test_sim102_not_active2():
-    ret = _results(
+@pytest.mark.parametrize(
+    "s",
+    (
+        """if __name__ == "__main__":
+    if foo(): ...""",
         """if a:
     d
     if b:
-        c"""
-    )
+        c""",
+    ),
+    ids=("__main__", "intermediate-statement"),
+)
+def test_sim102_should_not_trigger(s):
+    ret = _results(s)
     assert ret == set()
 
 


----

problem_statement: [Adjust Rule] SIM111 - Detect if another statement comes after the loop
## Desired change

* **Rule(s)**: SIM111
* **Adjustment**: Detect if another statement comes after the loop

## Explanation

The below code is flagged with SIM111, but if I would would use the suggested code, the `call_method` call would be lost.

## Example

```python
for a in my_list:
  if a == 2:
    return False
call_method()
return True
```

date: 2022-03-28
patch: diff --git a/flake8_simplify/rules/ast_for.py b/flake8_simplify/rules/ast_for.py
index fcbac36..8d4d984 100644
--- a/flake8_simplify/rules/ast_for.py
+++ b/flake8_simplify/rules/ast_for.py
@@ -97,7 +97,7 @@ def get_sim110_sim111(node: ast.For) -> List[Tuple[int, int, str]]:
         return errors
     if not hasattr(node.body[0].body[0].value, "value"):
         return errors
-    if isinstance(node.next_sibling, ast.Raise):  # type: ignore
+    if not isinstance(node.next_sibling, ast.Return):  # type: ignore
         return errors
     check = to_source(node.body[0].test)
     target = to_source(node.target)

test_patch: diff --git a/tests/test_100_rules.py b/tests/test_100_rules.py
index 2164e31..fba08fe 100644
--- a/tests/test_100_rules.py
+++ b/tests/test_100_rules.py
@@ -498,3 +498,15 @@ def test_sim120():
     assert results == {
         "1:0 SIM120 Use 'class FooBar:' instead of 'class FooBar(object):'"
     }
+
+
+def test_sim111_false_positive():
+    results = _results(
+        """for a in my_list:
+  if a == 2:
+    return False
+call_method()
+return True"""
+    )
+    for el in results:
+        assert "SIM111" not in el