Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,22 @@ public class EnforcerVersionRangeStrategy extends AbstractUpgradeStrategy {
*/
static final Pattern MAVEN4_EXCLUSIVE_UPPER_BOUND = Pattern.compile("^(\\[|\\()(.+?),\\s*(4(?:\\.\\d+)*)\\s*\\)$");

/**
* Pattern to match version ranges where the upper bound has a major version below 4,
* which blocks Maven 4. This includes exact version pins like {@code [3.8.6,3.8.6]}
* and ranges like {@code [3.8.0,3.9)}, {@code (,3.9]}.
* Captures:
* Group 1: opening bracket ([ or ()
* Group 2: lower bound (may be empty for unbounded lower ranges like {@code (,3.9]})
* Group 3: upper bound (numeric version like 3.8.6, 3.9, 3)
* Group 4: closing bracket () or ])
*
* Examples matched: [3.8.6,3.8.6], [3.8.0,3.9), (,3.9], [3.0,3.0], [3.9.0,3.9.0]
* Examples not matched: [3.8.8,), 3.8.8, [3.8.8,5), [3.8.8,4) (handled by MAVEN4_EXCLUSIVE_UPPER_BOUND)
*/
static final Pattern UPPER_BOUND_BELOW_MAVEN4 =
Pattern.compile("^(\\[|\\()(.*?),\\s*(\\d+(?:\\.\\d+)*)\\s*(\\)|\\])$");

@Override
public boolean isApplicable(UpgradeContext context) {
UpgradeOptions options = getOptions(context);
Expand Down Expand Up @@ -269,18 +285,54 @@ private boolean processRulesElement(Element configuration, UpgradeContext contex
}

/**
* Widens a Maven version range that has an exclusive upper bound at 4.x.
* Widens a Maven version range that blocks Maven 4.
*
* <p>Handles three cases:
* <ul>
* <li>Ranges with an exclusive upper bound at 4.x (e.g., {@code [3.8.8,4)} → {@code [3.8.8,5)})</li>
* <li>Exact version pins (e.g., {@code [3.8.6,3.8.6]} → {@code [3.8.6,5)})</li>
* <li>Ranges with an upper bound below 4 (e.g., {@code [3.8.0,3.9)} → {@code [3.8.0,5)})</li>
* </ul>
*
* @param versionRange the version range string (e.g., "[3.8.8,4)")
* @return the widened range (e.g., "[3.8.8,5)"), or null if no widening is needed
*/
static String widenVersionRange(String versionRange) {
// Check for ranges with exclusive upper bound at 4.x
Matcher matcher = MAVEN4_EXCLUSIVE_UPPER_BOUND.matcher(versionRange);
if (matcher.matches()) {
String openBracket = matcher.group(1);
String lowerBound = matcher.group(2);
return openBracket + lowerBound + ",5)";
}

// Check for ranges where the upper bound's major version is below 4,
// which block Maven 4. This catches exact version pins like [3.8.6,3.8.6]
// and ranges like [3.8.0,3.9), (,3.9].
Matcher belowMatcher = UPPER_BOUND_BELOW_MAVEN4.matcher(versionRange);
if (belowMatcher.matches()) {
String openBracket = belowMatcher.group(1);
String lowerBound = belowMatcher.group(2);
String upperBound = belowMatcher.group(3);
if (getMajorVersion(upperBound) < 4) {
return openBracket + lowerBound + ",5)";
}
}

return null;
}

/**
* Extracts the major version number from a version string.
*
* @param version the version string (e.g., "3.8.6", "3", "4.0.0")
* @return the major version number, or {@link Integer#MAX_VALUE} if unparseable
*/
private static int getMajorVersion(String version) {
try {
return Integer.parseInt(version.split("\\.")[0]);
} catch (NumberFormatException e) {
return Integer.MAX_VALUE;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,33 @@ void shouldNotWidenAlreadyWidened() {
}

@Test
@DisplayName("should not widen [3.8.8,3.9) — upper bound is not at 4")
void shouldNotWidenNon4UpperBound() {
assertNull(EnforcerVersionRangeStrategy.widenVersionRange("[3.8.8,3.9)"));
@DisplayName("should widen [3.8.8,3.9) to [3.8.8,5) — upper bound below 4 blocks Maven 4")
void shouldWidenSub4UpperBound() {
assertEquals("[3.8.8,5)", EnforcerVersionRangeStrategy.widenVersionRange("[3.8.8,3.9)"));
}

@Test
@DisplayName("should widen exact version pin [3.8.6,3.8.6] to [3.8.6,5)")
void shouldWidenExactVersionPin() {
assertEquals("[3.8.6,5)", EnforcerVersionRangeStrategy.widenVersionRange("[3.8.6,3.8.6]"));
}

@Test
@DisplayName("should widen exact version pin [3.0,3.0] to [3.0,5)")
void shouldWidenExactVersionPinShort() {
assertEquals("[3.0,5)", EnforcerVersionRangeStrategy.widenVersionRange("[3.0,3.0]"));
}

@Test
@DisplayName("should widen exact version pin [3.9.0,3.9.0] to [3.9.0,5)")
void shouldWidenExactVersionPinHighMinor() {
assertEquals("[3.9.0,5)", EnforcerVersionRangeStrategy.widenVersionRange("[3.9.0,3.9.0]"));
}

@Test
@DisplayName("should not widen [4.0.0,4.0.0] — already allows Maven 4")
void shouldNotWidenExactPinAtMaven4() {
assertNull(EnforcerVersionRangeStrategy.widenVersionRange("[4.0.0,4.0.0]"));
}
}

Expand Down Expand Up @@ -367,6 +391,48 @@ void shouldNotModifyNonEnforcerPlugins() {
assertEquals(0, result.modifiedCount(), "No changes expected for non-enforcer plugins");
}

@Test
@DisplayName("should widen exact version pin in requireMavenVersion configuration")
void shouldWidenExactVersionPinInConfiguration() {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0.0</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<configuration>
<rules>
<requireMavenVersion>
<version>[3.8.6,3.8.6]</version>
</requireMavenVersion>
</rules>
</configuration>
</plugin>
</plugins>
</build>
</project>
""";

Document document = Document.of(pomXml);
Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);

UpgradeContext context = createMockContext();
UpgradeResult result = strategy.doApply(context, pomMap);

assertTrue(result.success());
assertTrue(result.modifiedCount() > 0, "Exact version pin should be widened");

String xml = DomUtils.toXml(document);
assertTrue(xml.contains("[3.8.6,5)"), "Version range should be widened to [3.8.6,5)");
assertFalse(xml.contains("[3.8.6,3.8.6]"), "Original exact pin should be replaced");
}

@Test
@DisplayName("should handle enforcer plugin without explicit groupId")
void shouldHandleEnforcerWithoutGroupId() {
Expand Down
Loading