From f1d1a531df924f0f753311e5367738bc3af445c0 Mon Sep 17 00:00:00 2001 From: Korben11 Date: Thu, 27 Jan 2022 23:05:32 +0100 Subject: [PATCH] Add B020 check to find for-loop control variable overiding iter set (#220) * Implement rule B020: for-loop control variable overrides iter set * Update tests/b020.py Co-authored-by: Cooper Lees --- .gitignore | 3 +++ README.rst | 7 +++++++ bugbear.py | 21 +++++++++++++++++++++ tests/b020.py | 22 ++++++++++++++++++++++ tests/test_bugbear.py | 13 +++++++++++++ 5 files changed, 66 insertions(+) create mode 100644 tests/b020.py diff --git a/.gitignore b/.gitignore index 1dc5815..fca188e 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,6 @@ target/ .ipynb_checkpoints .vscode + +# JetBrains +.idea/ diff --git a/README.rst b/README.rst index 2ecc7b8..09e87df 100644 --- a/README.rst +++ b/README.rst @@ -132,6 +132,8 @@ data available in ``ex``. **B018**: Found useless expression. Either assign it to a variable or remove it. +**B020**: Loop control variable overrides iterable it iterates + Opinionated warnings ~~~~~~~~~~~~~~~~~~~~ @@ -245,6 +247,11 @@ MIT Change Log ---------- +Unreleased +~~~~~~~~~~ + +* B020: ensure loop control variable doesn't overrides iterable it iterates (#220) + 22.1.11 ~~~~~~~~~~ diff --git a/bugbear.py b/bugbear.py index b4fcf89..177af11 100644 --- a/bugbear.py +++ b/bugbear.py @@ -333,6 +333,7 @@ def visit_Assign(self, node): def visit_For(self, node): self.check_for_b007(node) + self.check_for_b020(node) self.generic_visit(node) def visit_Assert(self, node): @@ -506,6 +507,20 @@ def check_for_b017(self, node): ): self.errors.append(B017(node.lineno, node.col_offset)) + def check_for_b020(self, node): + targets = NameFinder() + targets.visit(node.target) + ctrl_names = set(targets.names) + + iterset = NameFinder() + iterset.visit(node.iter) + iterset_names = set(iterset.names) + + for name in sorted(ctrl_names): + if name in iterset_names: + n = targets.names[name][0] + self.errors.append(B020(n.lineno, n.col_offset, vars=(name,))) + def check_for_b904(self, node): """Checks `raise` without `from` inside an `except` clause. @@ -871,6 +886,12 @@ def visit(self, node): "B018 Found useless expression. Either assign it to a variable or remove it." ) ) +B020 = Error( + message=( + "B020 Found for loop that reassigns the iterable it is iterating " + + "with each iterable value." + ) +) # Warnings disabled by default. B901 = Error( diff --git a/tests/b020.py b/tests/b020.py new file mode 100644 index 0000000..df30e75 --- /dev/null +++ b/tests/b020.py @@ -0,0 +1,22 @@ +""" +Should emit: +B020 - on lines 8 and 21 +""" + +items = [1, 2, 3] + +for items in items: + print(items) + +items = [1, 2, 3] + +for item in items: + print(item) + +values = {"secret": 123} + +for key, value in values.items(): + print(f"{key}, {value}") + +for key, values in values.items(): + print(f"{key}, {values}") diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py index d02b637..7e25386 100644 --- a/tests/test_bugbear.py +++ b/tests/test_bugbear.py @@ -29,6 +29,7 @@ B016, B017, B018, + B020, B901, B902, B903, @@ -249,6 +250,18 @@ def test_b018_classes(self): expected.append(B018(33, 4)) self.assertEqual(errors, self.errors(*expected)) + def test_b020(self): + filename = Path(__file__).absolute().parent / "b020.py" + bbc = BugBearChecker(filename=str(filename)) + errors = list(bbc.run()) + self.assertEqual( + errors, + self.errors( + B020(8, 4, vars=("items",)), + B020(21, 9, vars=("values",)), + ), + ) + def test_b901(self): filename = Path(__file__).absolute().parent / "b901.py" bbc = BugBearChecker(filename=str(filename))