Skip to content

Commit 54a3d7f

Browse files
Emit used-before-assignment for self-referencing assignments under if conditions (#6958)
1 parent 1242e91 commit 54a3d7f

File tree

7 files changed

+66
-17
lines changed

7 files changed

+66
-17
lines changed

doc/whatsnew/2/2.15/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ False negatives fixed
4646

4747
Closes #5653
4848

49+
* Emit ``used-before-assignment`` for self-referencing assignments under if conditions.
50+
51+
Closes #6643
52+
4953

5054
Other bug fixes
5155
===============

pylint/checkers/utils.py

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -288,26 +288,32 @@ def is_defined_in_scope(
288288
varname: str,
289289
scope: nodes.NodeNG,
290290
) -> bool:
291+
return defnode_in_scope(var_node, varname, scope) is not None
292+
293+
294+
def defnode_in_scope(
295+
var_node: nodes.NodeNG,
296+
varname: str,
297+
scope: nodes.NodeNG,
298+
) -> nodes.NodeNG | None:
291299
if isinstance(scope, nodes.If):
292300
for node in scope.body:
293-
if (
294-
isinstance(node, nodes.Assign)
295-
and any(
296-
isinstance(target, nodes.AssignName) and target.name == varname
297-
for target in node.targets
298-
)
299-
) or (isinstance(node, nodes.Nonlocal) and varname in node.names):
300-
return True
301+
if isinstance(node, nodes.Nonlocal) and varname in node.names:
302+
return node
303+
if isinstance(node, nodes.Assign):
304+
for target in node.targets:
305+
if isinstance(target, nodes.AssignName) and target.name == varname:
306+
return target
301307
elif isinstance(scope, (COMP_NODE_TYPES, nodes.For)):
302308
for ass_node in scope.nodes_of_class(nodes.AssignName):
303309
if ass_node.name == varname:
304-
return True
310+
return ass_node
305311
elif isinstance(scope, nodes.With):
306312
for expr, ids in scope.items:
307313
if expr.parent_of(var_node):
308314
break
309315
if ids and isinstance(ids, nodes.AssignName) and ids.name == varname:
310-
return True
316+
return ids
311317
elif isinstance(scope, (nodes.Lambda, nodes.FunctionDef)):
312318
if scope.args.is_argument(varname):
313319
# If the name is found inside a default value
@@ -317,32 +323,57 @@ def is_defined_in_scope(
317323
try:
318324
scope.args.default_value(varname)
319325
scope = scope.parent
320-
is_defined_in_scope(var_node, varname, scope)
326+
defnode = defnode_in_scope(var_node, varname, scope)
321327
except astroid.NoDefault:
322328
pass
323-
return True
329+
else:
330+
return defnode
331+
return scope
324332
if getattr(scope, "name", None) == varname:
325-
return True
333+
return scope
326334
elif isinstance(scope, nodes.ExceptHandler):
327335
if isinstance(scope.name, nodes.AssignName):
328336
ass_node = scope.name
329337
if ass_node.name == varname:
330-
return True
331-
return False
338+
return ass_node
339+
return None
332340

333341

334342
def is_defined_before(var_node: nodes.Name) -> bool:
335343
"""Check if the given variable node is defined before.
336344
337345
Verify that the variable node is defined by a parent node
346+
(e.g. if or with) earlier than `var_node`, or is defined by a
338347
(list, set, dict, or generator comprehension, lambda)
339348
or in a previous sibling node on the same line
340349
(statement_defining ; statement_using).
341350
"""
342351
varname = var_node.name
343352
for parent in var_node.node_ancestors():
344-
if is_defined_in_scope(var_node, varname, parent):
353+
defnode = defnode_in_scope(var_node, varname, parent)
354+
if defnode is None:
355+
continue
356+
defnode_scope = defnode.scope()
357+
if isinstance(defnode_scope, COMP_NODE_TYPES + (nodes.Lambda,)):
358+
return True
359+
if defnode.lineno < var_node.lineno:
345360
return True
361+
# `defnode` and `var_node` on the same line
362+
for defnode_anc in defnode.node_ancestors():
363+
if defnode_anc.lineno != var_node.lineno:
364+
continue
365+
if isinstance(
366+
defnode_anc,
367+
(
368+
nodes.For,
369+
nodes.While,
370+
nodes.With,
371+
nodes.TryExcept,
372+
nodes.TryFinally,
373+
nodes.ExceptHandler,
374+
),
375+
):
376+
return True
346377
# possibly multiple statements on the same line using semicolon separator
347378
stmt = var_node.statement(future=True)
348379
_node = stmt.previous_sibling()

pylint/checkers/variables.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1980,7 +1980,7 @@ def _is_variable_violation(
19801980
# (b := b)
19811981
# Otherwise, safe if used after assignment:
19821982
# (b := 2) and b
1983-
maybe_before_assign = any(
1983+
maybe_before_assign = defnode.value is node or any(
19841984
anc is defnode.value for anc in node.node_ancestors()
19851985
)
19861986

tests/functional/u/undefined/undefined_variable_py38.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,8 @@ def expression_in_ternary_operator_inside_container_tuple():
175175
def expression_in_ternary_operator_inside_container_wrong_position():
176176
"""2-element list where named expression comes too late"""
177177
return [val3, val3 if (val3 := 'something') else 'anything'] # [used-before-assignment]
178+
179+
180+
# Self-referencing
181+
if (z := z): # [used-before-assignment]
182+
z = z + 1

tests/functional/u/undefined/undefined_variable_py38.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ undefined-variable:105:6:105:19::Undefined variable 'else_assign_2':INFERENCE
77
used-before-assignment:140:10:140:16:type_annotation_used_improperly_after_comprehension:Using variable 'my_int' before assignment:HIGH
88
used-before-assignment:147:10:147:16:type_annotation_used_improperly_after_comprehension_2:Using variable 'my_int' before assignment:HIGH
99
used-before-assignment:177:12:177:16:expression_in_ternary_operator_inside_container_wrong_position:Using variable 'val3' before assignment:HIGH
10+
used-before-assignment:181:9:181:10::Using variable 'z' before assignment:HIGH
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""used-before-assignment cases involving IF conditions"""
2+
if 1 + 1 == 2:
3+
x = x + 1 # [used-before-assignment]
4+
5+
if y: # [used-before-assignment]
6+
y = y + 1
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
used-before-assignment:3:8:3:9::Using variable 'x' before assignment:HIGH
2+
used-before-assignment:5:3:5:4::Using variable 'y' before assignment:HIGH

0 commit comments

Comments
 (0)