summaryrefslogtreecommitdiff
path: root/pylint/checkers/variables.py
diff options
context:
space:
mode:
Diffstat (limited to 'pylint/checkers/variables.py')
-rw-r--r--pylint/checkers/variables.py67
1 files changed, 61 insertions, 6 deletions
diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py
index f2bba355c..13b8589af 100644
--- a/pylint/checkers/variables.py
+++ b/pylint/checkers/variables.py
@@ -534,7 +534,7 @@ MSGS = {
ScopeConsumer = collections.namedtuple(
- "ScopeConsumer", "to_consume consumed scope_type"
+ "ScopeConsumer", "to_consume consumed consumed_uncertain scope_type"
)
@@ -544,17 +544,24 @@ class NamesConsumer:
"""
def __init__(self, node, scope_type):
- self._atomic = ScopeConsumer(copy.copy(node.locals), {}, scope_type)
+ self._atomic = ScopeConsumer(
+ copy.copy(node.locals), {}, collections.defaultdict(list), scope_type
+ )
self.node = node
def __repr__(self):
to_consumes = [f"{k}->{v}" for k, v in self._atomic.to_consume.items()]
consumed = [f"{k}->{v}" for k, v in self._atomic.consumed.items()]
+ consumed_uncertain = [
+ f"{k}->{v}" for k, v in self._atomic.consumed_uncertain.items()
+ ]
to_consumes = ", ".join(to_consumes)
consumed = ", ".join(consumed)
+ consumed_uncertain = ", ".join(consumed_uncertain)
return f"""
to_consume : {to_consumes}
consumed : {consumed}
+consumed_uncertain: {consumed_uncertain}
scope_type : {self._atomic.scope_type}
"""
@@ -570,6 +577,19 @@ scope_type : {self._atomic.scope_type}
return self._atomic.consumed
@property
+ 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.
+ """
+ return self._atomic.consumed_uncertain
+
+ @property
def scope_type(self):
return self._atomic.scope_type
@@ -589,7 +609,9 @@ scope_type : {self._atomic.scope_type}
def get_next_to_consume(self, node):
"""
- Return a list of the nodes that define `node` from this scope.
+ Return a list of the nodes that define `node` from this scope. If it is
+ uncertain whether a node will be consumed, such as for statements in
+ except blocks, add it to self.consumed_uncertain instead of returning it.
Return None to indicate a special case that needs to be handled by the caller.
"""
name = node.name
@@ -617,9 +639,28 @@ scope_type : {self._atomic.scope_type}
found_nodes = [
n
for n in found_nodes
- if not isinstance(n.statement(), nodes.ExceptHandler)
- or n.statement().parent_of(node)
+ if not isinstance(n.statement(future=True), nodes.ExceptHandler)
+ or n.statement(future=True).parent_of(node)
+ ]
+
+ # Filter out assignments in an Except clause that the node is not
+ # contained in, assuming they may fail
+ if found_nodes:
+ filtered_nodes = [
+ n
+ for n in found_nodes
+ if not (
+ isinstance(n.statement(future=True).parent, nodes.ExceptHandler)
+ and isinstance(
+ n.statement(future=True).parent.parent, nodes.TryExcept
+ )
+ )
+ or n.statement(future=True).parent.parent_of(node)
]
+ 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
@@ -1042,6 +1083,14 @@ class VariablesChecker(BaseChecker):
if action is VariableVisitConsumerAction.CONTINUE:
continue
if action is VariableVisitConsumerAction.CONSUME:
+ # pylint: disable-next=fixme
+ # TODO: remove assert after _check_consumer return value better typed
+ assert found_nodes is not None, "Cannot consume an empty list of nodes."
+ # Any nodes added to consumed_uncertain by get_next_to_consume()
+ # should be added back so that they are marked as used.
+ # They will have already had a chance to emit used-before-assignment.
+ # We check here instead of before every single return in _check_consumer()
+ found_nodes += current_consumer.consumed_uncertain[node.name]
current_consumer.mark_as_consumed(node.name, found_nodes)
if action in {
VariableVisitConsumerAction.RETURN,
@@ -1135,6 +1184,12 @@ class VariablesChecker(BaseChecker):
return (VariableVisitConsumerAction.CONTINUE, None)
if not found_nodes:
self.add_message("used-before-assignment", args=node.name, node=node)
+ if current_consumer.consumed_uncertain[node.name]:
+ # If there are nodes added to consumed_uncertain by
+ # get_next_to_consume() because they might not have executed,
+ # return a CONSUME action so that _undefined_and_used_before_checker()
+ # will mark them as used
+ return (VariableVisitConsumerAction.CONSUME, found_nodes)
return (VariableVisitConsumerAction.RETURN, found_nodes)
self._check_late_binding_closure(node)
@@ -2370,7 +2425,7 @@ class VariablesChecker(BaseChecker):
name = METACLASS_NAME_TRANSFORMS.get(name, name)
if name:
# check enclosing scopes starting from most local
- for scope_locals, _, _ in self._to_consume[::-1]:
+ for scope_locals, _, _, _ in self._to_consume[::-1]:
found_nodes = scope_locals.get(name, [])
for found_node in found_nodes:
if found_node.lineno <= klass.lineno: