Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions bugbear.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ def visit_For(self, node: ast.For) -> None:
self.check_for_b023(node)
self.check_for_b031(node)
self.check_for_b909(node)
self.check_for_b913(node)
self.generic_visit(node)

def visit_AsyncFor(self, node: ast.AsyncFor) -> None:
Expand Down Expand Up @@ -1929,6 +1930,42 @@ def check_for_b911(self, node: ast.Call) -> None:
) and not any(kw.arg == "strict" for kw in node.keywords):
self.add_error("B911", node)

def check_for_b913(self, node: ast.For) -> None:
# validate node shape
if (
not isinstance(node.target, (ast.Tuple, ast.List))
or not isinstance(node.iter, ast.Call)
or not isinstance(node.iter.func, ast.Name)
or node.iter.func.id != "zip"
or len(node.target.elts) == 0
or len(node.iter.args) == 0
or any(isinstance(elt, ast.Starred) for elt in node.target.elts)
):
return
target_elts = node.target.elts
call = node.iter
# count trailing underscore names
trailing_underscore = 0
for elt in reversed(target_elts):
if isinstance(elt, ast.Name) and elt.id == "_":
trailing_underscore += 1
else:
break
if trailing_underscore == 0 or len(target_elts) - trailing_underscore < 2:
return
# check that underscore names are not used in the loop body
body_finder = NameFinder()
for stmt in node.body + node.orelse:
body_finder.visit(stmt)
for i in range(1, trailing_underscore + 1):
name_node = target_elts[-i]
assert isinstance(name_node, ast.Name)
if name_node.id in body_finder.names:
return
if len(call.args) != len(target_elts):
return
self.add_error("B913", node)
Comment on lines +1965 to +1967


def compose_call_path(node: ast.expr) -> Iterator[str]:
if isinstance(node, ast.Attribute):
Expand Down Expand Up @@ -2583,6 +2620,13 @@ def __call__(self, lineno: int, col: int, vars: tuple[object, ...] = ()) -> erro
message="B911 `itertools.batched()` without an explicit `strict=` parameter."
),
"B912": Error(message="B912 `map()` without an explicit `strict=` parameter."),
"B913": Error(
message=(
"B913 Using `zip` with iterables whose values are immediately "
"discarded via `_` in the assignment. Remove the discarded "
"variables and the matching `zip()` arguments."
)
),
"B950": Error(message="B950 line too long ({} > {} characters)"),
}

Expand All @@ -2599,5 +2643,6 @@ def __call__(self, lineno: int, col: int, vars: tuple[object, ...] = ()) -> erro
"B910",
"B911",
"B912",
"B913",
"B950",
]