Skip to content

Commit bd55b27

Browse files
jacobtylerwallsDanielNoordPierre-Sassoulas
authored
Fix pylint-dev#4761: Emit used-before-assignment where single assignment only made in except blocks (pylint-dev#5402)
Co-authored-by: Daniël van Noord <[email protected]> Co-authored-by: Pierre Sassoulas <[email protected]>
1 parent a51a548 commit bd55b27

File tree

9 files changed

+97
-13
lines changed

9 files changed

+97
-13
lines changed

ChangeLog

+10-3
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,23 @@ Release date: TBA
1111
..
1212
Put new features here and also in 'doc/whatsnew/2.13.rst'
1313

14+
* ``used-before-assignment`` now assumes that assignments in except blocks
15+
may not have occurred and warns accordingly.
16+
17+
Closes #4761
18+
19+
* ``used-before-assignment`` now checks names in try blocks.
20+
1421
* Some files in ``pylint.testutils`` were deprecated. In the future imports should be done from the
1522
``pylint.testutils.functional`` namespace directly.
1623

1724
* Fix ``unnecessary_dict_index_lookup`` false positive when deleting a dictionary's entry.
1825

1926
Closes #4716
2027

28+
* The ``PyLinter`` class will now be initialiazed with a ``TextReporter``
29+
as its reporter if none is provided.
30+
2131
* Fix false positive - Allow unpacking of ``self`` in a subclass of ``typing.NamedTuple``.
2232

2333
Closes #5312
@@ -36,9 +46,6 @@ Release date: TBA
3646
Insert your changelog randomly, it will reduce merge conflicts
3747
(Ie. not necessarily at the end)
3848

39-
* The ``PyLinter`` class will now be initialiazed with a ``TextReporter``
40-
as its reporter if none is provided.
41-
4249

4350
What's New in Pylint 2.12.2?
4451
============================

doc/whatsnew/2.13.rst

+7
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ Other Changes
3131

3232
Closes #5323
3333

34+
* ``used-before-assignment`` now assumes that assignments in except blocks
35+
may not have occurred and warns accordingly.
36+
37+
Closes #4761
38+
39+
* ``used-before-assignment`` now checks names in try blocks.
40+
3441
* Require Python ``3.6.2`` to run pylint.
3542

3643
Closes #5065

pylint/checkers/variables.py

+61-6
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ def _has_locals_call_after_node(stmt, scope):
534534

535535

536536
ScopeConsumer = collections.namedtuple(
537-
"ScopeConsumer", "to_consume consumed scope_type"
537+
"ScopeConsumer", "to_consume consumed consumed_uncertain scope_type"
538538
)
539539

540540

