Skip to content

Commit 38e7fd8

Browse files
authored
add B906: visit_ function with no calls to a visit function (#316)
1 parent 7141009 commit 38e7fd8

File tree

4 files changed

+107
-3
lines changed

4 files changed

+107
-3
lines changed

README.rst

+9
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@ to raise a ``ValueError`` if the arguments are exhausted at differing lengths. T
205205
was added in Python 3.10, so don't enable this flag for code that should work on <3.10.
206206
For more information: https://peps.python.org/pep-0618/
207207

208+
**B906**: ``visit_`` function with no further call to a ``visit`` function. This is often an error, and will stop the visitor from recursing into the subnodes of a visited node. Consider adding a call ``self.generic_visit(node)`` at the end of the function.
209+
Will only trigger on function names where the part after ``visit_`` is a valid ``ast`` type with a non-empty ``_fields`` attribute.
210+
This is meant to be enabled by developers writing visitors using the ``ast`` module, such as flake8 plugin writers.
211+
208212
**B950**: Line too long. This is a pragmatic equivalent of
209213
``pycodestyle``'s ``E501``: it considers "max-line-length" but only triggers
210214
when the value has been exceeded by **more than 10%**. You will no
@@ -302,6 +306,11 @@ MIT
302306
Change Log
303307
----------
304308

309+
Future
310+
~~~~~~~~~
311+
312+
* Add B906: ``visit_`` function with no further calls to a ``visit`` function. (#313)
313+
305314
22.12.6
306315
~~~~~~~~~
307316

bugbear.py

+33-3
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ def visit_FunctionDef(self, node):
413413
self.check_for_b018(node)
414414
self.check_for_b019(node)
415415
self.check_for_b021(node)
416+
self.check_for_b906(node)
416417
self.generic_visit(node)
417418

418419
def visit_ClassDef(self, node):
@@ -969,6 +970,27 @@ def check_for_b905(self, node):
969970
):
970971
self.errors.append(B905(node.lineno, node.col_offset))
971972

973+
def check_for_b906(self, node: ast.FunctionDef):
974+
if not node.name.startswith("visit_"):
975+
return
976+
977+
# extract what's visited, only error if it's a valid ast subclass
978+
# with a non-empty _fields attribute - which is what's iterated over in
979+
# ast.NodeVisitor.generic_visit
980+
class_name = node.name[len("visit_") :]
981+
class_type = getattr(ast, class_name, None)
982+
if class_type is None or not getattr(class_type, "_fields", None):
983+
return
984+
985+
for n in itertools.chain.from_iterable(ast.walk(nn) for nn in node.body):
986+
if isinstance(n, ast.Call) and (
987+
(isinstance(n.func, ast.Attribute) and "visit" in n.func.attr)
988+
or (isinstance(n.func, ast.Name) and "visit" in n.func.id)
989+
):
990+
break
991+
else:
992+
self.errors.append(B906(node.lineno, node.col_offset))
993+
972994

973995
def compose_call_path(node):
974996
if isinstance(node, ast.Attribute):
@@ -990,7 +1012,7 @@ class NameFinder(ast.NodeVisitor):
9901012

9911013
names = attr.ib(default=attr.Factory(dict))
9921014

993-
def visit_Name(self, node):
1015+
def visit_Name(self, node): # noqa: B906 # names don't contain other names
9941016
self.names.setdefault(node.id, []).append(node)
9951017

9961018
def visit(self, node):
@@ -1054,7 +1076,7 @@ def visit_Call(self, node):
10541076
# Check for nested functions.
10551077
self.generic_visit(node)
10561078

1057-
def visit_Lambda(self, node):
1079+
def visit_Lambda(self, node): # noqa: B906
10581080
# Don't recurse into lambda expressions
10591081
# as they are evaluated at call time.
10601082
pass
@@ -1371,6 +1393,14 @@ def visit_Lambda(self, node):
13711393

13721394
B905 = Error(message="B905 `zip()` without an explicit `strict=` parameter.")
13731395

1396+
B906 = Error(
1397+
message=(
1398+
"B906 `visit_` function with no further calls to a visit function, which might"
1399+
" prevent the `ast` visitor from properly visiting all nodes."
1400+
" Consider adding a call to `self.generic_visit(node)`."
1401+
)
1402+
)
1403+
13741404
B950 = Error(message="B950 line too long ({} > {} characters)")
13751405

1376-
disabled_by_default = ["B901", "B902", "B903", "B904", "B905", "B950"]
1406+
disabled_by_default = ["B901", "B902", "B903", "B904", "B905", "B906", "B950"]

tests/b906.py

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import ast
2+
3+
# error if method name starts with `visit_`, the type is a valid `ast` type
4+
# which has subfields, and contains no call to a method name containing `visit`
5+
# anywhere in it's body
6+
7+
# error
8+
def visit_For():
9+
...
10+
11+
12+
# has call to visit function
13+
def visit_For():
14+
foo_visit_bar()
15+
16+
17+
# has call to visit method
18+
def visit_While():
19+
foo.bar_visit_bar()
20+
21+
22+
# this visit call clearly won't run, but is treated as safe
23+
def visit_If():
24+
def foo():
25+
a_visit_function()
26+
27+
28+
# not a valid AST class, no error
29+
def visit_foo():
30+
...
31+
32+
33+
# Break has no subfields to visit, so no error
34+
def visit_Break():
35+
...
36+
37+
38+
# explicitly check `visit` and `generic_visit`
39+
# doesn't start with _visit, safe
40+
def visit():
41+
...
42+
43+
44+
# doesn't start with _visit, safe
45+
def generic_visit():
46+
...
47+
48+
49+
# check no crash on short name
50+
def a():
51+
...
52+
53+
54+
def visit_():
55+
...

tests/test_bugbear.py

+10
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
B903,
4444
B904,
4545
B905,
46+
B906,
4647
B950,
4748
BugBearChecker,
4849
BugBearVisitor,
@@ -501,6 +502,15 @@ def test_b905(self):
501502
]
502503
self.assertEqual(errors, self.errors(*expected))
503504

505+
def test_b906(self):
506+
filename = Path(__file__).absolute().parent / "b906.py"
507+
bbc = BugBearChecker(filename=str(filename))
508+
errors = list(bbc.run())
509+
expected = [
510+
B906(8, 0),
511+
]
512+
self.assertEqual(errors, self.errors(*expected))
513+
504514
def test_b950(self):
505515
filename = Path(__file__).absolute().parent / "b950.py"
506516
bbc = BugBearChecker(filename=str(filename))

0 commit comments

Comments
 (0)