Skip to content

Commit 22b5dc1

Browse files
jacobtylerwallsPierre-SassoulasDanielNoord
committed
Account for more node types in handling of except block homonyms with comprehensions (#6073)
Fixes a false positive for `used-before-assignment`. Co-authored-by: Pierre Sassoulas <[email protected]> Co-authored-by: Daniël van Noord <[email protected]>
1 parent 0599016 commit 22b5dc1

File tree

5 files changed

+116
-69
lines changed

5 files changed

+116
-69
lines changed

ChangeLog

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ What's New in Pylint 2.13.5?
2020
============================
2121
Release date: TBA
2222

23+
* Fix false positive regression in 2.13.0 for ``used-before-assignment`` for
24+
homonyms between variable assignments in try/except blocks and variables in
25+
subscripts in comprehensions.
26+
27+
Closes #6069
28+
2329
* Narrow the scope of the ``unnecessary-ellipsis`` checker to:
2430
* functions & classes which contain both a docstring and an ellipsis.
2531
* A body which contains an ellipsis ``nodes.Expr`` node & at least one other statement.

pylint/checkers/variables.py

+54-63
Original file line numberDiff line numberDiff line change
@@ -623,8 +623,8 @@ def get_next_to_consume(self, node: nodes.Name) -> Optional[List[nodes.NodeNG]]:
623623
):
624624
return found_nodes
625625

626-
# And is not part of a test in a filtered comprehension
627-
if VariablesChecker._has_homonym_in_comprehension_test(node):
626+
# And no comprehension is under the node's frame
627+
if VariablesChecker._comprehension_between_frame_and_node(node):
628628
return found_nodes
629629

630630
# Filter out assignments in ExceptHandlers that node is not contained in
@@ -1276,8 +1276,23 @@ def leave_functiondef(self, node: nodes.FunctionDef) -> None:
12761276

12771277
global_names = _flattened_scope_names(node.nodes_of_class(nodes.Global))
12781278
nonlocal_names = _flattened_scope_names(node.nodes_of_class(nodes.Nonlocal))
1279+
comprehension_target_names: List[str] = []
1280+
1281+
for comprehension_scope in node.nodes_of_class(nodes.ComprehensionScope):
1282+
for generator in comprehension_scope.generators:
1283+
self._find_assigned_names_recursive(
1284+
generator.target, comprehension_target_names
1285+
)
1286+
12791287
for name, stmts in not_consumed.items():
1280-
self._check_is_unused(name, node, stmts[0], global_names, nonlocal_names)
1288+
self._check_is_unused(
1289+
name,
1290+
node,
1291+
stmts[0],
1292+
global_names,
1293+
nonlocal_names,
1294+
comprehension_target_names,
1295+
)
12811296

