@@ -346,14 +346,31 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
346
346
347
347
value = node .parent .value
348
348
349
- # Composition: parent creates child (self.x = P())
349
+ # Composition: direct object creation (self.x = P())
350
350
if isinstance (value , nodes .Call ):
351
351
current = set (parent .compositions_type [node .attrname ])
352
352
parent .compositions_type [node .attrname ] = list (
353
353
current | utils .infer_node (node )
354
354
)
355
355
return
356
356
357
+ # Composition: comprehensions with object creation (self.x = [P() for ...])
358
+ if isinstance (
359
+ value , (nodes .ListComp , nodes .DictComp , nodes .SetComp , nodes .GeneratorExp )
360
+ ):
361
+ if isinstance (value , nodes .DictComp ):
362
+ element = value .value
363
+ else :
364
+ element = value .elt
365
+
366
+ # If the element is a Call (object creation), it's composition
367
+ if isinstance (element , nodes .Call ):
368
+ current = set (parent .compositions_type [node .attrname ])
369
+ parent .compositions_type [node .attrname ] = list (
370
+ current | utils .infer_node (node )
371
+ )
372
+ return
373
+
357
374
# Not a composition, pass to next handler
358
375
super ().handle (node , parent )
359
376
@@ -376,26 +393,27 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
376
393
)
377
394
return
378
395
379
- # Aggregation: comprehensions (self.x = [P() for ...])
396
+ # Aggregation: comprehensions without object creation (self.x = [existing_obj for ...])
380
397
if isinstance (
381
398
value , (nodes .ListComp , nodes .DictComp , nodes .SetComp , nodes .GeneratorExp )
382
399
):
383
400
if isinstance (value , nodes .DictComp ):
384
- element_type = safe_infer ( value .value )
401
+ element = value .value
385
402
else :
386
- element_type = safe_infer (value .elt )
387
- if element_type :
388
- current = set (parent .aggregations_type [node .attrname ])
389
- parent .aggregations_type [node .attrname ] = list (current | {element_type })
390
- return
391
-
392
- # Type annotation only (x: P) defaults to aggregation
393
- if isinstance (node .parent , nodes .AnnAssign ) and node .parent .value is None :
394
- current = set (parent .aggregations_type [node .attrname ])
395
- parent .aggregations_type [node .attrname ] = list (
396
- current | utils .infer_node (node )
397
- )
398
- return
403
+ element = value .elt
404
+
405
+ # If the element is NOT a Call (no object creation), it's aggregation
406
+ if not isinstance (element , nodes .Call ):
407
+ if isinstance (value , nodes .DictComp ):
408
+ element_type = safe_infer (value .value )
409
+ else :
410
+ element_type = safe_infer (value .elt )
411
+ if element_type :
412
+ current = set (parent .aggregations_type [node .attrname ])
413
+ parent .aggregations_type [node .attrname ] = list (
414
+ current | {element_type }
415
+ )
416
+ return
399
417
400
418
# Not an aggregation, pass to next handler
401
419
super ().handle (node , parent )
@@ -405,7 +423,16 @@ class AssociationsHandler(AbstractAssociationHandler):
405
423
"""Handle regular association relationships."""
406
424
407
425
def handle (self , node : nodes .AssignAttr , parent : nodes .ClassDef ) -> None :
408
- # Everything else is a regular association
426
+ # Type annotation only (x: P) -> Association
427
+ # BUT only if there's no actual assignment (to avoid duplicates)
428
+ if isinstance (node .parent , nodes .AnnAssign ) and node .parent .value is None :
429
+ current = set (parent .associations_type [node .attrname ])
430
+ parent .associations_type [node .attrname ] = list (
431
+ current | utils .infer_node (node )
432
+ )
433
+ return
434
+
435
+ # Everything else is also association (fallback)
409
436
current = set (parent .associations_type [node .attrname ])
410
437
parent .associations_type [node .attrname ] = list (current | utils .infer_node (node ))
411
438
0 commit comments