Repository: tefra/xsdata
Pull request #987 (2024-03-22 09:06:54)

perf: Update all class references in one pass
## 📒 Description

The rename duplicate classes handler is quite slow for big suites with lots of types with duplicate class names. We can decrease that processing time dramatically if we update all the merged/renamed references in one pass and we also cache a few things.

Resolves #xxxx

## 🔗 What I've Done

> Write a description of the steps taken to resolve the issue

## 💬 Comments

> A place to write any comments to the reviewer.

## 🛫 Checklist

- [ ] Updated docs
- [ ] Added unit-tests
- [ ] [Sample tests](https://github.com/tefra/xsdata-samples) pass
- [ ] [W3C tests](https://github.com/tefra/xsdata-w3c-tests) pass


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=987) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=987&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=987&metric=new_accepted_issues&view=list)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=987&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=987&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=987)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/tefra/xsdata/pull/987?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.94%. Comparing base [(`ce27182`)](https://app.codecov.io/gh/tefra/xsdata/commit/ce27182ea31be70631c31f01f2f9af8265b94772?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) to head [(`9b90b11`)](https://app.codecov.io/gh/tefra/xsdata/pull/987?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #987      +/-   ##
==========================================
- Coverage   99.94%   99.94%   -0.01%     
==========================================
  Files         117      117              
  Lines        9140     9128      -12     
  Branches     2174     2166       -8     
==========================================
- Hits         9135     9123      -12     
  Misses          4        4              
  Partials        1        1              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tefra/xsdata/pull/987?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=987) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=987&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=987&metric=new_accepted_issues&view=list)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=987&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=987&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=987)



Patch:
diff --git a/xsdata/codegen/handlers/rename_duplicate_classes.py b/xsdata/codegen/handlers/rename_duplicate_classes.py
index dcd41ffb..844c94a7 100644
--- a/xsdata/codegen/handlers/rename_duplicate_classes.py
+++ b/xsdata/codegen/handlers/rename_duplicate_classes.py
@@ -1,9 +1,8 @@
-from typing import List, Set
+from typing import Dict, List, Set
 
