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