@@ -122,8 +122,14 @@ def __init__(self, project: Project, tag: bool = False) -> None:
122
122
self .tag = tag
123
123
# visited project
124
124
self .project = project
125
- self .associations_handler = AggregationsHandler ()
126
- self .associations_handler .set_next (OtherAssociationsHandler ())
125
+
126
+ # Chain: Composition → Aggregation → Association
127
+ self .associations_handler = CompositionsHandler ()
128
+ aggregation_handler = AggregationsHandler ()
129
+ association_handler = AssociationsHandler ()
130
+
131
+ self .associations_handler .set_next (aggregation_handler )
132
+ aggregation_handler .set_next (association_handler )
127
133
128
134
def visit_project (self , node : Project ) -> None :
129
135
"""Visit a pyreverse.utils.Project node.
@@ -167,6 +173,7 @@ def visit_classdef(self, node: nodes.ClassDef) -> None:
167
173
specializations .append (node )
168
174
baseobj .specializations = specializations
169
175
# resolve instance attributes
176
+ node .compositions_type = collections .defaultdict (list )
170
177
node .instance_attrs_type = collections .defaultdict (list )
171
178
node .aggregations_type = collections .defaultdict (list )
172
179
node .associations_type = collections .defaultdict (list )
@@ -327,28 +334,50 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
327
334
self ._next_handler .handle (node , parent )
328
335
329
336
337
+ class CompositionsHandler (AbstractAssociationHandler ):
338
+ """Handle composition relationships where parent creates child objects."""
339
+
340
+ def handle (self , node : nodes .AssignAttr , parent : nodes .ClassDef ) -> None :
341
+ if not isinstance (node .parent , (nodes .AnnAssign , nodes .Assign )):
342
+ super ().handle (node , parent )
343
+ return
344
+
345
+ value = node .parent .value
346
+
347
+ # Composition: parent creates child (self.x = P())
348
+ if isinstance (value , nodes .Call ):
349
+ current = set (parent .compositions_type [node .attrname ])
350
+ parent .compositions_type [node .attrname ] = list (
351
+ current | utils .infer_node (node )
352
+ )
353
+ return
354
+
355
+ # Not a composition, pass to next handler
356
+ super ().handle (node , parent )
357
+
358
+
330
359
class AggregationsHandler (AbstractAssociationHandler ):
360
+ """Handle aggregation relationships where parent receives child objects."""
361
+
331
362
def handle (self , node : nodes .AssignAttr , parent : nodes .ClassDef ) -> None :
332
- # Check if we're not in an assignment context
333
363
if not isinstance (node .parent , (nodes .AnnAssign , nodes .Assign )):
334
364
super ().handle (node , parent )
335
365
return
336
366
337
367
value = node .parent .value
338
368
339
- # Handle direct name assignments
369
+ # Aggregation: parent receives child (self.x = x)
340
370
if isinstance (value , astroid .node_classes .Name ):
341
371
current = set (parent .aggregations_type [node .attrname ])
342
372
parent .aggregations_type [node .attrname ] = list (
343
373
current | utils .infer_node (node )
344
374
)
345
375
return
346
376
347
- # Handle comprehensions
377
+ # Aggregation: comprehensions (self.x = [P() for ...])
348
378
if isinstance (
349
379
value , (nodes .ListComp , nodes .DictComp , nodes .SetComp , nodes .GeneratorExp )
350
380
):
351
- # Determine the type of the element in the comprehension
352
381
if isinstance (value , nodes .DictComp ):
353
382
element_type = safe_infer (value .value )
354
383
else :
@@ -358,12 +387,23 @@ def handle(self, node: nodes.AssignAttr, parent: nodes.ClassDef) -> None:
358
387
parent .aggregations_type [node .attrname ] = list (current | {element_type })
359
388
return
360
389
361
- # Fallback to parent handler
390
+ # Type annotation only (x: P) defaults to aggregation
391
+ if isinstance (node .parent , nodes .AnnAssign ) and node .parent .value is None :
392
+ current = set (parent .aggregations_type [node .attrname ])
393
+ parent .aggregations_type [node .attrname ] = list (
394
+ current | utils .infer_node (node )
395
+ )
396
+ return
397
+
398
+ # Not an aggregation, pass to next handler
362
399
super ().handle (node , parent )
363
400
364
401
365
- class OtherAssociationsHandler (AbstractAssociationHandler ):
402
+ class AssociationsHandler (AbstractAssociationHandler ):
403
+ """Handle regular association relationships."""
404
+
366
405
def handle (self , node : nodes .AssignAttr , parent : nodes .ClassDef ) -> None :
406
+ # Everything else is a regular association
367
407
current = set (parent .associations_type [node .attrname ])
368
408
parent .associations_type [node .attrname ] = list (current | utils .infer_node (node ))
369
409
0 commit comments