Skip to content

Commit 6c6a7aa

Browse files
Emit used-before-assignment in final or except blocks where try statements could have failed (#5384)
* Emit `used-before-assignment` in final or except blocks where try statements could have failed Fix #85, #2615
1 parent f8b23eb commit 6c6a7aa

7 files changed

+77
-7
lines changed

ChangeLog

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ Release date: TBA
1616

1717
Closes #4761
1818

19+
* ``used-before-assignment`` now considers that assignments in a try block
20+
may not have occurred when the except or finally blocks are executed.
21+
22+
Closes #85, #2615
23+
1924
* ``used-before-assignment`` now checks names in try blocks.
2025

2126
* Some files in ``pylint.testutils`` were deprecated. In the future imports should be done from the

doc/whatsnew/2.13.rst

+5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ Other Changes
4848

4949
Closes #4761
5050

51+
* ``used-before-assignment`` now considers that assignments in a try block
52+
may not have occurred when the except or finally blocks are executed.
53+
54+
Closes #85, #2615
55+
5156
* ``used-before-assignment`` now checks names in try blocks.
5257

5358
* Require Python ``3.6.2`` to run pylint.

pylint/checkers/variables.py

+47-7
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,10 @@ def _has_locals_call_after_node(stmt, scope):
405405
"E0601": (
406406
"Using variable %r before assignment",
407407
"used-before-assignment",
408-
"Used when a local variable is accessed before its assignment.",
408+
"Emitted when a local variable is accessed before its assignment took place. "
409+
"Assignments in try blocks are assumed not to have occurred when evaluating "
410+
"associated except/finally blocks. Assignments in except blocks are assumed "
411+
"not to have occurred when evaluating statements outside the block.",
409412
),
410413
"E0602": (
411414
"Undefined variable %r",
@@ -580,12 +583,10 @@ def consumed(self):
580583
def consumed_uncertain(self) -> DefaultDict[str, List[nodes.NodeNG]]:
581584
"""
582585
Retrieves nodes filtered out by get_next_to_consume() that may not
583-
have executed, such as statements in except blocks. Checkers that
584-
want to treat the statements as executed (e.g. for unused-variable)
585-
may need to add them back.
586-
587-
TODO: A pending PR will extend this to nodes in try blocks when
588-
evaluating their corresponding except and finally blocks.
586+
have executed, such as statements in except blocks, or statements
587+
in try blocks (when evaluating their corresponding except and finally
588+
blocks). Checkers that want to treat the statements as executed
589+
(e.g. for unused-variable) may need to add them back.
589590
"""
590591
return self._atomic.consumed_uncertain
591592

@@ -617,6 +618,7 @@ def get_next_to_consume(self, node):
617618
name = node.name
618619
parent_node = node.parent
619620
found_nodes = self.to_consume.get(name)
621+
node_statement = node.statement(future=True)
620622
if (
621623
found_nodes
622624
and isinstance(parent_node, nodes.Assign)
@@ -662,6 +664,44 @@ def get_next_to_consume(self, node):
662664
self.consumed_uncertain[node.name] += difference
663665
found_nodes = filtered_nodes
664666

667+
# If this node is in a Finally block of a Try/Finally,
668+
# filter out assignments in the try portion, assuming they may fail
669+
if (
670+
found_nodes
671+
and isinstance(node_statement.parent, nodes.TryFinally)
672+
and node_statement in node_statement.parent.finalbody
673+
):
674+
filtered_nodes = [
675+
n
676+
for n in found_nodes
677+
if not (
678+
n.statement(future=True).parent is node_statement.parent
679+
and n.statement(future=True) in n.statement(future=True).parent.body
680+
)
681+
]
682+
filtered_nodes_set = set(filtered_nodes)
683+
difference = [n for n in found_nodes if n not in filtered_nodes_set]
684+
self.consumed_uncertain[node.name] += difference
685+
found_nodes = filtered_nodes
686+
687+
# If this node is in an ExceptHandler,
688+
# filter out assignments in the try portion, assuming they may fail
689+
if found_nodes and isinstance(node_statement.parent, nodes.ExceptHandler):
690+
filtered_nodes = [
691+
n
692+
for n in found_nodes
693+
if not (
694+
isinstance(n.statement(future=True).parent, nodes.TryExcept)
695+
and n.statement(future=True) in n.statement(future=True).parent.body
696+
and node_statement.parent
697+
in n.statement(future=True).parent.handlers
698+
)
699+
]
700+
filtered_nodes_set = set(filtered_nodes)
701+
difference = [n for n in found_nodes if n not in filtered_nodes_set]
702+
self.consumed_uncertain[node.name] += difference
703+
found_nodes = filtered_nodes
704+
665705
return found_nodes
666706

667707

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""https://github.com/PyCQA/pylint/issues/2615"""
2+
def main():
3+
"""When evaluating except blocks, assume try statements fail."""
4+
try:
5+
res = 1 / 0
6+
res = 42
7+
except ZeroDivisionError:
8+
print(res) # [used-before-assignment]
9+
print(res)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
used-before-assignment:8:14:8:17:main:Using variable 'res' before assignment:UNDEFINED
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""https://github.com/PyCQA/pylint/issues/85"""
2+
def main():
3+
"""When evaluating finally blocks, assume try statements fail."""
4+
try:
5+
res = 1 / 0
6+
res = 42
7+
finally:
8+
print(res) # [used-before-assignment]
9+
print(res)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
used-before-assignment:8:14:8:17:main:Using variable 'res' before assignment:UNDEFINED

0 commit comments

Comments
 (0)