@@ -306,6 +306,27 @@ def _bind_assignment_target(self, target: ast.AST, value: ast.AST) -> None:
306306 for target_element in target .elts :
307307 self ._bind_assignment_target (target_element , value )
308308
309+ def _bind_iterated_target (self , target : ast .AST , iterable : ast .AST ) -> None :
310+ """Bind a loop target to every statically visible iterable value."""
311+ if isinstance (iterable , (ast .List , ast .Tuple , ast .Set )):
312+ values = iterable .elts
313+ elif isinstance (iterable , ast .Dict ):
314+ values = [key for key in iterable .keys if key is not None ]
315+ else :
316+ values = [iterable ]
317+
318+ before_iteration = self ._snapshot_alias_state ()
319+ iteration_states : list [_AliasState ] = []
320+ for value in values :
321+ self ._restore_alias_state (before_iteration )
322+ self ._bind_assignment_target (target , value )
323+ iteration_states .append (self ._snapshot_alias_state ())
324+
325+ if iteration_states :
326+ self ._merge_alias_states (iteration_states )
327+ else :
328+ self ._bind_assignment_target (target , iterable )
329+
309330 def _snapshot_alias_state (self ) -> _AliasState :
310331 return self .module_aliases .copy (), self .shadowed_aliases .copy ()
311332
@@ -314,6 +335,28 @@ def _restore_alias_state(self, state: _AliasState) -> None:
314335 self .module_aliases = aliases .copy ()
315336 self .shadowed_aliases = shadowed .copy ()
316337
338+ def _restore_target_names (self , target_names : set [str ], state : _AliasState ) -> None :
339+ aliases , shadowed = state
340+ for name in target_names :
341+ if name in aliases :
342+ self .module_aliases [name ] = aliases [name ]
343+ self .shadowed_aliases .discard (name )
344+ elif name in shadowed :
345+ self .module_aliases .pop (name , None )
346+ self .shadowed_aliases .add (name )
347+ else :
348+ self .module_aliases .pop (name , None )
349+ self .shadowed_aliases .discard (name )
350+
351+ def _assignment_target_names (self , target : ast .AST ) -> set [str ]:
352+ if isinstance (target , ast .Name ):
353+ return {target .id }
354+ if isinstance (target , ast .Starred ):
355+ return self ._assignment_target_names (target .value )
356+ if isinstance (target , (ast .Tuple , ast .List )):
357+ return set ().union (* (self ._assignment_target_names (element ) for element in target .elts ))
358+ return set ()
359+
317360 def _merge_alias_states (self , states : list [_AliasState ]) -> None :
318361 """Conservatively retain every module value reachable from a branch."""
319362 names = set ().union (* (set (aliases ) | shadowed for aliases , shadowed in states ))
@@ -414,6 +457,7 @@ def _visit_loop(self, node: ast.For | ast.AsyncFor | ast.While) -> None:
414457 else :
415458 self .visit (node .iter )
416459 self .visit (node .target )
460+ self ._bind_iterated_target (node .target , node .iter )
417461
418462 for statement in node .body :
419463 self .visit (statement )
@@ -437,6 +481,35 @@ def visit_AsyncFor(self, node: ast.AsyncFor):
437481 def visit_While (self , node : ast .While ):
438482 self ._visit_loop (node )
439483
484+ def _visit_comprehension_expression (
485+ self , generators : list [ast .comprehension ], expressions : tuple [ast .AST , ...]
486+ ) -> None :
487+ """Visit comprehensions in evaluation order with isolated target bindings."""
488+ enclosing_state = self ._snapshot_alias_state ()
489+ target_names : set [str ] = set ()
490+ for generator in generators :
491+ self .visit (generator .iter )
492+ self .visit (generator .target )
493+ self ._bind_iterated_target (generator .target , generator .iter )
494+ target_names .update (self ._assignment_target_names (generator .target ))
495+ for condition in generator .ifs :
496+ self .visit (condition )
497+ for expression in expressions :
498+ self .visit (expression )
499+ self ._restore_target_names (target_names , enclosing_state )
500+
501+ def visit_ListComp (self , node : ast .ListComp ):
502+ self ._visit_comprehension_expression (node .generators , (node .elt ,))
503+
504+ def visit_SetComp (self , node : ast .SetComp ):
505+ self ._visit_comprehension_expression (node .generators , (node .elt ,))
506+
507+ def visit_DictComp (self , node : ast .DictComp ):
508+ self ._visit_comprehension_expression (node .generators , (node .key , node .value ))
509+
510+ def visit_GeneratorExp (self , node : ast .GeneratorExp ):
511+ self ._visit_comprehension_expression (node .generators , (node .elt ,))
512+
440513 def _shadow_arguments (self , arguments : ast .arguments ) -> None :
441514 positional = [* arguments .posonlyargs , * arguments .args , * arguments .kwonlyargs ]
442515 for argument in positional :
0 commit comments