@@ -186,6 +186,14 @@ def visit_classdef(self, node: nodes.ClassDef) -> None:
186
186
self .compositions_handler .handle (assignattr , node )
187
187
self .handle_assignattr_type (assignattr , node )
188
188
189
+ # Process class attributes
190
+ for local_nodes in node .locals .values ():
191
+ for local_node in local_nodes :
192
+ if isinstance (local_node , nodes .AssignName ) and isinstance (
193
+ local_node .parent , nodes .Assign
194
+ ):
195
+ self .compositions_handler .handle (local_node , node )
196
+
189
197
def visit_functiondef (self , node : nodes .FunctionDef ) -> None :
190
198
"""Visit an astroid.Function node.
191
199
@@ -307,7 +315,9 @@ def set_next(
307
315
pass
308
316
309
317
@abstractmethod
310
- def handle (self , node : nodes .AssignAttr , parent : nodes .ClassDef ) -> None :
318
+ def handle (
319
+ self , node : nodes .AssignAttr | nodes .AssignName , parent : nodes .ClassDef
320
+ ) -> None :
311
321
pass
312
322
313
323
@@ -332,21 +342,29 @@ def set_next(
332
342
return handler
333
343
334
344
@abstractmethod
335
- def handle (self , node : nodes .AssignAttr , parent : nodes .ClassDef ) -> None :
345
+ def handle (
346
+ self , node : nodes .AssignAttr | nodes .AssignName , parent : nodes .ClassDef
347
+ ) -> None :
336
348
if self ._next_handler :
337
349
self ._next_handler .handle (node , parent )
338
350
339
351
340
352
class CompositionsHandler (AbstractRelationshipHandler ):
341
353
"""Handle composition relationships where parent creates child objects."""
342
354
343
- def handle (self , node : nodes .AssignAttr , parent : nodes .ClassDef ) -> None :
355
+ def handle (
356
+ self , node : nodes .AssignAttr | nodes .AssignName , parent : nodes .ClassDef
357
+ ) -> None :
358
+ # If the node is not part of an assignment, pass to next handler
344
359
if not isinstance (node .parent , (nodes .AnnAssign , nodes .Assign )):
345
360
super ().handle (node , parent )
346
361
return
347
362
348
363
value = node .parent .value
349
364
365
+ # Extract the name to handle both AssignAttr and AssignName nodes
366
+ name = node .attrname if isinstance (node , nodes .AssignAttr ) else node .name
367
+
350
368
# Composition: direct object creation (self.x = P())
351
369
if isinstance (value , nodes .Call ):
352
370
inferred_types = utils .infer_node (node )
@@ -355,8 +373,8 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
355
373
# Resolve nodes to actual class definitions
356
374
resolved_types = resolve_to_class_def (element_types )
357
375
358
- current = set (parent .compositions_type [node . attrname ])
359
- parent .compositions_type [node . attrname ] = list (current | resolved_types )
376
+ current = set (parent .compositions_type [name ])
377
+ parent .compositions_type [name ] = list (current | resolved_types )
360
378
return
361
379
362
380
# Composition: comprehensions with object creation (self.x = [P() for ...])
@@ -376,8 +394,8 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
376
394
# Resolve nodes to actual class definitions
377
395
resolved_types = resolve_to_class_def (element_types )
378
396
379
- current = set (parent .compositions_type [node . attrname ])
380
- parent .compositions_type [node . attrname ] = list (current | resolved_types )
397
+ current = set (parent .compositions_type [name ])
398
+ parent .compositions_type [name ] = list (current | resolved_types )
381
399
return
382
400
383
401
# Not a composition, pass to next handler
@@ -387,13 +405,19 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
387
405
class AggregationsHandler (AbstractRelationshipHandler ):
388
406
"""Handle aggregation relationships where parent receives child objects."""
389
407
390
- def handle (self , node : nodes .AssignAttr , parent : nodes .ClassDef ) -> None :
408
+ def handle (
409
+ self , node : nodes .AssignAttr | nodes .AssignName , parent : nodes .ClassDef
410
+ ) -> None :
411
+ # If the node is not part of an assignment, pass to next handler
391
412
if not isinstance (node .parent , (nodes .AnnAssign , nodes .Assign )):
392
413
super ().handle (node , parent )
393
414
return
394
415
395
416
value = node .parent .value
396
417
418
+ # Extract the name to handle both AssignAttr and AssignName nodes
419
+ name = node .attrname if isinstance (node , nodes .AssignAttr ) else node .name
420
+
397
421
# Aggregation: direct assignment (self.x = x)
398
422
if isinstance (value , nodes .Name ):
399
423
inferred_types = utils .infer_node (node )
@@ -402,8 +426,8 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
402
426
# Resolve nodes to actual class definitions
403
427
resolved_types = resolve_to_class_def (element_types )
404
428
405
- current = set (parent .aggregations_type [node . attrname ])
406
- parent .aggregations_type [node . attrname ] = list (current | resolved_types )
429
+ current = set (parent .aggregations_type [name ])
430
+ parent .aggregations_type [name ] = list (current | resolved_types )
407
431
return
408
432
409
433
# Aggregation: comprehensions without object creation (self.x = [existing_obj for ...])
@@ -423,8 +447,8 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
423
447
# Resolve nodes to actual class definitions
424
448
resolved_types = resolve_to_class_def (element_types )
425
449
426
- current = set (parent .aggregations_type [node . attrname ])
427
- parent .aggregations_type [node . attrname ] = list (current | resolved_types )
450
+ current = set (parent .aggregations_type [name ])
451
+ parent .aggregations_type [name ] = list (current | resolved_types )
428
452
return
429
453
430
454
# Not an aggregation, pass to next handler
@@ -434,7 +458,12 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
434
458
class AssociationsHandler (AbstractRelationshipHandler ):
435
459
"""Handle regular association relationships."""
436
460
437
- def handle (self , node : nodes .AssignAttr , parent : nodes .ClassDef ) -> None :
461
+ def handle (
462
+ self , node : nodes .AssignAttr | nodes .AssignName , parent : nodes .ClassDef
463
+ ) -> None :
464
+ # Extract the name to handle both AssignAttr and AssignName nodes
465
+ name = node .attrname if isinstance (node , nodes .AssignAttr ) else node .name
466
+
438
467
# Type annotation only (x: P) -> Association
439
468
# BUT only if there's no actual assignment (to avoid duplicates)
440
469
if isinstance (node .parent , nodes .AnnAssign ) and node .parent .value is None :
@@ -444,18 +473,18 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
444
473
# Resolve nodes to actual class definitions
445
474
resolved_types = resolve_to_class_def (element_types )
446
475
447
- current = set (parent .associations_type [node . attrname ])
448
- parent .associations_type [node . attrname ] = list (current | resolved_types )
476
+ current = set (parent .associations_type [name ])
477
+ parent .associations_type [name ] = list (current | resolved_types )
449
478
return
450
479
451
480
# Everything else is also association (fallback)
452
- current = set (parent .associations_type [node . attrname ])
481
+ current = set (parent .associations_type [name ])
453
482
inferred_types = utils .infer_node (node )
454
483
element_types = extract_element_types (inferred_types )
455
484
456
485
# Resolve Name nodes to actual class definitions
457
486
resolved_types = resolve_to_class_def (element_types )
458
- parent .associations_type [node . attrname ] = list (current | resolved_types )
487
+ parent .associations_type [name ] = list (current | resolved_types )
459
488
460
489
461
490
def resolve_to_class_def (types : set [nodes .NodeNG ]) -> set [nodes .ClassDef ]:
0 commit comments