Skip to content

Commit 6bf907c

Browse files
authored
B909 improvements (#460)
* refactor(B909): Clean up test for B909 This splits the convoluted test files into clearer categories of what is being tested. Previously I had the test file contain semantically correct code, that would also run just fine, except for the errors. I scratched that idea now to aid the readability and understandability of the test file. * feat(B909): Add more container mutating functions * feat(b909): Add more cases to detect for b909 This commit takes care of detecting mutations stemming from AugAssign and Assign. Also removes incorrect detection of "del foo". * feat(b909): Ignore mutations followed by unconfiditional break
1 parent 28fe268 commit 6bf907c

File tree

3 files changed

+173
-74
lines changed

3 files changed

+173
-74
lines changed

bugbear.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import re
99
import sys
1010
import warnings
11-
from collections import namedtuple
11+
from collections import defaultdict, namedtuple
1212
from contextlib import suppress
1313
from functools import lru_cache, partial
1414
from keyword import iskeyword
@@ -1585,7 +1585,9 @@ def check_for_b909(self, node: ast.For):
15851585
return
15861586
checker = B909Checker(name)
15871587
checker.visit(node.body)
1588-
for mutation in checker.mutations:
1588+
for mutation in itertools.chain.from_iterable(
1589+
m for m in checker.mutations.values()
1590+
):
15891591
self.errors.append(B909(mutation.lineno, mutation.col_offset))
15901592

15911593

@@ -1611,24 +1613,43 @@ class B909Checker(ast.NodeVisitor):
16111613
"insert",
16121614
"pop",
16131615
"popitem",
1616+
"setdefault",
1617+
"update",
1618+
"intersection_update",
1619+
"difference_update",
1620+
"symmetric_difference_update",
1621+
"add",
1622+
"discard",
16141623
)
16151624

16161625
def __init__(self, name: str):
16171626
self.name = name
1618-
self.mutations = []
1627+
self.mutations = defaultdict(list)
1628+
self._conditional_block = 0
1629+
1630+
def visit_Assign(self, node: ast.Assign):
1631+
for target in node.targets:
1632+
if isinstance(target, ast.Subscript) and _to_name_str(target.value):
1633+
self.mutations[self._conditional_block].append(node)
1634+
self.generic_visit(node)
1635+
1636+
def visit_AugAssign(self, node: ast.AugAssign):
1637+
if _to_name_str(node.target) == self.name:
1638+
self.mutations[self._conditional_block].append(node)
1639+
self.generic_visit(node)
16191640

16201641
def visit_Delete(self, node: ast.Delete):
16211642
for target in node.targets:
16221643
if isinstance(target, ast.Subscript):
16231644
name = _to_name_str(target.value)
16241645
elif isinstance(target, (ast.Attribute, ast.Name)):
1625-
name = _to_name_str(target)
1646+
name = "" # ignore "del foo"
16261647
else:
16271648
name = "" # fallback
16281649
self.generic_visit(target)
16291650

16301651
if name == self.name:
1631-
self.mutations.append(node)
1652+
self.mutations[self._conditional_block].append(node)
16321653

16331654
def visit_Call(self, node: ast.Call):
16341655
if isinstance(node.func, ast.Attribute):
@@ -1640,17 +1661,24 @@ def visit_Call(self, node: ast.Call):
16401661
function_object == self.name
16411662
and function_name in self.MUTATING_FUNCTIONS
16421663
):
1643-
self.mutations.append(node)
1664+
self.mutations[self._conditional_block].append(node)
16441665

16451666
self.generic_visit(node)
16461667

1668+
def visit_If(self, node: ast.If):
1669+
self._conditional_block += 1
1670+
self.visit(node.body)
1671+
self._conditional_block += 1
1672+
16471673
def visit(self, node):
16481674
"""Like super-visit but supports iteration over lists."""
16491675
if not isinstance(node, list):
16501676
return super().visit(node)
16511677

16521678
for elem in node:
1653-
super().visit(elem)
1679+
if isinstance(elem, ast.Break):
1680+
self.mutations[self._conditional_block].clear()
1681+
self.visit(elem)
16541682
return node
16551683

16561684

tests/b909.py

