Skip to content

Commit 67bfab4

Browse files
Emit possibly-used-before-assignment after if/else switches (#8952)
Co-authored-by: Pierre Sassoulas <[email protected]>
1 parent 73e82bd commit 67bfab4

23 files changed

+156
-73
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def check_lunchbox(items: list[str]):
2+
if not items:
3+
empty = True
4+
print(empty) # [possibly-used-before-assignment]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
If you rely on a pattern like:
2+
3+
.. sourcecode:: python
4+
5+
if guarded():
6+
var = 1
7+
8+
if guarded():
9+
print(var) # emits possibly-used-before-assignment
10+
11+
you may be concerned that ``possibly-used-before-assignment`` is not totally useful
12+
in this instance. However, consider that pylint, as a static analysis tool, does
13+
not know if ``guarded()`` is deterministic or talks to
14+
a database. (Likewise, for ``guarded`` instead of ``guarded()``, any other
15+
part of your program may have changed its value in the meantime.)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def check_lunchbox(items: list[str]):
2+
empty = False
3+
if not items:
4+
empty = True
5+
print(empty)

doc/user_guide/checkers/features.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,6 +1374,9 @@ Variables checker Messages
13741374
Used when an invalid (non-string) object occurs in __all__.
13751375
:no-name-in-module (E0611): *No name %r in module %r*
13761376
Used when a name cannot be found in a module.
1377+
:possibly-used-before-assignment (E0606): *Possibly using variable %r before assignment*
1378+
Emitted when a local variable is accessed before its assignment took place in
1379+
both branches of an if/else switch.
13771380
:undefined-variable (E0602): *Undefined variable %r*
13781381
Used when an undefined variable is accessed.
13791382
:undefined-all-variable (E0603): *Undefined variable name %r in __all__*

doc/user_guide/messages/messages_overview.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ All messages in the error category:
139139
error/not-in-loop
140140
error/notimplemented-raised
141141
error/positional-only-arguments-expected
142+
error/possibly-used-before-assignment
142143
error/potential-index-error
143144
error/raising-bad-type
144145
error/raising-non-exception

doc/whatsnew/fragments/1727.new_check

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add check ``possibly-used-before-assignment`` when relying on names after an ``if/else``
2+
switch when one branch failed to define the name, raise, or return.
3+
4+
Closes #1727

pylint/checkers/variables.py

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,12 @@ def _has_locals_call_after_node(stmt: nodes.NodeNG, scope: nodes.FunctionDef) ->
403403
"invalid-all-format",
404404
"Used when __all__ has an invalid format.",
405405
),
406+
"E0606": (
407+
"Possibly using variable %r before assignment",
408+
"possibly-used-before-assignment",
409+
"Emitted when a local variable is accessed before its assignment took place "
410+
"in both branches of an if/else switch.",
411+
),
406412
"E0611": (
407413
"No name %r in module %r",
408414
"no-name-in-module",
@@ -537,6 +543,8 @@ def __init__(self, node: nodes.NodeNG, scope_type: str) -> None:
537543
copy.copy(node.locals), {}, collections.defaultdict(list), scope_type
538544
)
539545
self.node = node
546+
self.names_under_always_false_test: set[str] = set()
547+
self.names_defined_under_one_branch_only: set[str] = set()
540548

541549
def __repr__(self) -> str:
542550
_to_consumes = [f"{k}->{v}" for k, v in self._atomic.to_consume.items()]
@@ -636,13 +644,6 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None:
636644
if VariablesChecker._comprehension_between_frame_and_node(node):
637645
return found_nodes
638646

639-
# Filter out assignments guarded by always false conditions
640-
if found_nodes:
641-
uncertain_nodes = self._uncertain_nodes_in_false_tests(found_nodes, node)
642-
self.consumed_uncertain[node.name] += uncertain_nodes
643-
uncertain_nodes_set = set(uncertain_nodes)
644-
found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set]
645-
646647
# Filter out assignments in ExceptHandlers that node is not contained in
647648
if found_nodes:
648649
found_nodes = [
@@ -652,6 +653,13 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None:
652653
or n.statement().parent_of(node)
653654
]
654655

