diff --git a/scripts/dts/python-devicetree/src/devicetree/edtlib.py b/scripts/dts/python-devicetree/src/devicetree/edtlib.py index 8cf3c4b7dd035..036243a0bf6b4 100644 --- a/scripts/dts/python-devicetree/src/devicetree/edtlib.py +++ b/scripts/dts/python-devicetree/src/devicetree/edtlib.py @@ -2404,30 +2404,32 @@ def _process_properties_r(self, root_node: Node, props_node: Node) -> None: """ # A Node depends on any Nodes present in 'phandle', # 'phandles', or 'phandle-array' property values. + # + # When a phandle target is a descendant of root_node, skip adding + # the dependency edge: the tree already has a child-parent edge + # for every child, so adding parent-child would create a cycle. + # The dependency is implicit because the parent already contains it. + + def is_child_node(child_node): + while child_node is not None: + if root_node is child_node: + return True + child_node = child_node.parent + + return False + for prop in props_node.props.values(): if prop.type == 'phandle': - # According to the DT spec, a property named 'phy-handle' is required when - # the Ethernet device is connected a physical layer device (PHY). - # But the 'phy-handle' property can point to a child node of the Ethernet device, - # so we need to check for that and not add a dependency in that case, otherwise - # we'll get a cycle in the graph. - if prop.name == "phy-handle": - def _is_child(parent_node: Node, child_node: Optional[Node]) -> bool: - if child_node is None: - return False - if parent_node is child_node: - return True - return _is_child(parent_node, child_node.parent) - if TYPE_CHECKING: - assert isinstance(prop.val, Node) - if _is_child(props_node, prop.val): - continue - self._graph.add_edge(root_node, prop.val) + if TYPE_CHECKING: + assert isinstance(prop.val, Node) + if not is_child_node(prop.val): + self._graph.add_edge(root_node, prop.val) elif prop.type == 'phandles': if TYPE_CHECKING: assert isinstance(prop.val, list) for phandle_node in prop.val: - self._graph.add_edge(root_node, phandle_node) + if not is_child_node(phandle_node): + self._graph.add_edge(root_node, phandle_node) elif prop.type == 'phandle-array': if TYPE_CHECKING: assert isinstance(prop.val, list) @@ -2436,7 +2438,8 @@ def _is_child(parent_node: Node, child_node: Optional[Node]) -> bool: continue if TYPE_CHECKING: assert isinstance(cd, ControllerAndData) - self._graph.add_edge(root_node, cd.controller) + if not is_child_node(cd.controller): + self._graph.add_edge(root_node, cd.controller) # A Node depends on whatever supports the interrupts it # generates. diff --git a/scripts/dts/python-devicetree/tests/test_edtlib.py b/scripts/dts/python-devicetree/tests/test_edtlib.py index d52bcf12c8775..93277687a1987 100644 --- a/scripts/dts/python-devicetree/tests/test_edtlib.py +++ b/scripts/dts/python-devicetree/tests/test_edtlib.py @@ -939,6 +939,68 @@ def test_child_dependencies(): assert edt.get_node("/child-binding/child-1/grandchild") in dep_node.required_by assert edt.get_node("/child-binding/child-2") in dep_node.required_by +def test_child_phandle_circular_dependency(tmp_path): + '''Test parent phandles to child states do not create dependency cycles.''' + + binding_dir = tmp_path / "bindings" + binding_dir.mkdir() + + binding_file = binding_dir / "test-stm.yaml" + binding_file.write_text(""" +description: Generic child state dependency test + +compatible: "test,stm" + +properties: + states: + type: phandles + +child-binding: + description: state node + properties: + next-state: + type: int +""", encoding="utf-8") + + dts_file = tmp_path / "child-descendant-ref.dts" + dts_file.write_text(""" +/dts-v1/; + +/ { + test_stm { + compatible = "test,stm"; + states = <&state1 &state2 &state3>; + + state1: state1 { + next-state = <2>; + }; + + state2: state2 { + next-state = <3>; + }; + + state3: state3 { + next-state = <1>; + }; + }; +}; +""", encoding="utf-8") + + edt = edtlib.EDT(os.fspath(dts_file), [os.fspath(binding_dir)]) + + parent = edt.get_node("/test_stm") + states = [ + edt.get_node("/test_stm/state1"), + edt.get_node("/test_stm/state2"), + edt.get_node("/test_stm/state3"), + ] + + assert parent.props["states"].val == states + for state in states: + assert parent not in state.required_by + assert state not in parent.depends_on + assert parent in state.depends_on + def test_slice_errs(tmp_path): '''Test error messages from the internal _slice() helper'''