Skip to content

Commit e0760c2

Browse files
authored
B035: Fix false positive when named expressions are used (#429) (#430)
1 parent 053c817 commit e0760c2

File tree

2 files changed

+49
-2
lines changed

2 files changed

+49
-2
lines changed

bugbear.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -962,13 +962,18 @@ def _get_names_from_tuple(self, node: ast.Tuple):
962962
elif isinstance(dim, ast.Tuple):
963963
yield from self._get_names_from_tuple(dim)
964964

965-
def _get_dict_comp_loop_var_names(self, node: ast.DictComp):
965+
def _get_dict_comp_loop_and_named_expr_var_names(self, node: ast.DictComp):
966+
finder = NamedExprFinder()
966967
for gen in node.generators:
967968
if isinstance(gen.target, ast.Name):
968969
yield gen.target.id
969970
elif isinstance(gen.target, ast.Tuple):
970971
yield from self._get_names_from_tuple(gen.target)
971972

973+
finder.visit(gen.ifs)
974+
975+
yield from finder.names.keys()
976+
972977
def check_for_b035(self, node: ast.DictComp):
973978
"""Check that a static key isn't used in a dict comprehension.
974979
@@ -980,7 +985,9 @@ def check_for_b035(self, node: ast.DictComp):
980985
B035(node.key.lineno, node.key.col_offset, vars=(node.key.value,))
981986
)
982987
elif isinstance(node.key, ast.Name):
983-
if node.key.id not in self._get_dict_comp_loop_var_names(node):
988+
if node.key.id not in self._get_dict_comp_loop_and_named_expr_var_names(
989+
node
990+
):
984991
self.errors.append(
985992
B035(node.key.lineno, node.key.col_offset, vars=(node.key.id,))
986993
)
@@ -1539,6 +1546,30 @@ def visit(self, node):
15391546
return node
15401547

15411548

1549+
@attr.s
1550+
class NamedExprFinder(ast.NodeVisitor):
1551+
"""Finds names defined through an ast.NamedExpr.
1552+
1553+
After `.visit(node)` is called, `found` is a dict with all name nodes inside,
1554+
key is name string, value is the node (useful for location purposes).
1555+
"""
1556+
1557+
names: Dict[str, List[ast.Name]] = attr.ib(default=attr.Factory(dict))
1558+
1559+
def visit_NamedExpr(self, node: ast.NamedExpr):
1560+
self.names.setdefault(node.target.id, []).append(node.target)
1561+
self.generic_visit(node)
1562+
1563+
def visit(self, node):
1564+
"""Like super-visit but supports iteration over lists."""
1565+
if not isinstance(node, list):
1566+
return super().visit(node)
1567+
1568+
for elem in node:
1569+
super().visit(elem)
1570+
return node
1571+
1572+
15421573
class FuntionDefDefaultsVisitor(ast.NodeVisitor):
15431574
def __init__(self, b008_extend_immutable_calls=None):
15441575
self.b008_extend_immutable_calls = b008_extend_immutable_calls or set()

tests/b035.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,19 @@
3333
# bad - variabe not from generator
3434
v3 = 1
3535
bad_var_not_from_nested_tuple = {v3: k for k, (v1, v2) in {"a": (1, 2)}.items()}
36+
37+
# OK - variable from named expression
38+
var_from_named_expr = {
39+
k: v
40+
for v in {"key": "foo", "data": {}}
41+
if (k := v.get("key")) is not None
42+
}
43+
44+
# nested generators with named expressions
45+
var_from_named_expr_nested = {
46+
k: v
47+
for v in {"keys": [{"key": "foo"}], "data": {}}
48+
if (keys := v.get("keys")) is not None
49+
for item in keys
50+
if (k := item.get("key")) is not None
51+
}

0 commit comments

Comments
 (0)