12821297
visit_asyncfunctiondef = visit_functiondef
12831298
leave_asyncfunctiondef = leave_functiondef
@@ -1405,7 +1420,7 @@ def _undefined_and_used_before_checker(
14051420
continue
14061421

14071422
action, nodes_to_consume = self._check_consumer(
1408-
node, stmt, frame, current_consumer, i, base_scope_type
1423+
node, stmt, frame, current_consumer, base_scope_type
14091424
)
14101425
if nodes_to_consume:
14111426
# Any nodes added to consumed_uncertain by get_next_to_consume()
@@ -1476,37 +1491,37 @@ def _should_node_be_skipped(
14761491

14771492
return False
14781493

1494+
def _find_assigned_names_recursive(
1495+
self,
1496+
target: Union[nodes.AssignName, nodes.BaseContainer],
1497+
target_names: List[str],
1498+
) -> None:
1499+
"""Update `target_names` in place with the names of assignment
1500+
targets, recursively (to account for nested assignments).
1501+
"""
1502+
if isinstance(target, nodes.AssignName):
1503+
target_names.append(target.name)
1504+
elif isinstance(target, nodes.BaseContainer):
1505+
for elt in target.elts:
1506+
self._find_assigned_names_recursive(elt, target_names)
1507+
14791508
# pylint: disable=too-many-return-statements
14801509
def _check_consumer(
14811510
self,
14821511
node: nodes.Name,
14831512
stmt: nodes.NodeNG,
14841513
frame: nodes.LocalsDictNodeNG,
14851514
current_consumer: NamesConsumer,
1486-
consumer_level: int,
14871515
base_scope_type: Any,
14881516
) -> Tuple[VariableVisitConsumerAction, Optional[List[nodes.NodeNG]]]:
14891517
"""Checks a consumer for conditions that should trigger messages."""
14901518
# If the name has already been consumed, only check it's not a loop
14911519
# variable used outside the loop.
1492-
# Avoid the case where there are homonyms inside function scope and
1493-
# comprehension current scope (avoid bug #1731)
14941520
if node.name in current_consumer.consumed:
1495-
if utils.is_func_decorator(current_consumer.node) or not (
1496-
current_consumer.scope_type == "comprehension"
1497-
and self._has_homonym_in_upper_function_scope(node, consumer_level)
1498-
# But don't catch homonyms against the filter of a comprehension,
1499-
# (like "if x" in "[x for x in expr() if x]")
1500-
# https://github.com/PyCQA/pylint/issues/5586
1501-
and not (
1502-
self._has_homonym_in_comprehension_test(node)
1503-
# Or homonyms against values to keyword arguments
1504-
# (like "var" in "[func(arg=var) for var in expr()]")
1505-
or (
1506-
isinstance(node.scope(), nodes.ComprehensionScope)
1507-
and isinstance(node.parent, (nodes.Call, nodes.Keyword))
1508-
)
1509-
)
1521+
# Avoid the case where there are homonyms inside function scope and
1522+
# comprehension current scope (avoid bug #1731)
1523+
if utils.is_func_decorator(current_consumer.node) or not isinstance(
1524+
node, nodes.ComprehensionScope
15101525
):
15111526
self._check_late_binding_closure(node)
15121527
self._loopvar_name(node)
@@ -2259,8 +2274,14 @@ def _loopvar_name(self, node: astroid.Name) -> None:
22592274
self.add_message("undefined-loop-variable", args=node.name, node=node)
22602275

22612276
def _check_is_unused(
2262-
self, name, node, stmt, global_names, nonlocal_names: Iterable[str]
2263-
):
2277+
self,
2278+
name,
2279+
node,
2280+
stmt,
2281+
global_names,
2282+
nonlocal_names: Iterable[str],
2283+
comprehension_target_names: List[str],
2284+
) -> None:
22642285
# Ignore some special names specified by user configuration.
22652286
if self._is_name_ignored(stmt, name):
22662287
return
@@ -2282,7 +2303,8 @@ def _check_is_unused(
22822303
argnames = node.argnames()
22832304
# Care about functions with unknown argument (builtins)
22842305
if name in argnames:
2285-
self._check_unused_arguments(name, node, stmt, argnames, nonlocal_names)
2306+
if name not in comprehension_target_names:
2307+
self._check_unused_arguments(name, node, stmt, argnames, nonlocal_names)
22862308
else:
22872309
if stmt.parent and isinstance(
22882310
stmt.parent, (nodes.Assign, nodes.AnnAssign, nodes.Tuple)
@@ -2455,46 +2477,15 @@ def _should_ignore_redefined_builtin(self, stmt):
24552477
def _allowed_redefined_builtin(self, name):
24562478
return name in self.config.allowed_redefined_builtins
24572479

2458-
def _has_homonym_in_upper_function_scope(
2459-
self, node: nodes.Name, index: int
2460-
) -> bool:
2461-
"""Return whether there is a node with the same name in the
2462-
to_consume dict of an upper scope and if that scope is a function
2463-
2464-
:param node: node to check for
2465-
:param index: index of the current consumer inside self._to_consume
2466-
:return: True if there is a node with the same name in the
2467-
to_consume dict of an upper scope and if that scope
2468-
is a function, False otherwise
2469-
"""
2470-
return any(
2471-
_consumer.scope_type == "function" and node.name in _consumer.to_consume
2472-
for _consumer in self._to_consume[index - 1 :: -1]
2473-
)
2474-
24752480
@staticmethod
2476-
def _has_homonym_in_comprehension_test(node: nodes.Name) -> bool:
2477-
"""Return True if `node`'s frame contains a comprehension employing an
2478-
identical name in a test.
2479-
2480-
The name in the test could appear at varying depths:
2481-
2482-
Examples:
2483-
[x for x in range(3) if name]
2484-
[x for x in range(3) if name.num == 1]
2485-
[x for x in range(3)] if call(name.num)]
2486-
"""
2487-
closest_comprehension = utils.get_node_first_ancestor_of_type(
2488-
node, nodes.Comprehension
2489-
)
2490-
return (
2491-
closest_comprehension is not None
2492-
and node.frame(future=True).parent_of(closest_comprehension)
2493-
and any(
2494-
test is node or test.parent_of(node)
2495-
for test in closest_comprehension.ifs
2496-
)
2481+
def _comprehension_between_frame_and_node(node: nodes.Name) -> bool:
2482+
"""Return True if a ComprehensionScope intervenes between `node` and its frame."""
2483+
closest_comprehension_scope = utils.get_node_first_ancestor_of_type(
2484+
node, nodes.ComprehensionScope
24972485
)
2486+
return closest_comprehension_scope is not None and node.frame(
2487+
future=True
2488+
).parent_of(closest_comprehension_scope)
24982489

24992490
def _store_type_annotation_node(self, type_annotation):
25002491
"""Given a type annotation, store all the name nodes it refers to."""

tests/functional/u/unused/unused_argument.py

+6
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ def metadata_from_dict(key):
4747
"""
4848
return {key: str(value) for key, value in key.items()}
4949

50+
51+
def metadata_from_dict_2(key):
52+
"""Similar, but with more nesting"""
53+
return {key: (a, b) for key, (a, b) in key.items()}
54+
55+
5056
# pylint: disable=too-few-public-methods, misplaced-future,wrong-import-position
5157
from __future__ import print_function
5258

Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
unused-argument:3:16:3:21:test_unused:Unused argument 'first':HIGH
22
unused-argument:3:23:3:29:test_unused:Unused argument 'second':HIGH
33
unused-argument:32:29:32:32:Sub.newmethod:Unused argument 'aay':INFERENCE
4-
unused-argument:54:13:54:16:function:Unused argument 'arg':HIGH
5-
unused-argument:61:21:61:24:AAAA.method:Unused argument 'arg':INFERENCE
6-
unused-argument:68:0:None:None:AAAA.selected:Unused argument 'args':INFERENCE
7-
unused-argument:68:0:None:None:AAAA.selected:Unused argument 'kwargs':INFERENCE
8-
unused-argument:87:23:87:26:BBBB.__init__:Unused argument 'arg':INFERENCE
9-
unused-argument:98:34:98:39:Ancestor.set_thing:Unused argument 'other':INFERENCE
4+
unused-argument:60:13:60:16:function:Unused argument 'arg':HIGH
5+
unused-argument:67:21:67:24:AAAA.method:Unused argument 'arg':INFERENCE
6+
unused-argument:74:0:None:None:AAAA.selected:Unused argument 'args':INFERENCE
7+
unused-argument:74:0:None:None:AAAA.selected:Unused argument 'kwargs':INFERENCE
8+
unused-argument:93:23:93:26:BBBB.__init__:Unused argument 'arg':INFERENCE
9+
unused-argument:104:34:104:39:Ancestor.set_thing:Unused argument 'other':INFERENCE

tests/functional/u/used/used_before_assignment_filtered_comprehension.py renamed to tests/functional/u/used/used_before_assignment_comprehension_homonyms.py

+44
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,47 @@ def func5():
4949
except ZeroDivisionError:
5050
k = None
5151
print(k, filtered)
52+
53+
54+
def func6(data, keys):
55+
"""Similar, but with a subscript in a key-value pair rather than the test
56+
See https://github.com/PyCQA/pylint/issues/6069"""
57+
try:
58+
results = {key: data[key] for key in keys}
59+
except KeyError as exc:
60+
key, *_ = exc.args
61+
raise Exception(f"{key} not found") from exc
62+
63+
return results
64+
65+
66+
def func7():
67+
"""Similar, but with a comparison"""
68+
bools = [str(i) == i for i in range(3)]
69+
70+
try:
71+
1 / 0
72+
except ZeroDivisionError:
73+
i = None
74+
print(i, bools)
75+
76+
77+
def func8():
78+
"""Similar, but with a container"""
79+
pairs = [(i, i) for i in range(3)]
80+
81+
try:
82+
1 / 0
83+
except ZeroDivisionError:
84+
i = None
85+
print(i, pairs)
86+
87+
88+
# Module level cases
89+
90+
module_ints = [j | j for j in range(3)]
91+
try:
92+
1 / 0
93+
except ZeroDivisionError:
94+
j = None
95+
print(j, module_ints)

0 commit comments

Comments
 (0)