From cac084cf3dc67aaa19fada74fb197a4181188d47 Mon Sep 17 00:00:00 2001 From: Vladimir Babin Date: Fri, 25 Sep 2026 11:36:48 +0300 Subject: [PATCH] fix(manifest_ingest): resolve inherited groupId and ${...} properties in pom.xml (#3806) (cherry picked from commit 292b9d8465474a6037cfc743d19712ce9588afff) --- graphify/manifest_ingest.py | 30 +++++++++++++++++++++++---- tests/test_manifest_ingest.py | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/graphify/manifest_ingest.py b/graphify/manifest_ingest.py index 313465334..a36071930 100644 --- a/graphify/manifest_ingest.py +++ b/graphify/manifest_ingest.py @@ -289,17 +289,39 @@ def _parse_pom(text: str) -> dict | None: text = re.sub(r'\sxmlns="[^"]*"', '', text, count=1) root = ET.fromstring(text) aid = root.findtext("artifactId") - gid = root.findtext("groupId") + gid = root.findtext("groupId") or root.findtext("parent/groupId") + version = root.findtext("version") or root.findtext("parent/version") if not aid: return None + props: dict[str, str] = {} + for prop in root.findall("properties/*"): + if isinstance(prop.tag, str) and prop.text: + props[prop.tag] = prop.text.strip() + for key, value in ( + ("project.groupId", gid), + ("project.artifactId", aid), + ("project.version", version), + ("project.parent.groupId", root.findtext("parent/groupId")), + ("project.parent.version", root.findtext("parent/version")), + ): + if value: + props.setdefault(key, value.strip()) + + def _resolve(value: str | None) -> str | None: + if not value: + return value + return re.sub(r"\$\{([^}]+)\}", lambda m: props.get(m.group(1), m.group(0)), value.strip()) + + gid = _resolve(gid) + version = _resolve(version) name = f"{gid}:{aid}" if gid else aid deps: list[str] = [] for dep in root.findall(".//dependencies/dependency"): - da = dep.findtext("artifactId") - dg = dep.findtext("groupId") + da = _resolve(dep.findtext("artifactId")) + dg = _resolve(dep.findtext("groupId")) if da: deps.append(f"{dg}:{da}" if dg else da) - return {"name": name, "version": root.findtext("version"), "deps": deps} + return {"name": name, "version": version, "deps": deps} _PARSERS = { diff --git a/tests/test_manifest_ingest.py b/tests/test_manifest_ingest.py index 9f01675c4..6407e477b 100644 --- a/tests/test_manifest_ingest.py +++ b/tests/test_manifest_ingest.py @@ -163,3 +163,42 @@ def test_cargo_workspace_inherited_version_does_not_crash(tmp_path): '[package]\nname = "member"\nversion.workspace = true\n') pkg = _pkg_nodes(extract_package_manifest(p))[0] assert pkg["label"] == "member" and "version" not in pkg + + +# ── #3806: inherited groupId and ${...} properties in pom.xml ──────────────── + +def test_pom_inherits_groupid_and_resolves_properties(tmp_path): + _write(tmp_path / "rsc/pom.xml", + '\n' + ' org.acmemain1.0\n' + ' org.acme\n acme-rsc\n\n') + _write(tmp_path / "server/pom.xml", + '\n' + ' org.acmemain1.0\n' + ' acme-server\n' + ' 2.12\n' + ' \n' + ' ${project.groupId}acme-rsc\n' + ' org.acmeacme-core_${core.suffix}\n' + ' \n\n') + _write(tmp_path / "web/pom.xml", + '\n' + ' org.acmemain1.0\n' + ' acme-web\n' + ' \n' + ' org.acmeacme-server\n' + ' \n\n') + + server = extract_package_manifest(tmp_path / "server/pom.xml") + pkg = _pkg_nodes(server)[0] + assert pkg["label"] == "org.acme:acme-server" and pkg["version"] == "1.0" + targets = {e["target"] for e in server["edges"] if e["relation"] == "depends_on"} + assert {"pkg_org_acme_acme_rsc", "pkg_org_acme_acme_core_2_12"} <= targets + + result = extract(sorted(tmp_path.rglob("pom.xml")), cache_root=tmp_path) + g = build_from_json(result) + labels = {n: d.get("label") for n, d in g.nodes(data=True)} + dep_edges = {frozenset((labels[u], labels[v])) for u, v, d in g.edges(data=True) + if d.get("relation") == "depends_on"} + assert frozenset(("org.acme:acme-server", "org.acme:acme-rsc")) in dep_edges + assert frozenset(("org.acme:acme-web", "org.acme:acme-server")) in dep_edges