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