Skip to content

Commit dea2e00

Browse files
Add B034: re.sub/subn/split must pass flags/count/maxsplit as keyword arguments (#398)
* Add B034: re.sub/subn/split must pass flags/count/maxsplit as keyword arguments. * remove <3.8 check accidentally added back * Apply suggestions from code review Co-authored-by: Jelle Zijlstra <[email protected]> * update column in testcase * improved wording --------- Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent f1c391a commit dea2e00

File tree

4 files changed

+81
-1
lines changed

4 files changed

+81
-1
lines changed

README.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ second usage. Save the result to a list if the result is needed multiple times.
188188

189189
**B033**: Sets should not contain duplicate items. Duplicate items will be replaced with a single item at runtime.
190190

191+
**B034**: Calls to `re.sub`, `re.subn` or `re.split` should pass `flags` or `count`/`maxsplit` as keyword arguments. It is commonly assumed that `flags` is the third positional parameter, forgetting about `count`/`maxsplit`, since many other `re` module functions are of the form `f(pattern, string, flags)`.
192+
191193
Opinionated warnings
192194
~~~~~~~~~~~~~~~~~~~~
193195

@@ -338,6 +340,7 @@ Unreleased
338340
* Fix a crash and several test failures on Python 3.12, all relating to the B907
339341
check.
340342
* Declare support for Python 3.12.
343+
* Add B034: re.sub/subn/split must pass flags/count/maxsplit as keyword arguments.
341344

342345
23.6.5
343346
~~~~~~

bugbear.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,8 +423,9 @@ def visit_Call(self, node):
423423

424424
self.check_for_b026(node)
425425

426-
self.check_for_b905(node)
427426
self.check_for_b028(node)
427+
self.check_for_b034(node)
428+
self.check_for_b905(node)
428429
self.generic_visit(node)
429430

430431
def visit_Module(self, node):
@@ -1400,6 +1401,27 @@ def check_for_b033(self, node):
14001401
else:
14011402
seen.add(elt.value)
14021403

1404+
def check_for_b034(self, node: ast.Call):
1405+
if not isinstance(node.func, ast.Attribute):
1406+
return
1407+
if not isinstance(node.func.value, ast.Name) or node.func.value.id != "re":
1408+
return
1409+
1410+
def check(num_args, param_name):
1411+
if len(node.args) > num_args:
1412+
self.errors.append(
1413+
B034(
1414+
node.args[num_args].lineno,
1415+
node.args[num_args].col_offset,
1416+
vars=(node.func.attr, param_name),
1417+
)
1418+
)
1419+
1420+
if node.func.attr in ("sub", "subn"):
1421+
check(3, "count")
1422+
elif node.func.attr == "split":
1423+
check(2, "maxsplit")
1424+
14031425

14041426
def compose_call_path(node):
14051427
if isinstance(node, ast.Attribute):
@@ -1804,6 +1826,13 @@ def visit_Lambda(self, node):
18041826
)
18051827
)
18061828

1829+
B034 = Error(
1830+
message=(
1831+
"B034 {} should pass `{}` and `flags` as keyword arguments to avoid confusion"
1832+
" due to unintuitive argument positions."
1833+
)
1834+
)
1835+
18071836
# Warnings disabled by default.
18081837
B901 = Error(
18091838
message=(

tests/b034.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import re
2+
from re import sub
3+
4+
# error
5+
re.sub("a", "b", "aaa", re.IGNORECASE)
6+
re.sub("a", "b", "aaa", 5)
7+
re.sub("a", "b", "aaa", 5, re.IGNORECASE)
8+
re.subn("a", "b", "aaa", re.IGNORECASE)
9+
re.subn("a", "b", "aaa", 5)
10+
re.subn("a", "b", "aaa", 5, re.IGNORECASE)
11+
re.split(" ", "a a a a", re.I)
12+
re.split(" ", "a a a a", 2)
13+
re.split(" ", "a a a a", 2, re.I)
14+
15+
# okay
16+
re.sub("a", "b", "aaa")
17+
re.sub("a", "b", "aaa", flags=re.IGNORECASE)
18+
re.sub("a", "b", "aaa", count=5)
19+
re.sub("a", "b", "aaa", count=5, flags=re.IGNORECASE)
20+
re.subn("a", "b", "aaa")
21+
re.subn("a", "b", "aaa", flags=re.IGNORECASE)
22+
re.subn("a", "b", "aaa", count=5)
23+
re.subn("a", "b", "aaa", count=5, flags=re.IGNORECASE)
24+
re.split(" ", "a a a a", flags=re.I)
25+
re.split(" ", "a a a a", maxsplit=2)
26+
re.split(" ", "a a a a", maxsplit=2, flags=re.I)
27+
28+
29+
# not covered
30+
sub("a", "b", "aaa", re.IGNORECASE)

tests/test_bugbear.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
B031,
4343
B032,
4444
B033,
45+
B034,
4546
B901,
4647
B902,
4748
B903,
@@ -502,6 +503,23 @@ def test_b033(self):
502503
)
503504
self.assertEqual(errors, expected)
504505

506+
def test_b034(self):
507+
filename = Path(__file__).absolute().parent / "b034.py"
508+
bbc = BugBearChecker(filename=str(filename))
509+
errors = list(bbc.run())
510+
expected = self.errors(
511+
B034(5, 24, vars=("sub", "count")),
512+
B034(6, 24, vars=("sub", "count")),
513+
B034(7, 24, vars=("sub", "count")),
514+
B034(8, 25, vars=("subn", "count")),
515+
B034(9, 25, vars=("subn", "count")),
516+
B034(10, 25, vars=("subn", "count")),
517+
B034(11, 25, vars=("split", "maxsplit")),
518+
B034(12, 25, vars=("split", "maxsplit")),
519+
B034(13, 25, vars=("split", "maxsplit")),
520+
)
521+
self.assertEqual(errors, expected)
522+
505523
def test_b908(self):
506524
filename = Path(__file__).absolute().parent / "b908.py"
507525
bbc = BugBearChecker(filename=str(filename))

0 commit comments

Comments
 (0)