diff options
Diffstat (limited to 'pylint/checkers/variables.py')
-rw-r--r-- | pylint/checkers/variables.py | 67 |
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: |