656+
# Filter out assignments guarded by always false conditions
657+
if found_nodes:
658+
uncertain_nodes = self._uncertain_nodes_if_tests(found_nodes, node)
659+
self.consumed_uncertain[node.name] += uncertain_nodes
660+
uncertain_nodes_set = set(uncertain_nodes)
661+
found_nodes = [n for n in found_nodes if n not in uncertain_nodes_set]
662+
655663
# Filter out assignments in an Except clause that the node is not
656664
# contained in, assuming they may fail
657665
if found_nodes:
@@ -688,8 +696,9 @@ def get_next_to_consume(self, node: nodes.Name) -> list[nodes.NodeNG] | None:
688696

689697
return found_nodes
690698

691-
@staticmethod
692-
def _inferred_to_define_name_raise_or_return(name: str, node: nodes.NodeNG) -> bool:
699+
def _inferred_to_define_name_raise_or_return(
700+
self, name: str, node: nodes.NodeNG
701+
) -> bool:
693702
"""Return True if there is a path under this `if_node`
694703
that is inferred to define `name`, raise, or return.
695704
"""
@@ -716,8 +725,8 @@ def _inferred_to_define_name_raise_or_return(name: str, node: nodes.NodeNG) -> b
716725
if not isinstance(node, nodes.If):
717726
return False
718727

719-
# Be permissive if there is a break
720-
if any(node.nodes_of_class(nodes.Break)):
728+
# Be permissive if there is a break or a continue
729+
if any(node.nodes_of_class(nodes.Break, nodes.Continue)):
721730
return True
722731

723732
# Is there an assignment in this node itself, e.g. in named expression?
@@ -739,17 +748,18 @@ def _inferred_to_define_name_raise_or_return(name: str, node: nodes.NodeNG) -> b
739748

740749
# Only search else branch when test condition is inferred to be false
741750
if all_inferred and only_search_else:
742-
return NamesConsumer._branch_handles_name(name, node.orelse)
743-
# Only search if branch when test condition is inferred to be true
744-
if all_inferred and only_search_if:
745-
return NamesConsumer._branch_handles_name(name, node.body)
751+
self.names_under_always_false_test.add(name)
752+
return self._branch_handles_name(name, node.orelse)
746753
# Search both if and else branches
747-
return NamesConsumer._branch_handles_name(
748-
name, node.body
749-
) or NamesConsumer._branch_handles_name(name, node.orelse)
750-
751-
@staticmethod
752-
def _branch_handles_name(name: str, body: Iterable[nodes.NodeNG]) -> bool:
754+
if_branch_handles = self._branch_handles_name(name, node.body)
755+
else_branch_handles = self._branch_handles_name(name, node.orelse)
756+
if if_branch_handles ^ else_branch_handles:
757+
self.names_defined_under_one_branch_only.add(name)
758+
elif name in self.names_defined_under_one_branch_only:
759+
self.names_defined_under_one_branch_only.remove(name)
760+
return if_branch_handles and else_branch_handles
761+
762+
def _branch_handles_name(self, name: str, body: Iterable[nodes.NodeNG]) -> bool:
753763
return any(
754764
NamesConsumer._defines_name_raises_or_returns(name, if_body_stmt)
755765
or isinstance(
@@ -762,17 +772,15 @@ def _branch_handles_name(name: str, body: Iterable[nodes.NodeNG]) -> bool:
762772
nodes.While,
763773
),
764774
)
765-
and NamesConsumer._inferred_to_define_name_raise_or_return(
766-
name, if_body_stmt
767-
)
775+
and self._inferred_to_define_name_raise_or_return(name, if_body_stmt)
768776
for if_body_stmt in body
769777
)
770778

