Skip to content

Commit f45bf09

Browse files
Fix FP used-before-assignment for statements guarded under same test (#8581)
1 parent 4a485e2 commit f45bf09

File tree

4 files changed

+89
-1
lines changed

4 files changed

+89
-1
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix false positive for ``used-before-assignment`` when usage and assignment
2+
are guarded by the same test in different statements.
3+
4+
Closes #8167

pylint/checkers/variables.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,9 @@ def _uncertain_nodes_in_false_tests(
811811
continue
812812

813813
outer_if = all_if[-1]
814+
if NamesConsumer._node_guarded_by_same_test(node, outer_if):
815+
continue
816+
814817
# Name defined in the if/else control flow
815818
if NamesConsumer._inferred_to_define_name_raise_or_return(name, outer_if):
816819
continue
@@ -819,6 +822,38 @@ def _uncertain_nodes_in_false_tests(
819822

820823
return uncertain_nodes
821824

825+
@staticmethod
826+
def _node_guarded_by_same_test(node: nodes.NodeNG, other_if: nodes.If) -> bool:
827+
"""Identify if `node` is guarded by an equivalent test as `other_if`.
828+
829+
Two tests are equivalent if their string representations are identical
830+
or if their inferred values consist only of constants and those constants
831+
are identical, and the if test guarding `node` is not a Name.
832+
"""
833+
other_if_test_as_string = other_if.test.as_string()
834+
other_if_test_all_inferred = utils.infer_all(other_if.test)
835+
for ancestor in node.node_ancestors():
836+
if not isinstance(ancestor, nodes.If):
837+
continue
838+
if ancestor.test.as_string() == other_if_test_as_string:
839+
return True
840+
if isinstance(ancestor.test, nodes.Name):
841+
continue
842+
all_inferred = utils.infer_all(ancestor.test)
843+
if len(all_inferred) == len(other_if_test_all_inferred):
844+
if any(
845+
not isinstance(test, nodes.Const)
846+
for test in (*all_inferred, *other_if_test_all_inferred)
847+
):
848+
continue
849+
if {test.value for test in all_inferred} != {
850+
test.value for test in other_if_test_all_inferred
851+
}:
852+
continue
853+
return True
854+
855+
return False
856+
822857
@staticmethod
823858
def _uncertain_nodes_in_except_blocks(
824859
found_nodes: list[nodes.NodeNG],

tests/functional/u/used/used_before_assignment.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Miscellaneous used-before-assignment cases"""
22
# pylint: disable=consider-using-f-string, missing-function-docstring
3-
3+
import datetime
44

55
MSG = "hello %s" % MSG # [used-before-assignment]
66

@@ -116,3 +116,50 @@ def turn_on2(**kwargs):
116116
var, *args = (1, "restore_dimmer_state")
117117

118118
print(var, *args)
119+
120+
121+
# Variables guarded by the same test when used.
122+
123+
# Always false
124+
if __name__ == "__main__":
125+
PERCENT = 20
126+
SALE = True
127+
128+
if __name__ == "__main__":
129+
print(PERCENT)
130+
131+
# Different test
132+
if __name__ is None:
133+
print(SALE) # [used-before-assignment]
134+
135+
136+
# Ambiguous, but same test
137+
if not datetime.date.today():
138+
WAS_TODAY = True
139+
140+
if not datetime.date.today():
141+
print(WAS_TODAY)
142+
143+
144+
# Different tests but same inferred values
145+
# Need falsy values here
146+
def give_me_zero():
147+
return 0
148+
149+
def give_me_nothing():
150+
return 0
151+
152+
if give_me_zero():
153+
WE_HAVE_ZERO = True
154+
ALL_DONE = True
155+
156+
if give_me_nothing():
157+
print(WE_HAVE_ZERO)
158+
159+
160+
# Different tests, different values
161+
def give_me_none():
162+
return None
163+
164+
if give_me_none():
165+
print(ALL_DONE) # [used-before-assignment]

tests/functional/u/used/used_before_assignment.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ used-before-assignment:34:3:34:7::Using variable 'VAR2' before assignment:CONTRO
66
used-before-assignment:52:3:52:7::Using variable 'VAR4' before assignment:CONTROL_FLOW
77
used-before-assignment:67:3:67:7::Using variable 'VAR6' before assignment:CONTROL_FLOW
88
used-before-assignment:102:6:102:11::Using variable 'VAR10' before assignment:CONTROL_FLOW
9+
used-before-assignment:133:10:133:14::Using variable 'SALE' before assignment:CONTROL_FLOW
10+
used-before-assignment:165:10:165:18::Using variable 'ALL_DONE' before assignment:CONTROL_FLOW

0 commit comments

Comments
 (0)