Skip to content

Commit b0c6e59

Browse files
authored
feat: resolve Maven properties in found POMs (#271)
Signed-off-by: Ben Selwyn-Smith <benselwynsmith@googlemail.com>
1 parent bdccd79 commit b0c6e59

File tree

3 files changed

+68
-10
lines changed

3 files changed

+68
-10
lines changed

src/macaron/dependency_analyzer/java_repo_finder.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
"""This module tries to find urls of repositories that match artifacts passed in 'group:artifact:version' form."""
55
import logging
6+
import re
67
import typing
78
from collections.abc import Iterator
89
from xml.etree.ElementTree import Element # nosec
@@ -120,7 +121,7 @@ def find_parent(pom: Element) -> tuple[str, str, str]:
120121
return "", "", ""
121122

122123

123-
def find_scm(pom: Element, tags: list[str]) -> tuple[Iterator[str], int]:
124+
def find_scm(pom: Element, tags: list[str], resolve_properties: bool = True) -> tuple[Iterator[str], int]:
124125
"""
125126
Parse the passed pom and extract the passed tags.
126127
@@ -130,6 +131,8 @@ def find_scm(pom: Element, tags: list[str]) -> tuple[Iterator[str], int]:
130131
The parsed POM.
131132
tags : list[str]
132133
The list of tags to try extracting from the POM.
134+
resolve_properties: bool
135+
Whether to attempt resolution of Maven properties within the POM.
133136
134137
Returns
135138
-------
@@ -141,7 +144,15 @@ def find_scm(pom: Element, tags: list[str]) -> tuple[Iterator[str], int]:
141144
# Try to match each tag with the contents of the POM.
142145
for tag in tags:
143146
element: typing.Optional[Element] = pom
144-
tag_parts = tag.split(".")
147+
148+
if tag.startswith("properties."):
149+
# Tags under properties are often "." separated
150+
# These can be safely split into two resulting tags as nested tags are not allowed here
151+
tag_parts = ["properties", tag[11:]]
152+
else:
153+
# Other tags can be split into distinct elements via "."
154+
tag_parts = tag.split(".")
155+
145156
for index, tag_part in enumerate(tag_parts):
146157
element = _find_element(element, tag_part)
147158
if element is None:
@@ -150,9 +161,53 @@ def find_scm(pom: Element, tags: list[str]) -> tuple[Iterator[str], int]:
150161
# Add the contents of the final tag
151162
results.append(element.text.strip())
152163

164+
# Resolve any Maven properties within the results
165+
if resolve_properties:
166+
results = _resolve_properties(pom, results)
167+
153168
return iter(results), len(results)
154169

155170

171+
def _resolve_properties(pom: Element, values: list[str]) -> list[str]:
172+
"""Resolve any Maven properties found within the passed list of values.
173+
174+
Maven POM files have five different use cases for properties (see https://maven.apache.org/pom.html).
175+
Only the two that relate to contents found elsewhere within the same POM file are considered here.
176+
That is: ${project.x} where x can be a child tag at any depth, or ${x} where x is found at project.properties.x.
177+
Entries with unresolved properties are not included in the returned list. In the case of chained properties,
178+
only the top most property is evaluated.
179+
"""
180+
resolved_values = []
181+
for value in values:
182+
replacements: list = []
183+
# Calculate replacements - matches any number of ${...} entries in the current value
184+
for match in re.finditer("\\$\\{[^}]+}", value):
185+
text = match.group().replace("$", "").replace("{", "").replace("}", "")
186+
if text.startswith("project."):
187+
text = text.replace("project.", "")
188+
else:
189+
text = f"properties.{text}"
190+
# Call find_scm with property resolution flag set to False to prevent the possibility of endless looping
191+
value_iterator, count = find_scm(pom, [text], False)
192+
if count == 0:
193+
break
194+
replacements.append([match.start(), next(value_iterator), match.end()])
195+
196+
# Apply replacements in reverse order
197+
# E.g.
198+
# git@github.com:owner/project${javac.src.version}-${project.inceptionYear}.git
199+
# ->
200+
# git@github.com:owner/project${javac.src.version}-2023.git
201+
# ->
202+
# git@github.com:owner/project1.8-2023.git
203+
for replacement in reversed(replacements):
204+
value = f"{value[:replacement[0]]}{replacement[1]}{value[replacement[2]:]}"
205+
206+
resolved_values.append(value)
207+
208+
return resolved_values
209+
210+
156211
def parse_pom(pom: str) -> Element | None:
157212
"""
158213
Parse the passed POM using defusedxml.

tests/dependency_analyzer/java_repo_finder/resources/example_pom.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@
2323
<inceptionYear>2023</inceptionYear>
2424
<licenses>
2525
<license>
26-
<name>Example License</name>
26+
<name>Example_License</name>
2727
<url>https://example.example/license</url>
28-
<distribution>repo</distribution>
28+
<distribution>${licenses.license.distribution}</distribution>
2929
</license>
3030
</licenses>
3131
<scm>
3232
<connection>
33-
ssh://git@hostname:port/owner/project.git
33+
ssh://git@hostname:port/owner/${project.licenses.license.name}.git
3434
</connection>
3535
<developerConnection>
36-
git@github.com:owner/project.git
36+
git@github.com:owner/project${javac.src.version}-${project.inceptionYear}.git
3737
</developerConnection>
3838
<url>https://github.com/owner/project</url>
3939
<tag>example-0.0.1</tag>

tests/dependency_analyzer/java_repo_finder/test_java_repo_finder.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@ def test_java_repo_finder() -> None:
2727
file_data = file.read()
2828
pom = parse_pom(file_data)
2929
assert pom is not None
30-
found_urls, count = find_scm(pom, ["scm.url", "scm.connection", "scm.developerConnection"])
31-
assert count == 3
30+
found_urls, count = find_scm(
31+
pom, ["scm.url", "scm.connection", "scm.developerConnection", "licenses.license.distribution"]
32+
)
33+
assert count == 4
3234
expected = [
3335
"https://github.com/owner/project",
34-
"ssh://git@hostname:port/owner/project.git",
35-
"git@github.com:owner/project.git",
36+
"ssh://git@hostname:port/owner/Example_License.git",
37+
"git@github.com:owner/project1.8-2023.git",
38+
"${licenses.license.distribution}",
3639
]
3740
assert expected == list(found_urls)
3841

0 commit comments

Comments
 (0)