Repository: tefra/xsdata
Pull request #485 (2021-05-08 16:44:33)

Vacuum bare inner classes
    Search and vacuum inner classes with no attributes or a single extension.

    Cases:
        1. Removing identical overriding fields can some times leave  a class
           bare with just an extension. For inner classes we can safely
           replace the forward reference with the inner extension reference.
        2. Empty nested complexContent with no restrictions or extensions,
           we can replace these references with xs:anySimpleType

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=485&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=485&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=485&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=CODE_SMELL) [1 Code Smell](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=485) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=485&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=485&metric=new_duplicated_lines_density&view=list)


codecov[bot]: # [Codecov](https://codecov.io/gh/tefra/xsdata/pull/485?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) Report
> Merging [#485](https://codecov.io/gh/tefra/xsdata/pull/485?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) (9fb8dd1) into [master](https://codecov.io/gh/tefra/xsdata/commit/63e423eaaede6e6be75b6008b1672212fe08fa8d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) (63e423e) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/tefra/xsdata/pull/485/graphs/tree.svg?width=650&height=150&src=pr&token=YzDDLtywvl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)](https://codecov.io/gh/tefra/xsdata/pull/485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)

```diff
@@            Coverage Diff            @@
##            master      #485   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           83        84    +1     
  Lines         7323      7349   +26     
  Branches      1315      1321    +6     
=========================================
+ Hits          7323      7349   +26     
```


| [Impacted Files](https://codecov.io/gh/tefra/xsdata/pull/485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) | Coverage Δ | |
|---|---|---|
| [xsdata/codegen/container.py](https://codecov.io/gh/tefra/xsdata/pull/485/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2NvZGVnZW4vY29udGFpbmVyLnB5) | `100.00% <100.00%> (ø)` | |
| [xsdata/codegen/handlers/\_\_init\_\_.py](https://codecov.io/gh/tefra/xsdata/pull/485/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2NvZGVnZW4vaGFuZGxlcnMvX19pbml0X18ucHk=) | `100.00% <100.00%> (ø)` | |
| [xsdata/codegen/handlers/class\_bar\_inner.py](https://codecov.io/gh/tefra/xsdata/pull/485/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2NvZGVnZW4vaGFuZGxlcnMvY2xhc3NfYmFyX2lubmVyLnB5) | `100.00% <100.00%> (ø)` | |
| [xsdata/utils/testing.py](https://codecov.io/gh/tefra/xsdata/pull/485/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL3V0aWxzL3Rlc3RpbmcucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tefra/xsdata/pull/485?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tefra/xsdata/pull/485?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou). Last update [63e423e...9fb8dd1](https://codecov.io/gh/tefra/xsdata/pull/485?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=485&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=485&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=485&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=485&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=485) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=485&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=485&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/xsdata/codegen/container.py b/xsdata/codegen/container.py
index 3e24f920..05f2293e 100644
--- a/xsdata/codegen/container.py
+++ b/xsdata/codegen/container.py
@@ -19,6 +19,7 @@ from xsdata.codegen.handlers import AttributeOverridesHandler
 from xsdata.codegen.handlers import AttributeRestrictionsHandler
 from xsdata.codegen.handlers import AttributeSubstitutionHandler
 from xsdata.codegen.handlers import AttributeTypeHandler
+from xsdata.codegen.handlers import ClassBareInnerHandler
 from xsdata.codegen.handlers import ClassEnumerationHandler
 from xsdata.codegen.handlers import ClassExtensionHandler
 from xsdata.codegen.handlers import ClassNameConflictHandler
@@ -65,6 +66,7 @@ class ClassContainer(ContainerInterface):
             AttributeDefaultValueHandler(self),
             AttributeRestrictionsHandler(),
             AttributeNameConflictHandler(),
+            ClassBareInnerHandler(),
         ]
         if self.config.output.compound_fields:
             self.post_processors.insert(0, AttributeCompoundChoiceHandler())
diff --git a/xsdata/codegen/handlers/__init__.py b/xsdata/codegen/handlers/__init__.py
index 5df6ac21..e3d9e263 100644
--- a/xsdata/codegen/handlers/__init__.py
+++ b/xsdata/codegen/handlers/__init__.py
@@ -1,41 +1,35 @@
-from xsdata.codegen.handlers.attribute_compound_choice import (
-    AttributeCompoundChoiceHandler,
-)
-from xsdata.codegen.handlers.attribute_default_validate import (
-    AttributeDefaultValidateHandler,
-)
-from xsdata.codegen.handlers.attribute_default_value import AttributeDefaultValueHandler
-from xsdata.codegen.handlers.attribute_effective_choice import (
-    AttributeEffectiveChoiceHandler,
-)
-from xsdata.codegen.handlers.attribute_group import AttributeGroupHandler
-from xsdata.codegen.handlers.attribute_merge import AttributeMergeHandler
-from xsdata.codegen.handlers.attribute_mixed_content import AttributeMixedContentHandler
-from xsdata.codegen.handlers.attribute_name_conflict import AttributeNameConflictHandler
-from xsdata.codegen.handlers.attribute_overrides import AttributeOverridesHandler
-from xsdata.codegen.handlers.attribute_restrictions import AttributeRestrictionsHandler
-from xsdata.codegen.handlers.attribute_substitution import AttributeSubstitutionHandler
-from xsdata.codegen.handlers.attribute_type import AttributeTypeHandler
-from xsdata.codegen.handlers.class_enumeration import ClassEnumerationHandler
-from xsdata.codegen.handlers.class_extension import ClassExtensionHandler
-from xsdata.codegen.handlers.class_name_conflict import ClassNameConflictHandler
-
+from .attribute_compound_choice import AttributeCompoundChoiceHandler
+from .attribute_default_validate import AttributeDefaultValidateHandler
+from .attribute_default_value import AttributeDefaultValueHandler
+from .attribute_effective_choice import AttributeEffectiveChoiceHandler
+from .attribute_group import AttributeGroupHandler
+from .attribute_merge import AttributeMergeHandler
+from .attribute_mixed_content import AttributeMixedContentHandler
+from .attribute_name_conflict import AttributeNameConflictHandler
+from .attribute_overrides import AttributeOverridesHandler
+from .attribute_restrictions import AttributeRestrictionsHandler
+from .attribute_substitution import AttributeSubstitutionHandler
+from .attribute_type import AttributeTypeHandler
+from .class_bar_inner import ClassBareInnerHandler
+from .class_enumeration import ClassEnumerationHandler
+from .class_extension import ClassExtensionHandler
+from .class_name_conflict import ClassNameConflictHandler
 
 __all__ = [
-    "ClassEnumerationHandler",
+    "AttributeCompoundChoiceHandler",
+    "AttributeDefaultValidateHandler",
+    "AttributeDefaultValueHandler",
+    "AttributeEffectiveChoiceHandler",
     "AttributeGroupHandler",
     "AttributeMergeHandler",
-    "AttributeOverridesHandler",
     "AttributeMixedContentHandler",
+    "AttributeNameConflictHandler",
+    "AttributeOverridesHandler",
+    "AttributeRestrictionsHandler",
     "AttributeSubstitutionHandler",
     "AttributeTypeHandler",
+    "ClassBareInnerHandler",
+    "ClassEnumerationHandler",
     "ClassExtensionHandler",
-    "AttributeCompoundChoiceHandler",
-    "AttributeRestrictionsHandler",
-    "AttributeDefaultValueHandler",
-    "AttributeRestrictionsHandler",
-    "AttributeNameConflictHandler",
-    "AttributeDefaultValidateHandler",
-    "AttributeEffectiveChoiceHandler",
     "ClassNameConflictHandler",
 ]
diff --git a/xsdata/codegen/handlers/class_bar_inner.py b/xsdata/codegen/handlers/class_bar_inner.py
new file mode 100644
index 00000000..df15919c
--- /dev/null
+++ b/xsdata/codegen/handlers/class_bar_inner.py
@@ -0,0 +1,42 @@
+from xsdata.codegen.mixins import HandlerInterface
+from xsdata.codegen.models import Class
+from xsdata.models.enums import DataType
+
+
+class ClassBareInnerHandler(HandlerInterface):
+    """
+    Search and vacuum inner classes with no attributes or a single extension.
+
+    Cases:
+        1. Removing identical overriding fields can some times leave a class
+           bare with just an extension. For inner classes we can safely
+           replace the forward reference with the inner extension reference.
+        2. Empty nested complexContent with no restrictions or extensions,
+           we can replace these references with xs:anySimpleType
+    """
+
+    def process(self, target: Class):
+        for inner in list(target.inner):
+            if not inner.attrs and len(inner.extensions) < 2:
+                self.remove_inner(target, inner)
+
+    @classmethod
+    def remove_inner(cls, target: Class, inner: Class):
+        target.inner.remove(inner)
+
+        for attr in target.attrs:
+            for attr_type in attr.types:
+
+                if attr_type.forward and attr_type.qname == inner.qname:
+                    attr_type.circular = False
+                    attr_type.forward = False
+
+                    if inner.extensions:
+                        ext = inner.extensions[0]
+                        attr_type.reference = ext.type.reference
+                        attr_type.qname = ext.type.qname
+                        attr_type.native = False
+                    else:
+                        attr_type.native = True
+                        attr_type.qname = str(DataType.ANY_SIMPLE_TYPE)
+                        attr_type.reference = 0


Test patch:
diff --git a/tests/codegen/handlers/test_class_bar_inner.py b/tests/codegen/handlers/test_class_bar_inner.py
new file mode 100644
index 00000000..ddb3be39
--- /dev/null
+++ b/tests/codegen/handlers/test_class_bar_inner.py
@@ -0,0 +1,58 @@
+from xsdata.codegen.handlers import ClassBareInnerHandler
+from xsdata.models.enums import DataType
+from xsdata.utils.testing import ClassFactory
+from xsdata.utils.testing import ExtensionFactory
+from xsdata.utils.testing import FactoryTestCase
+
+
+class ClassBareInnerHandlerTests(FactoryTestCase):
+    def setUp(self):
+        super().setUp()
+        self.processor = ClassBareInnerHandler()
+
+    def test_process(self):
+        target = ClassFactory.elements(3)
+
+        inner_1 = ClassFactory.elements(2)
+        inner_2 = ClassFactory.create(extensions=ExtensionFactory.list(2))
+
+        inner_3 = ClassFactory.create()
+        inner_4 = ClassFactory.create(
+            extensions=[ExtensionFactory.reference("foo", reference=15)]
+        )
+
+        target.inner.extend([inner_1, inner_2, inner_3, inner_4])
+
+        target.attrs[1].types[0].qname = inner_3.qname
+        target.attrs[1].types[0].forward = True
+        target.attrs[1].types[0].reference = 4
+
+        target.attrs[2].types[0].qname = inner_4.qname
+        target.attrs[2].types[0].forward = True
+        target.attrs[2].types[0].circular = True
+        target.attrs[2].types[0].reference = 5
+
+        self.processor.process(target)
+
+        self.assertEqual(2, len(target.inner))
+
+        # Not bare: has attrs
+        self.assertIn(inner_1, target.inner)
+        # Not bare: has more than one extension
+        self.assertIn(inner_2, target.inner)
+
+        self.assertEqual(str(DataType.ANY_SIMPLE_TYPE), target.attrs[1].types[0].qname)
+        self.assertEqual(0, target.attrs[1].types[0].reference)
+        self.assertFalse(target.attrs[1].types[0].forward)
+        self.assertFalse(target.attrs[1].types[0].circular)
+        self.assertTrue(target.attrs[1].types[0].native)
+
+        self.assertEqual(
+            inner_4.extensions[0].type.qname, target.attrs[2].types[0].qname
+        )
+        self.assertEqual(
+            inner_4.extensions[0].type.reference, target.attrs[2].types[0].reference
+        )
+        self.assertFalse(target.attrs[2].types[0].forward)
+        self.assertFalse(target.attrs[2].types[0].circular)
+        self.assertFalse(target.attrs[2].types[0].native)
diff --git a/tests/codegen/test_container.py b/tests/codegen/test_container.py
index 99f1f19c..10791f96 100644
--- a/tests/codegen/test_container.py
+++ b/tests/codegen/test_container.py
@@ -4,7 +4,6 @@ from xsdata.codegen.container import ClassContainer
 from xsdata.codegen.models import Class
 from xsdata.codegen.models import Status
 from xsdata.models.config import GeneratorConfig
-from xsdata.models.config import StructureStyle
 from xsdata.models.enums import Tag
 from xsdata.utils.testing import ClassFactory
 from xsdata.utils.testing import FactoryTestCase
@@ -53,6 +52,7 @@ class ClassContainerTests(FactoryTestCase):
                 "AttributeDefaultValueHandler",
                 "AttributeRestrictionsHandler",
                 "AttributeNameConflictHandler",
+                "ClassBareInnerHandler",
             ],
             [x.__class__.__name__ for x in container.post_processors],
         )
@@ -67,6 +67,7 @@ class ClassContainerTests(FactoryTestCase):
                 "AttributeDefaultValueHandler",
                 "AttributeRestrictionsHandler",
                 "AttributeNameConflictHandler",
+                "ClassBareInnerHandler",
             ],
             [x.__class__.__name__ for x in container.post_processors],
         )
@@ -107,7 +108,9 @@ class ClassContainerTests(FactoryTestCase):
         mock_pre_process_class.assert_called_once_with(first)
 
     def test_pre_process_class(self):
-        target = ClassFactory.create(inner=ClassFactory.list(2))
+        target = ClassFactory.create(
+            inner=[ClassFactory.elements(2), ClassFactory.elements(1)]
+        )
         self.container.add(target)
 
         self.container.process()
diff --git a/xsdata/utils/testing.py b/xsdata/utils/testing.py
index 62f91317..a695c0d7 100644
--- a/xsdata/utils/testing.py
+++ b/xsdata/utils/testing.py
@@ -188,7 +188,10 @@ class ExtensionFactory(Factory):
 
     @classmethod
     def reference(cls, qname: str, **kwargs: Any) -> Extension:
-        return cls.create(AttrTypeFactory.create(qname=qname), **kwargs)
+        restrictions = kwargs.pop("restrictions", None)
+        return cls.create(
+            AttrTypeFactory.create(qname=qname, **kwargs), restrictions=restrictions
+        )
 
     @classmethod
     def native(cls, datatype: DataType, **kwargs: Any) -> Extension: