diff --git a/ChangeLog b/ChangeLog index 22c918048b..53cf7b4cf2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -16,6 +16,11 @@ Release date: TBA Closes #4761 +* ``used-before-assignment`` now considers that assignments in a try block + may not have occurred when the except or finally blocks are executed. + + Closes #85, #2615 + * ``used-before-assignment`` now checks names in try blocks. * Some files in ``pylint.testutils`` were deprecated. In the future imports should be done from the diff --git a/doc/whatsnew/2.13.rst b/doc/whatsnew/2.13.rst index 55535638ec..d5ea7130c3 100644 --- a/doc/whatsnew/2.13.rst +++ b/doc/whatsnew/2.13.rst @@ -36,6 +36,11 @@ Other Changes Closes #4761 +* ``used-before-assignment`` now considers that assignments in a try block + may not have occurred when the except or finally blocks are executed. + + Closes #85, #2615 + * ``used-before-assignment`` now checks names in try blocks. * Require Python ``3.6.2`` to run pylint. diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 13b8589afb..0a4a21c182 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -405,7 +405,10 @@ def _has_locals_call_after_node(stmt, scope): "E0601": ( "Using variable %r before assignment", "used-before-assignment", - "Used when a local variable is accessed before its assignment.", + "Emitted when a local variable is accessed before its assignment took place. " + "Assignments in try blocks are assumed not to have occurred when evaluating " + "associated except/finally blocks. Assignments in except blocks are assumed " + "not to have occurred when evaluating statements outside the block.", ), "E0602": ( "Undefined variable %r", @@ -580,12 +583,10 @@ def consumed(self): def consumed_uncertain(self) -> DefaultDict[str, List[nodes.NodeNG]]: """ Retrieves nodes filtered out by get_next_to_consume() that may not - have executed, such as statements in except blocks. Checkers that - want to treat the statements as executed (e.g. for unused-variable) - may need to add them back. - - TODO: A pending PR will extend this to nodes in try blocks when - evaluating their corresponding except and finally blocks. + have executed, such as statements in except blocks, or statements + in try blocks (when evaluating their corresponding except and finally + blocks). Checkers that want to treat the statements as executed + (e.g. for unused-variable) may need to add them back. """ return self._atomic.consumed_uncertain @@ -617,6 +618,7 @@ def get_next_to_consume(self, node): name = node.name parent_node = node.parent found_nodes = self.to_consume.get(name) + node_statement = node.statement(future=True) if ( found_nodes and isinstance(parent_node, nodes.Assign) @@ -662,6 +664,44 @@ def get_next_to_consume(self, node): self.consumed_uncertain[node.name] += difference found_nodes = filtered_nodes + # If this node is in a Finally block of a Try/Finally, + # filter out assignments in the try portion, assuming they may fail + if ( + found_nodes + and isinstance(node_statement.parent, nodes.TryFinally) + and node_statement in node_statement.parent.finalbody + ): + filtered_nodes = [ + n + for n in found_nodes + if not ( + n.statement(future=True).parent is node_statement.parent + and n.statement(future=True) in n.statement(future=True).parent.body + ) + ] + filtered_nodes_set = set(filtered_nodes) + difference = [n for n in found_nodes if n not in filtered_nodes_set] + self.consumed_uncertain[node.name] += difference + found_nodes = filtered_nodes + + # If this node is in an ExceptHandler, + # filter out assignments in the try portion, assuming they may fail + if found_nodes and isinstance(node_statement.parent, nodes.ExceptHandler): + filtered_nodes = [ + n + for n in found_nodes + if not ( + isinstance(n.statement(future=True).parent, nodes.TryExcept) + and n.statement(future=True) in n.statement(future=True).parent.body + and node_statement.parent + in n.statement(future=True).parent.handlers + ) + ] + filtered_nodes_set = set(filtered_nodes) + difference = [n for n in found_nodes if n not in filtered_nodes_set] + self.consumed_uncertain[node.name] += difference + found_nodes = filtered_nodes + return found_nodes diff --git a/tests/functional/u/use/used_before_assignment_issue2615.py b/tests/functional/u/use/used_before_assignment_issue2615.py new file mode 100644 index 0000000000..912c713878 --- /dev/null +++ b/tests/functional/u/use/used_before_assignment_issue2615.py @@ -0,0 +1,9 @@ +"""https://github.com/PyCQA/pylint/issues/2615""" +def main(): + """When evaluating except blocks, assume try statements fail.""" + try: + res = 1 / 0 + res = 42 + except ZeroDivisionError: + print(res) # [used-before-assignment] + print(res) diff --git a/tests/functional/u/use/used_before_assignment_issue2615.txt b/tests/functional/u/use/used_before_assignment_issue2615.txt new file mode 100644 index 0000000000..ce6e4b9d06 --- /dev/null +++ b/tests/functional/u/use/used_before_assignment_issue2615.txt @@ -0,0 +1 @@ +used-before-assignment:8:14:8:17:main:Using variable 'res' before assignment:UNDEFINED diff --git a/tests/functional/u/use/used_before_assignment_issue85.py b/tests/functional/u/use/used_before_assignment_issue85.py new file mode 100644 index 0000000000..58d8e38d85 --- /dev/null +++ b/tests/functional/u/use/used_before_assignment_issue85.py @@ -0,0 +1,9 @@ +"""https://github.com/PyCQA/pylint/issues/85""" +def main(): + """When evaluating finally blocks, assume try statements fail.""" + try: + res = 1 / 0 + res = 42 + finally: + print(res) # [used-before-assignment] + print(res) diff --git a/tests/functional/u/use/used_before_assignment_issue85.txt b/tests/functional/u/use/used_before_assignment_issue85.txt new file mode 100644 index 0000000000..ce6e4b9d06 --- /dev/null +++ b/tests/functional/u/use/used_before_assignment_issue85.txt @@ -0,0 +1 @@ +used-before-assignment:8:14:8:17:main:Using variable 'res' before assignment:UNDEFINED