771-
def _uncertain_nodes_in_false_tests(
779+
def _uncertain_nodes_if_tests(
772780
self, found_nodes: list[nodes.NodeNG], node: nodes.NodeNG
773781
) -> list[nodes.NodeNG]:
774-
"""Identify nodes of uncertain execution because they are defined under
775-
tests that evaluate false.
782+
"""Identify nodes of uncertain execution because they are defined under if
783+
tests.
776784
777785
Don't identify a node if there is a path that is inferred to
778786
define the name, raise, or return (e.g. any executed if/elif/else branch).
@@ -808,7 +816,7 @@ def _uncertain_nodes_in_false_tests(
808816
continue
809817

810818
# Name defined in the if/else control flow
811-
if NamesConsumer._inferred_to_define_name_raise_or_return(name, outer_if):
819+
if self._inferred_to_define_name_raise_or_return(name, outer_if):
812820
continue
813821

814822
uncertain_nodes.append(other_node)
@@ -930,7 +938,7 @@ def _uncertain_nodes_in_except_blocks(
930938

931939
@staticmethod
932940
def _defines_name_raises_or_returns(name: str, node: nodes.NodeNG) -> bool:
933-
if isinstance(node, (nodes.Raise, nodes.Assert, nodes.Return)):
941+
if isinstance(node, (nodes.Raise, nodes.Assert, nodes.Return, nodes.Continue)):
934942
return True
935943
if (
936944
isinstance(node, nodes.AnnAssign)
@@ -1993,11 +2001,19 @@ def _report_unfound_name_definition(
19932001
):
19942002
return
19952003

1996-
confidence = (
1997-
CONTROL_FLOW if node.name in current_consumer.consumed_uncertain else HIGH
1998-
)
2004+
confidence = HIGH
2005+
if node.name in current_consumer.names_under_always_false_test:
2006+
confidence = INFERENCE
2007+
elif node.name in current_consumer.consumed_uncertain:
2008+
confidence = CONTROL_FLOW
2009+
2010+
if node.name in current_consumer.names_defined_under_one_branch_only:
2011+
msg = "possibly-used-before-assignment"
2012+
else:
2013+
msg = "used-before-assignment"
2014+
19992015
self.add_message(
2000-
"used-before-assignment",
2016+
msg,
20012017
args=node.name,
20022018
node=node,
20032019
confidence=confidence,

pylintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ disable=
109109
# We anticipate #3512 where it will become optional
110110
fixme,
111111
consider-using-assignment-expr,
112+
possibly-used-before-assignment,
112113

113114

114115
[REPORTS]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
redefined-outer-name:11:4:12:12::Redefining name 'err' from outer scope (line 8):UNDEFINED
22
redefined-outer-name:57:8:58:16::Redefining name 'err' from outer scope (line 51):UNDEFINED
3-
used-before-assignment:69:14:69:29:func:Using variable 'CustomException' before assignment:CONTROL_FLOW
3+
used-before-assignment:69:14:69:29:func:Using variable 'CustomException' before assignment:HIGH
44
redefined-outer-name:71:4:72:12:func:Redefining name 'CustomException' from outer scope (line 62):UNDEFINED

tests/functional/u/undefined/undefined_variable.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ undefined-variable:166:4:166:13::Undefined variable 'unicode_2':UNDEFINED
2727
undefined-variable:171:4:171:13::Undefined variable 'unicode_3':UNDEFINED
2828
undefined-variable:226:25:226:37:LambdaClass4.<lambda>:Undefined variable 'LambdaClass4':UNDEFINED
2929
undefined-variable:234:25:234:37:LambdaClass5.<lambda>:Undefined variable 'LambdaClass5':UNDEFINED
30-
used-before-assignment:255:26:255:34:func_should_fail:Using variable 'datetime' before assignment:CONTROL_FLOW
30+
used-before-assignment:255:26:255:34:func_should_fail:Using variable 'datetime' before assignment:INFERENCE
3131
undefined-variable:291:18:291:24:not_using_loop_variable_accordingly:Undefined variable 'iteree':UNDEFINED
3232
undefined-variable:308:27:308:28:undefined_annotation:Undefined variable 'x':UNDEFINED
3333
used-before-assignment:309:7:309:8:undefined_annotation:Using variable 'x' before assignment:HIGH

tests/functional/u/undefined/undefined_variable_py38.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ undefined-variable:106:6:106:19::Undefined variable 'else_assign_2':INFERENCE
77
used-before-assignment:141:10:141:16:type_annotation_used_improperly_after_comprehension:Using variable 'my_int' before assignment:HIGH
88
used-before-assignment:148:10:148:16:type_annotation_used_improperly_after_comprehension_2:Using variable 'my_int' before assignment:HIGH
99
used-before-assignment:186:9:186:10::Using variable 'z' before assignment:HIGH
10-
used-before-assignment:193:6:193:19::Using variable 'NEVER_DEFINED' before assignment:CONTROL_FLOW
10+
used-before-assignment:193:6:193:19::Using variable 'NEVER_DEFINED' before assignment:INFERENCE

tests/functional/u/used/used_before_assignment.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def redefine_time_import_with_global():
6060
pass
6161
else:
6262
VAR4 = False
63-
if VAR4: # [used-before-assignment]
63+
if VAR4: # [possibly-used-before-assignment]
6464
pass
6565

6666
if FALSE:
@@ -70,7 +70,7 @@ def redefine_time_import_with_global():
7070
VAR5 = True
7171
else:
7272
VAR5 = True
73-
if VAR5:
73+
if VAR5: # [possibly-used-before-assignment]
7474
pass
7575

7676
if FALSE:
@@ -116,7 +116,7 @@ def redefine_time_import_with_global():
116116
VAR11 = num
117117
if VAR11:
118118
VAR12 = False
119-
print(VAR12)
119+
print(VAR12) # [possibly-used-before-assignment]
120120

121121
def turn_on2(**kwargs):
122122
"""https://github.com/pylint-dev/pylint/issues/7873"""
@@ -180,3 +180,21 @@ def give_me_none():
180180
class T: # pylint: disable=invalid-name, too-few-public-methods, undefined-variable
181181
'''Issue #8754, no crash from unexpected assignment between attribute and variable'''
182182
T.attr = attr
183+
184+
185+
if outer():
186+
NOT_ALWAYS_DEFINED = True
187+
print(NOT_ALWAYS_DEFINED) # [used-before-assignment]
188+
189+
190+
def inner_if_continues_outer_if_has_no_other_statements():
191+
for i in range(5):
192+
if isinstance(i, int):
193+
# Testing no assignment here, before the inner if
194+
if i % 2 == 0:
195+
order = None
196+
else:
197+
continue
198+
else:
199+
order = None
200+
print(order)

tests/functional/u/used/used_before_assignment.txt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ used-before-assignment:10:4:10:9:outer:Using variable 'inner' before assignment:
44
used-before-assignment:19:20:19:40:ClassWithProperty:Using variable 'redefine_time_import' before assignment:HIGH
55
used-before-assignment:23:0:23:9::Using variable 'calculate' before assignment:HIGH
66
used-before-assignment:31:10:31:14:redefine_time_import:Using variable 'time' before assignment:HIGH
7-
used-before-assignment:45:3:45:7::Using variable 'VAR2' before assignment:CONTROL_FLOW
8-
used-before-assignment:63:3:63:7::Using variable 'VAR4' before assignment:CONTROL_FLOW
9-
used-before-assignment:78:3:78:7::Using variable 'VAR6' before assignment:CONTROL_FLOW
10-
used-before-assignment:113:6:113:11::Using variable 'VAR10' before assignment:CONTROL_FLOW
11-
used-before-assignment:144:10:144:14::Using variable 'SALE' before assignment:CONTROL_FLOW
12-
used-before-assignment:176:10:176:18::Using variable 'ALL_DONE' before assignment:CONTROL_FLOW
7+
used-before-assignment:45:3:45:7::Using variable 'VAR2' before assignment:INFERENCE
8+
possibly-used-before-assignment:63:3:63:7::Possibly using variable 'VAR4' before assignment:INFERENCE
9+
possibly-used-before-assignment:73:3:73:7::Possibly using variable 'VAR5' before assignment:INFERENCE
10+
used-before-assignment:78:3:78:7::Using variable 'VAR6' before assignment:INFERENCE
11+
used-before-assignment:113:6:113:11::Using variable 'VAR10' before assignment:INFERENCE
12+
possibly-used-before-assignment:119:6:119:11::Possibly using variable 'VAR12' before assignment:CONTROL_FLOW
13+
used-before-assignment:144:10:144:14::Using variable 'SALE' before assignment:INFERENCE
14+
used-before-assignment:176:10:176:18::Using variable 'ALL_DONE' before assignment:INFERENCE
15+
used-before-assignment:187:6:187:24::Using variable 'NOT_ALWAYS_DEFINED' before assignment:INFERENCE
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
used-before-assignment:16:14:16:29:function:Using variable 'failure_message' before assignment:CONTROL_FLOW
22
used-before-assignment:120:10:120:13:func_invalid1:Using variable 'msg' before assignment:CONTROL_FLOW
33
used-before-assignment:131:10:131:13:func_invalid2:Using variable 'msg' before assignment:CONTROL_FLOW
4-
used-before-assignment:150:10:150:13:func_invalid3:Using variable 'msg' before assignment:CONTROL_FLOW
4+
used-before-assignment:150:10:150:13:func_invalid3:Using variable 'msg' before assignment:INFERENCE
55
used-before-assignment:163:10:163:13:func_invalid4:Using variable 'msg' before assignment:CONTROL_FLOW
66
used-before-assignment:175:10:175:13:func_invalid5:Using variable 'msg' before assignment:CONTROL_FLOW
77
used-before-assignment:187:10:187:13:func_invalid6:Using variable 'msg' before assignment:CONTROL_FLOW

tests/functional/u/used/used_before_assignment_issue1081.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def used_before_assignment_2(a):
1616

1717

1818
def used_before_assignment_3(a):
19-
if x == a: # [used-before-assignment]
19+
if x == a: # [possibly-used-before-assignment]
2020
if x > 3:
2121
x = 2 # [redefined-outer-name]
2222

tests/functional/u/used/used_before_assignment_issue1081.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ used-before-assignment:7:7:7:8:used_before_assignment_1:Using variable 'x' befor
22
redefined-outer-name:8:12:8:13:used_before_assignment_1:Redefining name 'x' from outer scope (line 3):UNDEFINED
33
used-before-assignment:13:7:13:8:used_before_assignment_2:Using variable 'x' before assignment:HIGH
44
redefined-outer-name:15:4:15:5:used_before_assignment_2:Redefining name 'x' from outer scope (line 3):UNDEFINED
5-
used-before-assignment:19:7:19:8:used_before_assignment_3:Using variable 'x' before assignment:HIGH
5+
possibly-used-before-assignment:19:7:19:8:used_before_assignment_3:Possibly using variable 'x' before assignment:CONTROL_FLOW
66
redefined-outer-name:21:12:21:13:used_before_assignment_3:Redefining name 'x' from outer scope (line 3):UNDEFINED
77
redefined-outer-name:30:4:30:5:not_used_before_assignment_2:Redefining name 'x' from outer scope (line 3):UNDEFINED
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
used-before-assignment:12:14:12:17:main:Using variable 'res' before assignment:CONTROL_FLOW
1+
used-before-assignment:12:14:12:17:main:Using variable 'res' before assignment:INFERENCE
22
used-before-assignment:30:18:30:35:nested_except_blocks:Using variable 'more_bad_division' before assignment:CONTROL_FLOW
3-
used-before-assignment:31:18:31:21:nested_except_blocks:Using variable 'res' before assignment:CONTROL_FLOW
3+
used-before-assignment:31:18:31:21:nested_except_blocks:Using variable 'res' before assignment:INFERENCE
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
unused-variable:5:4:6:12:main1:Unused variable 'e':UNDEFINED
2-
used-before-assignment:8:10:8:11:main1:Using variable 'e' before assignment:CONTROL_FLOW
2+
used-before-assignment:8:10:8:11:main1:Using variable 'e' before assignment:HIGH
33
unused-variable:21:4:22:12:main3:Unused variable 'e':UNDEFINED
44
unused-variable:31:4:32:12:main4:Unused variable 'e':UNDEFINED
5-
used-before-assignment:44:10:44:11:main4:Using variable 'e' before assignment:CONTROL_FLOW
5+
used-before-assignment:44:10:44:11:main4:Using variable 'e' before assignment:HIGH
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
used-before-assignment:10:6:10:9::Using variable 'var' before assignment:CONTROL_FLOW
1+
used-before-assignment:10:6:10:9::Using variable 'var' before assignment:INFERENCE
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
used-before-assignment:10:13:10:21:func_two:Using variable 'datetime' before assignment:CONTROL_FLOW
2-
used-before-assignment:16:12:16:20:func:Using variable 'datetime' before assignment:CONTROL_FLOW
1+
used-before-assignment:10:13:10:21:func_two:Using variable 'datetime' before assignment:INFERENCE
2+
used-before-assignment:16:12:16:20:func:Using variable 'datetime' before assignment:INFERENCE

0 commit comments

Comments
 (0)