@@ -544,17 +544,24 @@ class NamesConsumer:
544544
"""
545545

546546
def __init__(self, node, scope_type):
547-
self._atomic = ScopeConsumer(copy.copy(node.locals), {}, scope_type)
547+
self._atomic = ScopeConsumer(
548+
copy.copy(node.locals), {}, collections.defaultdict(list), scope_type
549+
)
548550
self.node = node
549551

550552
def __repr__(self):
551553
to_consumes = [f"{k}->{v}" for k, v in self._atomic.to_consume.items()]
552554
consumed = [f"{k}->{v}" for k, v in self._atomic.consumed.items()]
555+
consumed_uncertain = [
556+
f"{k}->{v}" for k, v in self._atomic.consumed_uncertain.items()
557+
]
553558
to_consumes = ", ".join(to_consumes)
554559
consumed = ", ".join(consumed)
560+
consumed_uncertain = ", ".join(consumed_uncertain)
555561
return f"""
556562
to_consume : {to_consumes}
557563
consumed : {consumed}
564+
consumed_uncertain: {consumed_uncertain}
558565
scope_type : {self._atomic.scope_type}
559566
"""
560567

@@ -569,6 +576,19 @@ def to_consume(self):
569576
def consumed(self):
570577
return self._atomic.consumed
571578

579+
@property
580+
def consumed_uncertain(self) -> DefaultDict[str, List[nodes.NodeNG]]:
581+
"""
582+
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.
589+
"""
590+
return self._atomic.consumed_uncertain
591+
572592
@property
573593
def scope_type(self):
574594
return self._atomic.scope_type
@@ -589,7 +609,9 @@ def mark_as_consumed(self, name, consumed_nodes):
589609

590610
def get_next_to_consume(self, node):
591611
"""
592-
Return a list of the nodes that define `node` from this scope.
612+
Return a list of the nodes that define `node` from this scope. If it is
613+
uncertain whether a node will be consumed, such as for statements in
614+
except blocks, add it to self.consumed_uncertain instead of returning it.
593615
Return None to indicate a special case that needs to be handled by the caller.
594616
"""
595617
name = node.name
@@ -617,9 +639,28 @@ def get_next_to_consume(self, node):
617639
found_nodes = [
618640
n
619641
for n in found_nodes
620-
if not isinstance(n.statement(), nodes.ExceptHandler)
621-
or n.statement().parent_of(node)
642+
if not isinstance(n.statement(future=True), nodes.ExceptHandler)
643+
or n.statement(future=True).parent_of(node)
644+
]
645+
646+
# Filter out assignments in an Except clause that the node is not
647+
# contained in, assuming they may fail
648+
if found_nodes:
649+
filtered_nodes = [
650+
n
651+
for n in found_nodes
652+
if not (
653+
isinstance(n.statement(future=True).parent, nodes.ExceptHandler)
654+
and isinstance(
655+
n.statement(future=True).parent.parent, nodes.TryExcept
656+
)
657+
)
658+
or n.statement(future=True).parent.parent_of(node)
622659
]
660+
filtered_nodes_set = set(filtered_nodes)
661+
difference = [n for n in found_nodes if n not in filtered_nodes_set]
662+
self.consumed_uncertain[node.name] += difference
663+
found_nodes = filtered_nodes
623664

624665
return found_nodes
625666

@@ -1042,6 +1083,14 @@ def _undefined_and_used_before_checker(
10421083
if action is VariableVisitConsumerAction.CONTINUE:
10431084
continue
10441085
if action is VariableVisitConsumerAction.CONSUME:
1086+
# pylint: disable-next=fixme
1087+
# TODO: remove assert after _check_consumer return value better typed
1088+
assert found_nodes is not None, "Cannot consume an empty list of nodes."
1089+
# Any nodes added to consumed_uncertain by get_next_to_consume()
1090+
# should be added back so that they are marked as used.
1091+
# They will have already had a chance to emit used-before-assignment.
1092+
# We check here instead of before every single return in _check_consumer()
1093+
found_nodes += current_consumer.consumed_uncertain[node.name]
10451094
current_consumer.mark_as_consumed(node.name, found_nodes)
10461095
if action in {
10471096
VariableVisitConsumerAction.RETURN,
@@ -1135,6 +1184,12 @@ def _check_consumer(
11351184
return (VariableVisitConsumerAction.CONTINUE, None)
11361185
if not found_nodes:
11371186
self.add_message("used-before-assignment", args=node.name, node=node)
1187+
if current_consumer.consumed_uncertain[node.name]:
1188+
# If there are nodes added to consumed_uncertain by
1189+
# get_next_to_consume() because they might not have executed,
1190+
# return a CONSUME action so that _undefined_and_used_before_checker()
1191+
# will mark them as used
1192+
return (VariableVisitConsumerAction.CONSUME, found_nodes)
11381193
return (VariableVisitConsumerAction.RETURN, found_nodes)
11391194

11401195
self._check_late_binding_closure(node)
@@ -2370,7 +2425,7 @@ def _check_classdef_metaclasses(self, klass, parent_node):
23702425
name = METACLASS_NAME_TRANSFORMS.get(name, name)
23712426
if name:
23722427
# check enclosing scopes starting from most local
2373-
for scope_locals, _, _ in self._to_consume[::-1]:
2428+
for scope_locals, _, _, _ in self._to_consume[::-1]:
23742429
found_nodes = scope_locals.get(name, [])
23752430
for found_node in found_nodes:
23762431
if found_node.lineno <= klass.lineno:

pylintrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ max-locals=25
332332
max-returns=11
333333

334334
# Maximum number of branch for function / method body
335-
max-branches=26
335+
max-branches=27
336336

337337
# Maximum number of statements in function / method body
338338
max-statements=100

tests/functional/u/undefined/undefined_variable.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def bad_default(var, default=unknown2): # [undefined-variable]
3535
LMBD2 = lambda x, y: x+z # [undefined-variable]
3636

3737
try:
38-
POUET # don't catch me
38+
POUET # [used-before-assignment]
3939
except NameError:
4040
POUET = 'something'
4141

@@ -45,7 +45,7 @@ def bad_default(var, default=unknown2): # [undefined-variable]
4545
POUETT = 'something'
4646

4747
try:
48-
POUETTT # don't catch me
48+
POUETTT # [used-before-assignment]
4949
except: # pylint:disable = bare-except
5050
POUETTT = 'something'
5151

tests/functional/u/undefined/undefined_variable.txt

+2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ undefined-variable:31:4:31:10:bad_default:Undefined variable 'augvar':UNDEFINED
88
undefined-variable:32:8:32:14:bad_default:Undefined variable 'vardel':UNDEFINED
99
undefined-variable:34:19:34:31:<lambda>:Undefined variable 'doesnotexist':UNDEFINED
1010
undefined-variable:35:23:35:24:<lambda>:Undefined variable 'z':UNDEFINED
11+
used-before-assignment:38:4:38:9::Using variable 'POUET' before assignment:UNDEFINED
1112
used-before-assignment:43:4:43:10::Using variable 'POUETT' before assignment:UNDEFINED
13+
used-before-assignment:48:4:48:11::Using variable 'POUETTT' before assignment:UNDEFINED
1214
used-before-assignment:56:4:56:9::Using variable 'PLOUF' before assignment:UNDEFINED
1315
used-before-assignment:65:11:65:14:if_branch_test:Using variable 'xxx' before assignment:UNDEFINED
1416
used-before-assignment:91:23:91:32:test_arguments:Using variable 'TestClass' before assignment:UNDEFINED
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""used-before-assignment (E0601)"""
2+
def function():
3+
"""Consider that except blocks may not execute."""
4+
try:
5+
pass
6+
except ValueError:
7+
some_message = 'some message'
8+
9+
if not some_message: # [used-before-assignment]
10+
return 1
11+
12+
return some_message
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
used-before-assignment:9:11:9:23:function:Using variable 'some_message' before assignment:UNDEFINED

tests/lint/test_utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def test_prepare_crash_report(tmp_path: PosixPath) -> None:
1515
template_path = prepare_crash_report(
1616
ex, str(python_file), str(tmp_path / "pylint-crash-%Y.txt")
1717
)
18-
assert str(tmp_path) in str(template_path)
18+
assert str(tmp_path) in str(template_path) # pylint: disable=used-before-assignment
1919
with open(template_path, encoding="utf8") as f:
2020
template_content = f.read()
2121
assert python_content in template_content

0 commit comments

Comments
 (0)