@@ -534,7 +534,7 @@ def _has_locals_call_after_node(stmt, scope):
534
534
535
535
536
536
ScopeConsumer = collections .namedtuple (
537
- "ScopeConsumer" , "to_consume consumed scope_type"
537
+ "ScopeConsumer" , "to_consume consumed consumed_uncertain scope_type"
538
538
)
539
539
540
540
@@ -544,17 +544,24 @@ class NamesConsumer:
544
544
"""
545
545
546
546
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
+ )
548
550
self .node = node
549
551
550
552
def __repr__ (self ):
551
553
to_consumes = [f"{ k } ->{ v } " for k , v in self ._atomic .to_consume .items ()]
552
554
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
+ ]
553
558
to_consumes = ", " .join (to_consumes )
554
559
consumed = ", " .join (consumed )
560
+ consumed_uncertain = ", " .join (consumed_uncertain )
555
561
return f"""
556
562
to_consume : { to_consumes }
557
563
consumed : { consumed }
564
+ consumed_uncertain: { consumed_uncertain }
558
565
scope_type : { self ._atomic .scope_type }
559
566
"""
560
567
@@ -569,6 +576,19 @@ def to_consume(self):
569
576
def consumed (self ):
570
577
return self ._atomic .consumed
571
578
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
+
572
592
@property
573
593
def scope_type (self ):
574
594
return self ._atomic .scope_type
@@ -589,7 +609,9 @@ def mark_as_consumed(self, name, consumed_nodes):
589
609
590
610
def get_next_to_consume (self , node ):
591
611
"""
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.
593
615
Return None to indicate a special case that needs to be handled by the caller.
594
616
"""
595
617
name = node .name
@@ -617,9 +639,28 @@ def get_next_to_consume(self, node):
617
639
found_nodes = [
618
640
n
619
641
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 )
622
659
]
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
623
664
624
665
return found_nodes
625
666
@@ -1042,6 +1083,14 @@ def _undefined_and_used_before_checker(
1042
1083
if action is VariableVisitConsumerAction .CONTINUE :
1043
1084
continue
1044
1085
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 ]
1045
1094
current_consumer .mark_as_consumed (node .name , found_nodes )
1046
1095
if action in {
1047
1096
VariableVisitConsumerAction .RETURN ,
@@ -1135,6 +1184,12 @@ def _check_consumer(
1135
1184
return (VariableVisitConsumerAction .CONTINUE , None )
1136
1185
if not found_nodes :
1137
1186
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 )
1138
1193
return (VariableVisitConsumerAction .RETURN , found_nodes )
1139
1194
1140
1195
self ._check_late_binding_closure (node )
@@ -2370,7 +2425,7 @@ def _check_classdef_metaclasses(self, klass, parent_node):
2370
2425
name = METACLASS_NAME_TRANSFORMS .get (name , name )
2371
2426
if name :
2372
2427
# 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 ]:
2374
2429
found_nodes = scope_locals .get (name , [])
2375
2430
for found_node in found_nodes :
2376
2431
if found_node .lineno <= klass .lineno :
0 commit comments