Lines changed: 106 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,74 @@
33
B999 - on lines 11, 25, 26, 40, 46
44
"""
55

6-
7-
some_list = [1, 2, 3]
8-
for elem in some_list:
9-
print(elem)
10-
if elem % 2 == 0:
11-
some_list.remove(elem) # should error
6+
# lists
127

138
some_list = [1, 2, 3]
149
some_other_list = [1, 2, 3]
1510
for elem in some_list:
16-
print(elem)
17-
if elem % 2 == 0:
18-
some_other_list.remove(elem) # should not error
19-
del some_other_list
11+
# errors
12+
some_list.remove(elem)
13+
del some_list[2]
14+
some_list.append(elem)
15+
some_list.sort()
16+
some_list.reverse()
17+
some_list.clear()
18+
some_list.extend([1, 2])
19+
some_list.insert(1, 1)
20+
some_list.pop(1)
21+
some_list.pop()
22+
23+
# conditional break should error
24+
if elem == 2:
25+
some_list.remove(elem)
26+
if elem == 3:
27+
break
28+
29+
# non-errors
30+
some_other_list.remove(elem)
31+
del some_list
32+
del some_other_list
33+
found_idx = some_list.index(elem)
34+
some_list = 3
35+
36+
# unconditional break should not error
37+
if elem == 2:
38+
some_list.remove(elem)
39+
break
2040

2141

22-
some_list = [1, 2, 3]
23-
for elem in some_list:
24-
print(elem)
25-
if elem % 2 == 0:
26-
del some_list[2] # should error
27-
del some_list
42+
# dicts
43+
mydicts = {'a': {'foo': 1, 'bar': 2}}
44+
45+
for elem in mydicts:
46+
# errors
47+
mydicts.popitem()
48+
mydicts.setdefault('foo', 1)
49+
mydicts.update({'foo': 'bar'})
50+
51+
# no errors
52+
elem.popitem()
53+
elem.setdefault('foo', 1)
54+
elem.update({'foo': 'bar'})
55+
56+
# sets
57+
58+
myset = {1, 2, 3}
59+
60+
for _ in myset:
61+
# errors
62+
myset.update({4, 5})
63+
myset.intersection_update({4, 5})
64+
myset.difference_update({4, 5})
65+
myset.symmetric_difference_update({4, 5})
66+
myset.add(4)
67+
myset.discard(3)
68+
69+
# no errors
70+
del myset
2871

2972

73+
# members
3074
class A:
3175
some_list: list
3276

@@ -35,41 +79,51 @@ def __init__(self, ls):
3579

3680

3781
a = A((1, 2, 3))
82+
# ensure member accesses are handled
3883
for elem in a.some_list:
39-
print(elem)
40-
if elem % 2 == 0:
41-
a.some_list.remove(elem) # should error
42-
43-
a = A((1, 2, 3))
44-
for elem in a.some_list:
45-
print(elem)
46-
if elem % 2 == 0:
47-
del a.some_list[2] # should error
48-
49-
50-
51-
some_list = [1, 2, 3]
52-
for elem in some_list:
53-
print(elem)
54-
if elem == 2:
55-
found_idx = some_list.index(elem) # should not error
56-
some_list.append(elem) # should error
57-
some_list.sort() # should error
58-
some_list.reverse() # should error
59-
some_list.clear() # should error
60-
some_list.extend([1,2]) # should error
61-
some_list.insert(1, 1) # should error
62-
some_list.pop(1) # should error
63-
some_list.pop() # should error
64-
some_list = 3 # should error
84+
a.some_list.remove(elem)
85+
del a.some_list[2]
86+
87+
88+
# Augassign
89+
90+
foo = [1, 2, 3]
91+
bar = [4, 5, 6]
92+
for _ in foo:
93+
foo *= 2
94+
foo += bar
95+
foo[1] = 9 #todo
96+
foo[1:2] = bar
97+
foo[1:2:3] = bar
98+
99+
foo = {1,2,3}
100+
bar = {4,5,6}
101+
for _ in foo:
102+
foo |= bar
103+
foo &= bar
104+
foo -= bar
105+
foo ^= bar
106+
107+
108+
# more tests for unconditional breaks
109+
for _ in foo:
110+
foo.remove(1)
111+
for _ in bar:
112+
bar.remove(1)
65113
break
66-
67-
68-
69-
mydicts = {'a': {'foo': 1, 'bar': 2}}
70-
71-
for mydict in mydicts:
72-
if mydicts.get('a', ''):
73-
print(mydict['foo']) # should not error
74-
mydicts.popitem() # should error
75-
114+
break
115+
116+
# should not error
117+
for _ in foo:
118+
foo.remove(1)
119+
for _ in bar:
120+
...
121+
break
122+
123+
# should error (?)
124+
for _ in foo:
125+
foo.remove(1)
126+
if bar:
127+
bar.remove(1)
128+
break
129+
break

tests/test_bugbear.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -974,22 +974,39 @@ def test_b909(self):
974974
mock_options = Namespace(select=[], extend_select=["B909"])
975975
bbc = BugBearChecker(filename=str(filename), options=mock_options)
976976
errors = list(bbc.run())
977-
print(errors)
978977
expected = [
979-
B909(11, 8),
980-
B909(26, 8),
981-
B909(27, 8),
982-
B909(41, 8),
983-
B909(47, 8),
984-
B909(56, 8),
985-
B909(57, 8),
986-
B909(58, 8),
987-
B909(59, 8),
988-
B909(60, 8),
989-
B909(61, 8),
990-
B909(62, 8),
991-
B909(63, 8),
992-
B909(74, 8),
978+
B909(12, 4),
979+
B909(13, 4),
980+
B909(14, 4),
981+
B909(15, 4),
982+
B909(16, 4),
983+
B909(17, 4),
984+
B909(18, 4),
985+
B909(19, 4),
986+
B909(20, 4),
987+
B909(21, 4),
988+
B909(25, 8),
989+
B909(47, 4),
990+
B909(48, 4),
991+
B909(49, 4),
992+
B909(62, 4),
993+
B909(63, 4),
994+
B909(64, 4),
995+
B909(65, 4),
996+
B909(66, 4),
997+
B909(67, 4),
998+
B909(84, 4),
999+
B909(85, 4),
1000+
B909(93, 4),
1001+
B909(94, 4),
1002+
B909(95, 4),
1003+
B909(96, 4),
1004+
B909(97, 4),
1005+
B909(102, 4),
1006+
B909(103, 4),
1007+
B909(104, 4),
1008+
B909(105, 4),
1009+
B909(125, 4),
9931010
]
9941011
self.assertEqual(errors, self.errors(*expected))
9951012

0 commit comments

Comments
 (0)