-from xsdata.codegen.mixins import ContainerHandlerInterface
+from xsdata.codegen.mixins import ContainerHandlerInterface, ContainerInterface
 from xsdata.codegen.models import (
     Attr,
-    AttrType,
     Class,
     get_location,
     get_name,
@@ -16,14 +15,31 @@ REQUIRE_UNIQUE_NAMES = (StructureStyle.SINGLE_PACKAGE, StructureStyle.CLUSTERS)
 
 
 class RenameDuplicateClasses(ContainerHandlerInterface):
-    """Resolve class name conflicts depending on the output structure style."""
+    """Resolve class name conflicts depending on the output structure style.
 
-    __slots__ = ()
+    Args:
+        container: The class container instance
+
+    Attributes:
+        use_names: Whether names or qualified names should be used to group classes
+        merges: The renamed instructions applied at the end
+        merges: The merge instructions applied at the end
+        reserved: The reserved class names or qualified names
+    """
+
+    __slots__ = ("use_names", "renames", "merges", "reserved")
+
+    def __init__(self, container: ContainerInterface):
+        super().__init__(container)
+
+        self.use_names = self.should_use_names()
+        self.renames: Dict[int, str] = {}
+        self.merges: Dict[int, int] = {}
+        self.reserved: Set[str] = set()
 
     def run(self):
         """Detect and resolve class name conflicts."""
-        use_name = self.should_use_names()
-        getter = get_name if use_name else get_qname
+        getter = get_name if self.use_names else get_qname
         groups = collections.group_by(self.container, lambda x: text.alnum(getter(x)))
 
         for classes in groups.values():
@@ -33,7 +49,11 @@ class RenameDuplicateClasses(ContainerHandlerInterface):
             if all(x == classes[0] for x in classes):
                 self.merge_classes(classes)
             else:
-                self.rename_classes(classes, use_name)
+                self.rename_classes(classes)
+
+        if self.renames or self.merges:
+            for target in self.container:
+                self.update_references(target)
 
     def should_use_names(self) -> bool:
         """Determine if names or qualified names should be used for detection.
@@ -57,12 +77,11 @@ class RenameDuplicateClasses(ContainerHandlerInterface):
         keep = classes.pop()
         replace = keep.ref
         self.container.remove(*classes)
-        search = {item.ref for item in classes}
 
-        for item in self.container:
-            self.update_class_references(item, search, replace)
+        for item in classes:
+            self.merges[item.ref] = replace
 
-    def rename_classes(self, classes: List[Class], use_name: bool):
+    def rename_classes(self, classes: List[Class]):
         """Rename the classes in the list.
 
         Cases:
@@ -72,8 +91,6 @@ class RenameDuplicateClasses(ContainerHandlerInterface):
 
         Args:
             classes: A list of classes with duplicate names
-            use_name: Whether simple or qualified names should be used
-                during renaming
         """
         abstract = [x for x in classes if x.abstract]
         if len(classes) == 2 and len(abstract) == 1:
@@ -82,14 +99,14 @@ class RenameDuplicateClasses(ContainerHandlerInterface):
             total_elements = sum(x.is_element for x in classes)
             for target in sorted(classes, key=get_name):
                 if not target.is_element or total_elements > 1:
-                    self.add_numeric_suffix(target, use_name)
+                    self.add_numeric_suffix(target)
 
     def add_abstract_suffix(self, target: Class):
         """Add the abstract suffix to class name."""
         new_qname = f"{target.qname}_abstract"
         self.rename_class(target, new_qname)
 
-    def add_numeric_suffix(self, target: Class, use_name: bool):
+    def add_numeric_suffix(self, target: Class):
         """Find the next available class name.
 
         Save the original name in the class metadata and update
@@ -102,7 +119,7 @@ class RenameDuplicateClasses(ContainerHandlerInterface):
                 used during renaming
         """
         namespace, name = namespaces.split_qname(target.qname)
-        new_qname = self.next_qname(namespace, name, use_name)
+        new_qname = self.next_qname(namespace, name)
         self.rename_class(target, new_qname)
 
     def rename_class(self, target: Class, new_qname: str):
@@ -117,97 +134,53 @@ class RenameDuplicateClasses(ContainerHandlerInterface):
         target.meta_name = namespaces.local_name(qname)
 
         self.container.reset(target, qname)
+        self.renames[target.ref] = new_qname
 
-        for item in self.container:
-            self.rename_class_dependencies(item, id(target), new_qname)
-
-    def next_qname(self, namespace: str, name: str, use_name: bool) -> str:
+    def next_qname(self, namespace: str, name: str) -> str:
         """Use int suffixes to get the next available qualified name.
 
         Args:
             namespace: The class namespace
             name: The class name
-            use_name: Whether simple or qualified names should be
-                used during renaming
         """
         index = 0
-
-        if use_name:
-            reserved = {text.alnum(obj.name) for obj in self.container}
-        else:
-            reserved = {text.alnum(obj.qname) for obj in self.container}
+        reserved = self.get_reserved()
 
         while True:
             index += 1
             new_name = f"{name}_{index}"
             qname = namespaces.build_qname(namespace, new_name)
-            cmp = text.alnum(new_name if use_name else qname)
+            cmp = text.alnum(new_name if self.use_names else qname)
 
             if cmp not in reserved:
+                reserved.add(cmp)
                 return qname
 
-    def update_class_references(self, target: Class, search: Set[int], replace: int):
-        """Go through all class types and update all references.
-
-        Args:
-            target: The target class instance to update
-            search: A set of class references to find
-            replace: The new class reference to replace
-        """
-
-        def update_maybe(attr_type: AttrType):
-            if attr_type.reference in search:
-                attr_type.reference = replace
-
-        for attr in target.attrs:
-            for tp in attr.types:
-                update_maybe(tp)
-
-            for choice in attr.choices:
-                for tp in choice.types:
-                    update_maybe(tp)
-
-        for ext in target.extensions:
-            update_maybe(ext.type)
+    def get_reserved(self) -> Set[str]:
+        """Build the reserved names or qualified names of the container."""
+        if not self.reserved:
+            getter = get_name if self.use_names else get_qname
+            self.reserved = {text.alnum(getter(obj)) for obj in self.container}
 
-        for inner in target.inner:
-            self.update_class_references(inner, search, replace)
+        return self.reserved
 
-    def rename_class_dependencies(self, target: Class, reference: int, replace: str):
-        """Search and replace the old qualified class name in all classes.
+    def update_references(self, target: Class):
+        """Search and update the target class for renamed and merged references.
 
         Args:
-            target: The target class instance to inspect
-            reference: The reference id of the renamed class
-            replace: The new qualified name of the renamed class
+            target: The target class instance to inspect and update
         """
-        for attr in target.attrs:
-            self.rename_attr_dependencies(attr, reference, replace)
-
-        for ext in target.extensions:
-            if ext.type.reference == reference:
-                ext.type.qname = replace
-
-        for inner in target.inner:
-            self.rename_class_dependencies(inner, reference, replace)
-
-    def rename_attr_dependencies(self, attr: Attr, reference: int, replace: str):
-        """Search and replace the old qualified class name in the attr types.
-
-        This also covers any choices and references to enum values.
-
-        Args:
-            attr: The target attr instance to inspect
-            reference: The reference id of the renamed class
-            replace: The new qualified name of the renamed class
-        """
-        for attr_type in attr.types:
-            if attr_type.reference == reference:
-                attr_type.qname = replace
-
-                if isinstance(attr.default, str) and attr.default.startswith("@enum@"):
-                    members = text.suffix(attr.default, "::")
-                    attr.default = f"@enum@{replace}::{members}"
-
-        for choice in attr.choices:
-            self.rename_attr_dependencies(choice, reference, replace)
+        for parent, tp in target.types_with_parents():
+            if tp.reference in self.renames:
+                tp.qname = self.renames[tp.reference]
+
+                if (
+                    isinstance(parent, Attr)
+                    and isinstance(parent.default, str)
+                    and parent.default.startswith("@enum@")
+                ):
+                    members = text.suffix(parent.default, "::")
+                    parent.default = f"@enum@{tp.qname}::{members}"
+
+            elif tp.reference in self.merges:
+                tp.reference = self.merges[tp.reference]
diff --git a/xsdata/codegen/models.py b/xsdata/codegen/models.py
index 83035595..bd1cf95c 100644
--- a/xsdata/codegen/models.py
+++ b/xsdata/codegen/models.py
@@ -652,17 +652,24 @@ class Class(CodegenModel):
 
     def types(self) -> Iterator[AttrType]:
         """Yields all class types."""
+        for _, tp in self.types_with_parents():
+            yield tp
+
+    def types_with_parents(self) -> Iterator[Tuple[CodegenModel, AttrType]]:
+        """Yields all class types with their parent codegen instance."""
         for ext in self.extensions:
-            yield ext.type
+            yield ext, ext.type
 
         for attr in self.attrs:
-            yield from attr.types
+            for tp in attr.types:
+                yield attr, tp
 
             for choice in attr.choices:
-                yield from choice.types
+                for tp in choice.types:
+                    yield choice, tp
 
         for inner in self.inner:
-            yield from inner.types()
+            yield from inner.types_with_parents()
 
     def children(self) -> Iterator[CodegenModel]:
         """Yield all codegen children."""


Test patch:
diff --git a/tests/codegen/handlers/test_rename_duplicate_classes.py b/tests/codegen/handlers/test_rename_duplicate_classes.py
index cbc48532..357ef3c7 100644
--- a/tests/codegen/handlers/test_rename_duplicate_classes.py
+++ b/tests/codegen/handlers/test_rename_duplicate_classes.py
@@ -2,13 +2,11 @@ from unittest import mock
 
 from xsdata.codegen.container import ClassContainer
 from xsdata.codegen.handlers import RenameDuplicateClasses
-from xsdata.models.config import GeneratorConfig, StructureStyle
-from xsdata.models.enums import Tag
+from xsdata.models.config import GeneratorConfig
+from xsdata.models.enums import DataType, Tag
 from xsdata.utils.testing import (
     AttrFactory,
-    AttrTypeFactory,
     ClassFactory,
-    ExtensionFactory,
     FactoryTestCase,
 )
 
@@ -20,91 +18,30 @@ class RenameDuplicateClassesTests(FactoryTestCase):
         self.container = ClassContainer(config=GeneratorConfig())
         self.processor = RenameDuplicateClasses(container=self.container)
 
-    @mock.patch.object(RenameDuplicateClasses, "merge_classes")
-    @mock.patch.object(RenameDuplicateClasses, "rename_classes")
-    def test_run(self, mock_rename_classes, mock_merge_classes):
+    def test_run_renames_classes_with_duplicate_qnames(self):
         classes = [
-            ClassFactory.create(qname="{foo}A"),
-            ClassFactory.create(qname="{foo}a"),
-            ClassFactory.create(qname="_a"),
-            ClassFactory.create(qname="_b"),
-            ClassFactory.create(qname="b", location="!@#$"),
-        ]
-        self.container.extend(classes)
-        self.processor.run()
-
-        mock_rename_classes.assert_has_calls(
-            [
-                mock.call(classes[:2], False),
-                mock.call(classes[3:], False),
-            ]
-        )
-        self.assertEqual(0, mock_merge_classes.call_count)
-
-    @mock.patch.object(RenameDuplicateClasses, "merge_classes")
-    @mock.patch.object(RenameDuplicateClasses, "rename_classes")
-    def test_run_with_single_package_structure(
-        self, mock_rename_classes, mock_merge_classes
-    ):
-        classes = [
-            ClassFactory.create(qname="{foo}a"),
-            ClassFactory.create(qname="{bar}a"),
-            ClassFactory.create(qname="a"),
-        ]
-        self.container.extend(classes)
-        self.processor.run()
-
-        mock_rename_classes.assert_called_once_with(classes, True)
-        self.assertEqual(0, mock_merge_classes.call_count)
-
-    @mock.patch.object(RenameDuplicateClasses, "merge_classes")
-    @mock.patch.object(RenameDuplicateClasses, "rename_classes")
-    def test_run_with_single_location_source(
-        self, mock_rename_classes, mock_merge_classes
-    ):
-        classes = [
-            ClassFactory.create(qname="{foo}a"),
-            ClassFactory.create(qname="{bar}a"),
-            ClassFactory.create(qname="a"),
-        ]
-
-        self.container.config.output.structure_style = StructureStyle.SINGLE_PACKAGE
-        self.container.extend(classes)
-        self.processor.run()
-
-        mock_rename_classes.assert_called_once_with(classes, True)
-        self.assertEqual(0, mock_merge_classes.call_count)
-
-    @mock.patch.object(RenameDuplicateClasses, "merge_classes")
-    @mock.patch.object(RenameDuplicateClasses, "rename_classes")
-    def test_run_with_clusters_structure(self, mock_rename_classes, mock_merge_classes):
-        classes = [
-            ClassFactory.create(qname="{foo}a"),
-            ClassFactory.create(qname="{bar}a"),
-            ClassFactory.create(qname="a"),
+            ClassFactory.create(qname="{foo}A", tag=Tag.ELEMENT),
+            ClassFactory.create(qname="{foo}a", tag=Tag.ELEMENT),
+            ClassFactory.create(qname="_a", tag=Tag.ELEMENT),
+            ClassFactory.create(qname="_b", tag=Tag.ELEMENT),
+            ClassFactory.create(qname="b", location="!@#$", tag=Tag.ELEMENT),
         ]
-        self.container.config.output.structure_style = StructureStyle.CLUSTERS
         self.container.extend(classes)
         self.processor.run()
 
-        mock_rename_classes.assert_called_once_with(classes, True)
-        self.assertEqual(0, mock_merge_classes.call_count)
-
-    @mock.patch.object(RenameDuplicateClasses, "merge_classes")
-    @mock.patch.object(RenameDuplicateClasses, "rename_classes")
-    def test_run_with_same_classes(self, mock_rename_classes, mock_merge_classes):
-        first = ClassFactory.create()
-        second = first.clone()
-        third = ClassFactory.create()
-
-        self.container.extend([first, second, third])
-        self.processor.run()
-
-        self.assertEqual(0, mock_rename_classes.call_count)
-        mock_merge_classes.assert_called_once_with([first, second])
-
-    @mock.patch.object(RenameDuplicateClasses, "update_class_references")
-    def test_merge_classes(self, mock_update_class_references):
+        expected = {
+            classes[0].ref: "{foo}A_1",
+            classes[1].ref: "{foo}a_2",
+            classes[3].ref: "_b_1",
+            classes[4].ref: "b_2",
+        }
+        self.assertEqual(expected, self.processor.renames)
+        self.assertEqual("{foo}A_1", classes[0].qname)
+        self.assertEqual("{foo}a_2", classes[1].qname)
+        self.assertEqual("_b_1", classes[3].qname)
+        self.assertEqual("b_2", classes[4].qname)
+
+    def test_run_merges_same_classes(self):
         first = ClassFactory.create()
         second = first.clone()
         third = first.clone()
@@ -114,34 +51,10 @@ class RenameDuplicateClassesTests(FactoryTestCase):
         self.container.extend([first, second, third, fourth, fifth])
         self.processor.run()
 
-        search = {first.ref, second.ref}
-        replace = third.ref
-
-        mock_update_class_references.assert_has_calls(
-            [
-                mock.call(first, search, replace),
-                mock.call(fourth, search, replace),
-                mock.call(fifth, search, replace),
-            ]
-        )
         self.assertEqual([first, fourth, fifth], list(self.container))
-
-    def test_update_class_references(self):
-        replacements = {1, 2, 3, 4}
-        target = ClassFactory.create(
-            attrs=AttrFactory.list(3),
-            extensions=ExtensionFactory.list(2),
-            inner=[ClassFactory.elements(2), ClassFactory.create()],
+        self.assertEqual(
+            {first.ref: third.ref, second.ref: third.ref}, self.processor.merges
         )
-        target.attrs[1].choices = AttrFactory.list(2)
-
-        target.attrs[0].types[0].reference = 1
-        target.attrs[1].choices[0].types[0].reference = 2
-        target.extensions[1].type.reference = 3
-        target.inner[0].attrs[0].types[0].reference = 4
-
-        self.processor.update_class_references(target, replacements, 5)
-        self.assertEqual([5, 5, 5, 5], list(target.references))
 
     @mock.patch.object(RenameDuplicateClasses, "add_numeric_suffix")
     def test_rename_classes(self, mock_add_numeric_suffix):
@@ -150,17 +63,17 @@ class RenameDuplicateClassesTests(FactoryTestCase):
             ClassFactory.create(qname="_A", tag=Tag.ELEMENT),
             ClassFactory.create(qname="a", tag=Tag.COMPLEX_TYPE),
         ]
-        self.processor.rename_classes(classes, False)
-        self.processor.rename_classes(classes, True)
+        self.processor.rename_classes(classes)
+        self.processor.rename_classes(classes)
 
         mock_add_numeric_suffix.assert_has_calls(
             [
-                mock.call(classes[1], False),
-                mock.call(classes[0], False),
-                mock.call(classes[2], False),
-                mock.call(classes[1], True),
-                mock.call(classes[0], True),
-                mock.call(classes[2], True),
+                mock.call(classes[1]),
+                mock.call(classes[0]),
+                mock.call(classes[2]),
+                mock.call(classes[1]),
+                mock.call(classes[0]),
+                mock.call(classes[2]),
             ]
         )
 
@@ -170,7 +83,7 @@ class RenameDuplicateClassesTests(FactoryTestCase):
             ClassFactory.create(qname="_a", tag=Tag.ELEMENT),
             ClassFactory.create(qname="_A", tag=Tag.ELEMENT, abstract=True),
         ]
