Skip to content

Commit 3b68aa7

Browse files
Fix false negative for used-before-assignment when an Except intervenes between Try/Finally (#5583)
Co-authored-by: Pierre Sassoulas <[email protected]>
1 parent 51cee69 commit 3b68aa7

File tree

7 files changed

+198
-17
lines changed

7 files changed

+198
-17
lines changed

ChangeLog

+4
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ Release date: TBA
8787

8888
Closes #4045
8989

90+
* Fixed false negative for ``used-before-assignment`` in finally blocks
91+
if an except handler did not define the assignment that might have failed
92+
in the try block.
93+
9094
* Fixed extremely long processing of long lines with comma's.
9195

9296
Closes #5483

doc/whatsnew/2.13.rst

+4
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ Other Changes
150150

151151
Closes #4045
152152

153+
* Fixed false negative for ``used-before-assignment`` in finally blocks
154+
if an except handler did not define the assignment that might have failed
155+
in the try block.
156+
153157
* Fix a false positive for ``assigning-non-slot`` when the slotted class
154158
defined ``__setattr__``.
155159

pylint/checkers/variables.py

+61-16
Original file line numberDiff line numberDiff line change
@@ -667,23 +667,15 @@ def get_next_to_consume(self, node: nodes.Name) -> Optional[List[nodes.NodeNG]]:
667667

668668
# If this node is in a Finally block of a Try/Finally,
669669
# filter out assignments in the try portion, assuming they may fail
670-
if (
671-
found_nodes
672-
and isinstance(node_statement.parent, nodes.TryFinally)
673-
and node_statement in node_statement.parent.finalbody
674-
):
675-
filtered_nodes = [
676-
n
677-
for n in found_nodes
678-
if not (
679-
n.statement(future=True).parent is node_statement.parent
680-
and n.statement(future=True) in n.statement(future=True).parent.body
670+
if found_nodes:
671+
uncertain_nodes = (
672+
self._uncertain_nodes_in_try_blocks_when_evaluating_finally_blocks(
673+
found_nodes, node_statement
681674
)
682-
]
683-
filtered_nodes_set = set(filtered_nodes)
684-
difference = [n for n in found_nodes if n not in filtered_nodes_set]
685-
self.consumed_uncertain[node.name] += difference
686-
found_nodes = filtered_nodes
675+
)
676+
self.consumed_uncertain[node.name] += uncertain_nodes
677+
uncertain_nodes_set = set(uncertain_nodes)
678+
found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set]
687679

688680
# If this node is in an ExceptHandler,
689681
# filter out assignments in the try portion, assuming they may fail
@@ -794,6 +786,59 @@ def _uncertain_nodes_in_try_blocks_when_evaluating_except_blocks(
794786
uncertain_nodes.append(other_node)
795787
return uncertain_nodes
796788

789+
@staticmethod
790+
def _uncertain_nodes_in_try_blocks_when_evaluating_finally_blocks(
791+
found_nodes: List[nodes.NodeNG], node_statement: nodes.Statement
792+
) -> List[nodes.NodeNG]:
793+
uncertain_nodes: List[nodes.NodeNG] = []
794+
(
795+
closest_try_finally_ancestor,
796+
child_of_closest_try_finally_ancestor,
797+
) = utils.get_node_first_ancestor_of_type_and_its_child(
798+
node_statement, nodes.TryFinally
799+
)
800+
if closest_try_finally_ancestor is None:
801+
return uncertain_nodes
802+
if (
803+
child_of_closest_try_finally_ancestor
804+
not in closest_try_finally_ancestor.finalbody
805+
):
806+
return uncertain_nodes
807+
for other_node in found_nodes:
808+
other_node_statement = other_node.statement(future=True)
809+
(
810+
other_node_try_finally_ancestor,
811+
child_of_other_node_try_finally_ancestor,
812+
) = utils.get_node_first_ancestor_of_type_and_its_child(
813+
other_node_statement, nodes.TryFinally
814+
)
815+
if other_node_try_finally_ancestor is None:
816+
continue
817+
# other_node needs to descend from the try of a try/finally.
818+
if (
819+
child_of_other_node_try_finally_ancestor
820+
not in other_node_try_finally_ancestor.body
821+
):
822+
continue
823+
# If the two try/finally ancestors are not the same, then
824+
# node_statement's closest try/finally ancestor needs to be in
825+
# the final body of other_node's try/finally ancestor, or
826+
# descend from one of the statements in that final body.
827+
if (
828+
other_node_try_finally_ancestor is not closest_try_finally_ancestor
829+
and not any(
830+
other_node_final_statement is closest_try_finally_ancestor
831+
or other_node_final_statement.parent_of(
832+
closest_try_finally_ancestor
833+
)
834+
for other_node_final_statement in other_node_try_finally_ancestor.finalbody
835+
)
836+
):
837+
continue
838+
# Passed all tests for uncertain execution
839+
uncertain_nodes.append(other_node)
840+
return uncertain_nodes
841+
797842

798843
# pylint: disable=too-many-public-methods
799844
class VariablesChecker(BaseChecker):

tests/functional/c/consider/consider_using_with_open.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ def test_defined_in_try_and_finally(self):
137137
except FileNotFoundError:
138138
Path("foo").touch()
139139
finally:
140-
file_handle.open("foo", encoding="utf") # must not trigger
140+
# +1: [used-before-assignment]
141+
file_handle.open("foo", encoding="utf") # must not trigger consider-using-with
141142
with file_handle:
142143
return file_handle.read()
143144

tests/functional/c/consider/consider_using_with_open.txt

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ consider-using-with:46:4:46:33:test_open_outside_assignment:Consider using 'with
44
consider-using-with:47:14:47:43:test_open_outside_assignment:Consider using 'with' for resource-allocating operations:UNDEFINED
55
consider-using-with:52:8:52:37:test_open_inside_with_block:Consider using 'with' for resource-allocating operations:UNDEFINED
66
consider-using-with:120:26:122:13:TestControlFlow.test_triggers_if_reassigned_after_if_else:Consider using 'with' for resource-allocating operations:UNDEFINED
7+
used-before-assignment:141:12:141:23:TestControlFlow.test_defined_in_try_and_finally:Using variable 'file_handle' before assignment:UNDEFINED

tests/functional/u/use/used_before_assignment_issue85.py

+117
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,120 @@ def main():
77
finally:
88
print(res) # [used-before-assignment]
99
print(res)
10+
11+
12+
def try_except_finally():
13+
"""When evaluating finally blocks, assume try statements fail."""
14+
try:
15+
res = 1 / 0
16+
res = 42
17+
except ZeroDivisionError:
18+
print()
19+
finally:
20+
print(res) # [used-before-assignment]
21+
print(res)
22+
23+
24+
def try_except_finally_assignment_in_final_block():
25+
"""Assignment of the name in the final block does not warn."""
26+
try:
27+
res = 1 / 0
28+
res = 42
29+
except ZeroDivisionError:
30+
print()
31+
finally:
32+
res = 999
33+
print(res)
34+
print(res)
35+
36+
37+
def try_except_finally_nested_try_finally_in_try():
38+
"""Don't confuse assignments in different finally statements where
39+
one is nested inside a try.
40+
"""
41+
try:
42+
try:
43+
res = 1 / 0
44+
finally:
45+
print(res) # [used-before-assignment]
46+
print(1 / 0)
47+
except ZeroDivisionError:
48+
print()
49+
finally:
50+
res = 999 # this assignment could be confused for that above
51+
print(res)
52+
print(res)
53+
54+
55+
def try_except_finally_nested_in_finally():
56+
"""Until Pylint comes to a consensus on requiring all except handlers to
57+
define a name, raise, or return (https://github.com/PyCQA/pylint/issues/5524),
58+
Pylint assumes statements in try blocks succeed when accessed *after*
59+
except or finally blocks and fail when accessed *in* except or finally
60+
blocks.)
61+
"""
62+
try:
63+
outer_times = 1
64+
finally:
65+
try:
66+
inner_times = 1
67+
except TypeError:
68+
pass
69+
finally:
70+
print(outer_times) # [used-before-assignment]
71+
print(inner_times) # see docstring: might emit in a future version
72+
73+
74+
def try_except_finally_nested_in_finally_2():
75+
"""Neither name is accessed after a finally block."""
76+
try:
77+
outer_times = 1
78+
finally:
79+
try:
80+
inner_times = 1
81+
except TypeError:
82+
pass
83+
finally:
84+
print(inner_times) # [used-before-assignment]
85+
print(outer_times) # [used-before-assignment]
86+
87+
88+
def try_except_finally_nested_in_finally_3():
89+
"""One name is never accessed after a finally block, but just emit
90+
once per name.
91+
"""
92+
try:
93+
outer_times = 1
94+
finally:
95+
try:
96+
inner_times = 1
97+
except TypeError:
98+
pass
99+
finally:
100+
print(inner_times) # [used-before-assignment]
101+
print(outer_times) # [used-before-assignment]
102+
print(inner_times)
103+
# used-before-assignment is only raised once per name
104+
print(outer_times)
105+
106+
107+
def try_except_finally_nested_in_finally_4():
108+
"""Triple nesting: don't assume direct parentages of outer try/finally
109+
and inner try/finally.
110+
"""
111+
try:
112+
outer_times = 1
113+
finally:
114+
try:
115+
pass
116+
finally:
117+
try:
118+
inner_times = 1
119+
except TypeError:
120+
pass
121+
finally:
122+
print(inner_times) # [used-before-assignment]
123+
print(outer_times) # [used-before-assignment]
124+
print(inner_times)
125+
# used-before-assignment is only raised once per name
126+
print(outer_times)
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
11
used-before-assignment:8:14:8:17:main:Using variable 'res' before assignment:UNDEFINED
2+
used-before-assignment:20:14:20:17:try_except_finally:Using variable 'res' before assignment:UNDEFINED
3+
used-before-assignment:45:18:45:21:try_except_finally_nested_try_finally_in_try:Using variable 'res' before assignment:UNDEFINED
4+
used-before-assignment:70:18:70:29:try_except_finally_nested_in_finally:Using variable 'outer_times' before assignment:UNDEFINED
5+
used-before-assignment:84:18:84:29:try_except_finally_nested_in_finally_2:Using variable 'inner_times' before assignment:UNDEFINED
6+
used-before-assignment:85:14:85:25:try_except_finally_nested_in_finally_2:Using variable 'outer_times' before assignment:UNDEFINED
7+
used-before-assignment:100:18:100:29:try_except_finally_nested_in_finally_3:Using variable 'inner_times' before assignment:UNDEFINED
8+
used-before-assignment:101:18:101:29:try_except_finally_nested_in_finally_3:Using variable 'outer_times' before assignment:UNDEFINED
9+
used-before-assignment:122:22:122:33:try_except_finally_nested_in_finally_4:Using variable 'inner_times' before assignment:UNDEFINED
10+
used-before-assignment:123:22:123:33:try_except_finally_nested_in_finally_4:Using variable 'outer_times' before assignment:UNDEFINED

0 commit comments

Comments
 (0)