Repository: tefra/xsdata

Issue:
Child node selects fields already assigned
When an xml node qname matches a model field it's always selected even if has already been assigned to a previous node. In the end the parser auto-corrects this issue but often results into a warning.

```python 
@dataclass
class Zing:
    class Meta:
        name = "zing"

    a: Optional[int] = field(
        default=None,
        metadata={
            "type": "Element"
        }
    )
    local_element: Optional[object] = field(
        default=None,
        metadata={
            "type": "Wildcard",
            "namespace": "##local"
        }
    )
```

```xml
<root>
  <a>23</a>
  <a>2010-10-16</a>
</root>
```




Discussion:
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=396&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=396&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=396&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=396&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=396&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=396&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=396&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=396&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=396&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=396&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=396&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=396&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=396) 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=396&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=396&metric=new_duplicated_lines_density&view=list)


codecov[bot]: # [Codecov](https://codecov.io/gh/tefra/xsdata/pull/396?src=pr&el=h1) Report
> Merging [#396](https://codecov.io/gh/tefra/xsdata/pull/396?src=pr&el=desc) (2745a63) into [master](https://codecov.io/gh/tefra/xsdata/commit/196a2b3af349e0a6939450e697470660d20b65b9?el=desc) (196a2b3) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/tefra/xsdata/pull/396/graphs/tree.svg?width=650&height=150&src=pr&token=YzDDLtywvl)](https://codecov.io/gh/tefra/xsdata/pull/396?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##            master      #396   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           72        72           
  Lines         6392      6399    +7     
  Branches      1116      1118    +2     
=========================================
+ Hits          6392      6399    +7     
```


| [Impacted Files](https://codecov.io/gh/tefra/xsdata/pull/396?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [xsdata/formats/dataclass/parsers/nodes.py](https://codecov.io/gh/tefra/xsdata/pull/396/diff?src=pr&el=tree#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvbm9kZXMucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tefra/xsdata/pull/396?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tefra/xsdata/pull/396?src=pr&el=footer). Last update [196a2b3...2745a63](https://codecov.io/gh/tefra/xsdata/pull/396?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/xsdata/formats/dataclass/parsers/nodes.py b/xsdata/formats/dataclass/parsers/nodes.py
index 52dad649..6d2c12fb 100644
--- a/xsdata/formats/dataclass/parsers/nodes.py
+++ b/xsdata/formats/dataclass/parsers/nodes.py
@@ -7,6 +7,7 @@ from typing import Dict
 from typing import Iterator
 from typing import List
 from typing import Optional
+from typing import Set
 from typing import Tuple
 from typing import Type
 
@@ -56,6 +57,7 @@ class ElementNode(XmlNode):
     position: int
     mixed: bool = False
     derived: bool = False
+    assigned: Set = field(default_factory=set)
 
     def bind(self, qname: str, text: NoneStr, tail: NoneStr, objects: List) -> bool:
         params: Dict = {}
@@ -89,13 +91,19 @@ class ElementNode(XmlNode):
         return True
 
     def child(self, qname: str, attrs: Dict, ns_map: Dict, position: int) -> XmlNode:
-        for var in self.fetch_vars(qname):
-            node = self.build_node(var, attrs, ns_map, position)
-            if node:
-                return node
+        for unique, var in self.fetch_vars(qname):
+            if not unique or unique not in self.assigned:
+                node = self.build_node(var, attrs, ns_map, position)
+
+                if node:
+                    if unique:
+                        self.assigned.add(unique)
+
+                    return node
 
         if self.config.fail_on_unknown_properties:
             raise ParserError(f"Unknown property {self.meta.qname}:{qname}")
+
         return SkipNode()
 
     def build_node(
@@ -132,7 +140,7 @@ class ElementNode(XmlNode):
 
         return PrimitiveNode(var=var, ns_map=ns_map)
 
-    def fetch_vars(self, qname: str) -> Iterator[XmlVar]:
+    def fetch_vars(self, qname: str) -> Iterator[Tuple[Any, XmlVar]]:
         for mode in FIND_MODES:
             var = self.meta.find_var(qname, mode)
 
@@ -140,11 +148,13 @@ class ElementNode(XmlNode):
                 continue
 
             if var.elements:
+                unique = None
                 choice = var.find_choice(qname)
                 assert choice is not None
-                yield choice
+                yield unique, choice
             else:
-                yield var
+                unique = None if var.list_element or var.wildcard else id(var)
+                yield unique, var
 
     def build_element_node(
         self,


Test patch:
diff --git a/tests/formats/dataclass/parsers/test_nodes.py b/tests/formats/dataclass/parsers/test_nodes.py
index e99ad430..d73c5d53 100644
--- a/tests/formats/dataclass/parsers/test_nodes.py
+++ b/tests/formats/dataclass/parsers/test_nodes.py
@@ -264,7 +264,7 @@ class ElementNodeTests(TestCase):
 
         matching_vars = self.node.fetch_vars("a")
         self.assertIsInstance(matching_vars, Generator)
-        self.assertEqual([elem, wild], list(matching_vars))
+        self.assertEqual([(id(elem), elem), (None, wild)], list(matching_vars))
 
     def test_fetch_vars_with_elements_var(self):
         elem = XmlVar(element=True, name="a", qname="a", types=[Foo], dataclass=True)
@@ -273,7 +273,7 @@ class ElementNodeTests(TestCase):
 
         matching_vars = self.node.fetch_vars("a")
         self.assertIsInstance(matching_vars, Generator)
-        self.assertEqual(elem, next(matching_vars))
+        self.assertEqual((None, elem), next(matching_vars))
 
     @mock.patch.object(ElementNode, "fetch_vars")
     def test_child(self, mock_match_vars):
@@ -281,7 +281,7 @@ class ElementNodeTests(TestCase):
         attrs = {"a": "b"}
         ns_map = {"ns0": "xsdata"}
         position = 1
-        mock_match_vars.return_value = [var]
+        mock_match_vars.return_value = [(id(var), var)]
 
         actual = self.node.child("a", attrs, ns_map, position)
         self.assertIsInstance(actual, ElementNode)
@@ -289,6 +289,25 @@ class ElementNodeTests(TestCase):
         self.assertEqual(ns_map, actual.ns_map)
         self.assertEqual(position, actual.position)
 
+    def test_child_unique_vars(self):
+        single = XmlVar(element=True, name="a", qname="a", types=[Foo], dataclass=True)
+        wildcard = XmlVar(wildcard=True, name="a", qname="a", types=[object])
+
+        self.meta.vars.append(single)
+        self.meta.vars.append(wildcard)
+
+        attrs = {"a": "b"}
+        ns_map = {"ns0": "xsdata"}
+        position = 1
+
+        actual = self.node.child("a", attrs, ns_map, position)
+        self.assertIsInstance(actual, ElementNode)
+        self.assertIn(id(single), self.node.assigned)
+
+        actual = self.node.child("a", attrs, ns_map, position)
+        self.assertIsInstance(actual, WildcardNode)
+        self.assertNotIn(id(wildcard), self.node.assigned)
+
     @mock.patch.object(ElementNode, "build_node")
     def test_child_when_failed_to_build_next_node(self, mock_build_node):
         mock_build_node.return_value = None