-        self.processor.rename_classes(classes, True)
+        self.processor.rename_classes(classes)
 
         mock_add_abstract_suffix.assert_called_once_with(classes[1])
 
@@ -180,49 +93,40 @@ class RenameDuplicateClassesTests(FactoryTestCase):
             ClassFactory.create(qname="_a", tag=Tag.ELEMENT),
             ClassFactory.create(qname="a", tag=Tag.COMPLEX_TYPE),
         ]
-        self.processor.rename_classes(classes, False)
+        self.processor.rename_classes(classes)
 
-        mock_rename_class.assert_called_once_with(classes[1], False)
+        mock_rename_class.assert_called_once_with(classes[1])
 
-    @mock.patch.object(RenameDuplicateClasses, "rename_class_dependencies")
-    def test_add_numeric_suffix_by_slug(self, mock_rename_class_dependencies):
+    def test_add_numeric_suffix_by_slug(self):
         target = ClassFactory.create(qname="{foo}_a")
         self.processor.container.add(target)
         self.processor.container.add(ClassFactory.create(qname="{foo}a_1"))
         self.processor.container.add(ClassFactory.create(qname="{foo}A_2"))
         self.processor.container.add(ClassFactory.create(qname="{bar}a_3"))
-        self.processor.add_numeric_suffix(target, False)
+        self.processor.add_numeric_suffix(target)
 
         self.assertEqual("{foo}_a_3", target.qname)
         self.assertEqual("_a", target.meta_name)
 
-        mock_rename_class_dependencies.assert_has_calls(
-            mock.call(item, id(target), "{foo}_a_3")
-            for item in self.processor.container
-        )
-
         self.assertEqual([target], self.container.data["{foo}_a_3"])
         self.assertEqual([], self.container.data["{foo}_a"])
+        self.assertEqual({target.ref: target.qname}, self.processor.renames)
 
-    @mock.patch.object(RenameDuplicateClasses, "rename_class_dependencies")
-    def test_add_numeric_suffix_by_name(self, mock_rename_class_dependencies):
+    def test_add_numeric_suffix_by_name(self):
         target = ClassFactory.create(qname="{foo}_a")
+        self.processor.use_names = True
         self.processor.container.add(target)
         self.processor.container.add(ClassFactory.create(qname="{bar}a_1"))
         self.processor.container.add(ClassFactory.create(qname="{thug}A_2"))
         self.processor.container.add(ClassFactory.create(qname="{bar}a_3"))
-        self.processor.add_numeric_suffix(target, True)
+        self.processor.add_numeric_suffix(target)
 
         self.assertEqual("{foo}_a_4", target.qname)
         self.assertEqual("_a", target.meta_name)
 
-        mock_rename_class_dependencies.assert_has_calls(
-            mock.call(item, id(target), "{foo}_a_4")
-            for item in self.processor.container
-        )
-
         self.assertEqual([target], self.container.data["{foo}_a_4"])
         self.assertEqual([], self.container.data["{foo}_a"])
+        self.assertEqual({target.ref: target.qname}, self.processor.renames)
 
     def test_add_abstract_suffix(self):
         target = ClassFactory.create(qname="{xsdata}line", abstract=True)
@@ -232,67 +136,34 @@ class RenameDuplicateClassesTests(FactoryTestCase):
 
         self.assertEqual("{xsdata}line_abstract", target.qname)
         self.assertEqual("line", target.meta_name)
-
-    def test_rename_class_dependencies(self):
-        attr_type = AttrTypeFactory.create(qname="{foo}bar", reference=1)
-
-        target = ClassFactory.create(
-            extensions=[
-                ExtensionFactory.create(),
-                ExtensionFactory.create(attr_type.clone()),
-            ],
-            attrs=[
-                AttrFactory.create(),
-                AttrFactory.create(types=[AttrTypeFactory.create(), attr_type.clone()]),
-            ],
-            inner=[
-                ClassFactory.create(
-                    extensions=[ExtensionFactory.create(attr_type.clone())],
-                    attrs=[
-                        AttrFactory.create(),
-                        AttrFactory.create(
-                            types=[AttrTypeFactory.create(), attr_type.clone()]
-                        ),
-                    ],
-                )
-            ],
-        )
-
-        self.processor.rename_class_dependencies(target, 1, "thug")
-        dependencies = set(target.dependencies())
-        self.assertNotIn("{foo}bar", dependencies)
-        self.assertIn("thug", dependencies)
-
-    def test_rename_attr_dependencies_with_default_enum(self):
-        attr_type = AttrTypeFactory.create(qname="{foo}bar", reference=1)
-        target = ClassFactory.create(
-            attrs=[
-                AttrFactory.create(
-                    types=[attr_type],
-                    default=f"@enum@{attr_type.qname}::member",
-                ),
-            ]
+        self.assertEqual({target.ref: target.qname}, self.processor.renames)
+
+    def test_update_references(self):
+        target = ClassFactory.create()
+        target.attrs.append(AttrFactory.reference(qname="foo", reference=1))
+        target.attrs.append(
+            AttrFactory.reference(
+                qname="bar",
+                reference=2,
+            )
         )
+        target.attrs[1].default = "@enum@bar::member"
 
-        self.processor.rename_class_dependencies(target, 1, "thug")
-        dependencies = set(target.dependencies())
-        self.assertEqual("@enum@thug::member", target.attrs[0].default)
-        self.assertNotIn("{foo}bar", dependencies)
-        self.assertIn("thug", dependencies)
-
-    def test_rename_attr_dependencies_with_choices(self):
-        attr_type = AttrTypeFactory.create(qname="foo", reference=1)
-        target = ClassFactory.create(
-            attrs=[
-                AttrFactory.create(
-                    choices=[
-                        AttrFactory.create(types=[attr_type.clone()]),
-                    ]
-                )
-            ]
+        target.attrs.append(
+            AttrFactory.reference(
+                qname="thug",
+                reference=3,
+            )
         )
-
-        self.processor.rename_class_dependencies(target, 1, "bar")
-        dependencies = set(target.dependencies())
-        self.assertNotIn("foo", dependencies)
-        self.assertIn("bar", dependencies)
+        target.attrs.append(AttrFactory.native(DataType.STRING))
+
+        self.processor.renames = {1: "bar", 2: "foo"}
+        self.processor.merges = {3: 4}
+        self.processor.update_references(target)
+
+        self.assertEqual("bar", target.attrs[0].types[0].qname)
+        self.assertEqual("foo", target.attrs[1].types[0].qname)
+        self.assertEqual("@enum@foo::member", target.attrs[1].default)
+        self.assertEqual("thug", target.attrs[2].types[0].qname)
+        self.assertEqual(4, target.attrs[2].types[0].reference)
+        self.assertEqual(0, target.attrs[3].types[0].reference)