This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch feature/gh-11904-dependency-id-attribute
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to
refs/heads/feature/gh-11904-dependency-id-attribute by this push:
new 3d4ddbec46 [GH-11904] Support managed-version formats in dependency id
attribute
3d4ddbec46 is described below
commit 3d4ddbec46fa36cc1b3f45c6ad31d155f2a191ec
Author: Guillaume Nodet <[email protected]>
AuthorDate: Tue Jun 23 22:13:40 2026 +0200
[GH-11904] Support managed-version formats in dependency id attribute
Add support for 2-part `g:a` format and trailing-colon formats
(`g:a:`, `g:a:type:`, `g:a:type:classifier:`) for dependencies
whose version is provided by dependencyManagement. The trailing
colon convention signals that version is managed, not missing.
Leave aside inferred ids (`:a:` with inferred groupId) for now.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
api/maven-api-model/src/main/mdo/maven.mdo | 8 +-
.../invoker/mvnup/goals/DependencyIdStrategy.java | 34 +++++--
.../mvnup/goals/DependencyIdStrategyTest.java | 104 ++++++++++++++++++++-
.../maven/impl/model/DefaultModelNormalizer.java | 31 +++---
.../maven/impl/model/DefaultModelValidator.java | 9 +-
.../impl/model/DefaultModelNormalizerTest.java | 54 +++++++++++
.../impl/model/DefaultModelValidatorTest.java | 24 +++++
.../poms/validation/dependency-id-2parts-valid.xml | 11 +++
.../dependency-id-trailing-colon-3parts-valid.xml | 11 +++
.../dependency-id-trailing-colon-4parts-valid.xml | 11 +++
.../dependency-id-trailing-colon-5parts-valid.xml | 11 +++
11 files changed, 275 insertions(+), 33 deletions(-)
diff --git a/api/maven-api-model/src/main/mdo/maven.mdo
b/api/maven-api-model/src/main/mdo/maven.mdo
index 00681ebe4f..8501c35711 100644
--- a/api/maven-api-model/src/main/mdo/maven.mdo
+++ b/api/maven-api-model/src/main/mdo/maven.mdo
@@ -1211,9 +1211,13 @@
<description>
<![CDATA[
Combined coordinates in one of the following formats:
+ {@code groupId:artifactId} (version managed),
{@code groupId:artifactId:version},
- {@code groupId:artifactId:type:version}, or
- {@code groupId:artifactId:type:classifier:version}.
+ {@code groupId:artifactId:type:version},
+ {@code groupId:artifactId:type:classifier:version},
+ {@code groupId:artifactId:type:} (version managed, with type), or
+ {@code groupId:artifactId:type:classifier:} (version managed, with
type and classifier).
+ A trailing {@code :} indicates the version is managed (i.e.
provided by dependencyManagement).
When specified, it is split into the corresponding fields during
model normalization.
The individual child elements should not be specified when using
this attribute.
@since Maven 4.2
diff --git
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/DependencyIdStrategy.java
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/DependencyIdStrategy.java
index 3b77c1e6bf..5efccfaec3 100644
---
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/DependencyIdStrategy.java
+++
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/DependencyIdStrategy.java
@@ -66,11 +66,14 @@
* <dependency id="org.example:lib:1.0"/>
* }</pre>
*
- * <p>Supports three formats:
+ * <p>Supported formats (trailing {@code :} means version is managed):
* <ul>
- * <li>{@code g:a:v} (3-part, default type "jar")</li>
- * <li>{@code g:a:type:v} (4-part, non-default type)</li>
- * <li>{@code g:a:type:classifier:v} (5-part, with classifier)</li>
+ * <li>{@code g:a} — version managed</li>
+ * <li>{@code g:a:v} — default type "jar"</li>
+ * <li>{@code g:a:type:} — non-default type, version managed</li>
+ * <li>{@code g:a:type:v} — non-default type</li>
+ * <li>{@code g:a:type:classifier:} — with classifier, version managed</li>
+ * <li>{@code g:a:type:classifier:v} — with classifier</li>
* </ul>
*
* <p>Also collapses exclusions from {@code <groupId>}/{@code <artifactId>}
into {@code id="g:a"}.
@@ -210,7 +213,7 @@ private boolean collapseDependency(UpgradeContext context,
Element dependency) {
String artifactId = dependency.childText(ARTIFACT_ID);
String version = dependency.childText(VERSION);
- if (groupId == null || artifactId == null || version == null) {
+ if (groupId == null || artifactId == null) {
return false;
}
@@ -222,7 +225,9 @@ private boolean collapseDependency(UpgradeContext context,
Element dependency) {
removeChildElement(dependency, GROUP_ID);
removeChildElement(dependency, ARTIFACT_ID);
- removeChildElement(dependency, VERSION);
+ if (version != null) {
+ removeChildElement(dependency, VERSION);
+ }
if (classifier != null) {
removeChildElement(dependency, CLASSIFIER);
@@ -230,7 +235,6 @@ private boolean collapseDependency(UpgradeContext context,
Element dependency) {
} else if (type != null && !DEFAULT_TYPE.equals(type)) {
removeChildElement(dependency, TYPE);
} else if (type != null) {
- // default type "jar" — remove it as it's implied
removeChildElement(dependency, TYPE);
}
@@ -280,11 +284,21 @@ private boolean collapseExclusion(UpgradeContext context,
Element exclusion) {
static String buildIdValue(String groupId, String artifactId, String
version, String type, String classifier) {
if (classifier != null && !classifier.isEmpty()) {
String effectiveType = (type != null && !type.isEmpty()) ? type :
DEFAULT_TYPE;
- return groupId + ":" + artifactId + ":" + effectiveType + ":" +
classifier + ":" + version;
+ if (version != null) {
+ return groupId + ":" + artifactId + ":" + effectiveType + ":"
+ classifier + ":" + version;
+ } else {
+ return groupId + ":" + artifactId + ":" + effectiveType + ":"
+ classifier + ":";
+ }
} else if (type != null && !type.isEmpty() &&
!DEFAULT_TYPE.equals(type)) {
- return groupId + ":" + artifactId + ":" + type + ":" + version;
- } else {
+ if (version != null) {
+ return groupId + ":" + artifactId + ":" + type + ":" + version;
+ } else {
+ return groupId + ":" + artifactId + ":" + type + ":";
+ }
+ } else if (version != null) {
return groupId + ":" + artifactId + ":" + version;
+ } else {
+ return groupId + ":" + artifactId;
}
}
diff --git
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/DependencyIdStrategyTest.java
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/DependencyIdStrategyTest.java
index 5c8b9abc9c..3be91fb164 100644
---
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/DependencyIdStrategyTest.java
+++
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/DependencyIdStrategyTest.java
@@ -342,8 +342,8 @@ void shouldPreserveOptionalWhenCollapsing() {
}
@Test
- @DisplayName("should skip dependency without version")
- void shouldSkipDependencyWithoutVersion() {
+ @DisplayName("should collapse dependency without version to g:a")
+ void shouldCollapseDependencyWithoutVersion() {
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.2.0">
@@ -370,9 +370,83 @@ void shouldSkipDependencyWithoutVersion() {
.childElement("dependency")
.orElseThrow();
- assertNull(dependency.attribute("id"));
- assertNotNull(DomUtils.findChildElement(dependency, "groupId"));
- assertNotNull(DomUtils.findChildElement(dependency, "artifactId"));
+ assertEquals("org.example:lib", dependency.attribute("id"));
+ assertNull(DomUtils.findChildElement(dependency, "groupId"));
+ assertNull(DomUtils.findChildElement(dependency, "artifactId"));
+ }
+
+ @Test
+ @DisplayName("should collapse managed-version dependency with
non-default type to g:a:type:")
+ void shouldCollapseManagedWithType() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.2.0">
+ <modelVersion>4.2.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <dependencies>
+ <dependency>
+ <groupId>org.example</groupId>
+ <artifactId>lib</artifactId>
+ <type>pom</type>
+ </dependency>
+ </dependencies>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.2.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element dependency = doc.root()
+ .childElement("dependencies")
+ .orElseThrow()
+ .childElement("dependency")
+ .orElseThrow();
+
+ assertEquals("org.example:lib:pom:", dependency.attribute("id"));
+ assertNull(DomUtils.findChildElement(dependency, "groupId"));
+ assertNull(DomUtils.findChildElement(dependency, "artifactId"));
+ assertNull(DomUtils.findChildElement(dependency, "type"));
+ }
+
+ @Test
+ @DisplayName("should collapse managed-version dependency with
classifier to g:a:type:classifier:")
+ void shouldCollapseManagedWithClassifier() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.2.0">
+ <modelVersion>4.2.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <dependencies>
+ <dependency>
+ <groupId>org.example</groupId>
+ <artifactId>lib</artifactId>
+ <type>jar</type>
+ <classifier>sources</classifier>
+ </dependency>
+ </dependencies>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.2.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element dependency = doc.root()
+ .childElement("dependencies")
+ .orElseThrow()
+ .childElement("dependency")
+ .orElseThrow();
+
+ assertEquals("org.example:lib:jar:sources:",
dependency.attribute("id"));
+ assertNull(DomUtils.findChildElement(dependency, "groupId"));
+ assertNull(DomUtils.findChildElement(dependency, "artifactId"));
+ assertNull(DomUtils.findChildElement(dependency, "type"));
+ assertNull(DomUtils.findChildElement(dependency, "classifier"));
}
@Test
@@ -839,6 +913,11 @@ void shouldProcessOnly420PomsInMixedMap() {
@DisplayName("buildIdValue")
class BuildIdValueTests {
+ @Test
+ void twoPartFormat() {
+ assertEquals("g:a", DependencyIdStrategy.buildIdValue("g", "a",
null, null, null));
+ }
+
@Test
void threePartFormat() {
assertEquals("g:a:1.0", DependencyIdStrategy.buildIdValue("g",
"a", "1.0", null, null));
@@ -869,5 +948,20 @@ void fivePartFormatWithNonDefaultType() {
void fivePartFormatClassifierNoType() {
assertEquals("g:a:jar:sources:1.0",
DependencyIdStrategy.buildIdValue("g", "a", "1.0", null, "sources"));
}
+
+ @Test
+ void fourPartTrailingColon() {
+ assertEquals("g:a:pom:", DependencyIdStrategy.buildIdValue("g",
"a", null, "pom", null));
+ }
+
+ @Test
+ void fivePartTrailingColon() {
+ assertEquals("g:a:jar:sources:",
DependencyIdStrategy.buildIdValue("g", "a", null, "jar", "sources"));
+ }
+
+ @Test
+ void fivePartTrailingColonClassifierNoType() {
+ assertEquals("g:a:jar:sources:",
DependencyIdStrategy.buildIdValue("g", "a", null, null, "sources"));
+ }
}
}
diff --git
a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelNormalizer.java
b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelNormalizer.java
index a36992f33f..e8b13988f0 100644
---
a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelNormalizer.java
+++
b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelNormalizer.java
@@ -199,11 +199,15 @@ private Profile expandProfileDependencyIds(Profile
profile) {
/**
* Expands the {@code id} attribute on a dependency into its component
fields.
- * Supported formats:
+ * Supported formats (trailing {@code :} means version is managed):
* <ul>
- * <li>{@code groupId:artifactId:version} (3 parts)</li>
- * <li>{@code groupId:artifactId:type:version} (4 parts)</li>
- * <li>{@code groupId:artifactId:type:classifier:version} (5 parts)</li>
+ * <li>{@code groupId:artifactId} (version managed)</li>
+ * <li>{@code groupId:artifactId:} (version managed, explicit trailing
colon)</li>
+ * <li>{@code groupId:artifactId:version}</li>
+ * <li>{@code groupId:artifactId:type:} (version managed, with type)</li>
+ * <li>{@code groupId:artifactId:type:version}</li>
+ * <li>{@code groupId:artifactId:type:classifier:} (version managed,
with type and classifier)</li>
+ * <li>{@code groupId:artifactId:type:classifier:version}</li>
* </ul>
*/
Dependency expandDependencyId(Dependency d) {
@@ -212,8 +216,8 @@ Dependency expandDependencyId(Dependency d) {
List<Exclusion> expanded = injectList(d.getExclusions(),
this::expandExclusionId);
return expanded != null ? d.withExclusions(expanded) : d;
}
- String[] parts = id.split(":");
- if (parts.length < 3 || parts.length > 5) {
+ String[] parts = id.split(":", -1);
+ if (parts.length < 2 || parts.length > 5) {
return d;
}
Dependency.Builder builder = Dependency.newBuilder(d, true);
@@ -225,30 +229,33 @@ Dependency expandDependencyId(Dependency d) {
builder.artifactId(parts[1]);
}
switch (parts.length) {
+ case 2:
+ // g:a (version managed)
+ break;
case 3:
- // g:a:v
- if (isNullOrEmpty(d.getVersion())) {
+ // g:a:v or g:a: (managed)
+ if (!parts[2].isEmpty() && isNullOrEmpty(d.getVersion())) {
builder.version(parts[2]);
}
break;
case 4:
- // g:a:type:v
+ // g:a:type:v or g:a:type: (managed)
if (isNullOrEmptyOrDefault(d.getType())) {
builder.type(parts[2]);
}
- if (isNullOrEmpty(d.getVersion())) {
+ if (!parts[3].isEmpty() && isNullOrEmpty(d.getVersion())) {
builder.version(parts[3]);
}
break;
case 5:
- // g:a:type:classifier:v
+ // g:a:type:classifier:v or g:a:type:classifier: (managed)
if (isNullOrEmptyOrDefault(d.getType())) {
builder.type(parts[2]);
}
if (isNullOrEmpty(d.getClassifier())) {
builder.classifier(parts[3]);
}
- if (isNullOrEmpty(d.getVersion())) {
+ if (!parts[4].isEmpty() && isNullOrEmpty(d.getVersion())) {
builder.version(parts[4]);
}
break;
diff --git
a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java
b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java
index 72e65e1864..e4d5b26f99 100644
---
a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java
+++
b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java
@@ -1269,16 +1269,17 @@ private void
validateDependencyIdAttribute(ModelProblemCollector problems, Depen
return;
}
int colonCount = id.length() - id.replace(":", "").length();
- if (colonCount < 2 || colonCount > 4) {
+ if (colonCount < 1 || colonCount > 4) {
addViolation(
problems,
Severity.ERROR,
Version.V42,
prefix + "id",
null,
- "has invalid format '" + id + "', must be
'groupId:artifactId:version', "
- + "'groupId:artifactId:type:version', or "
- + "'groupId:artifactId:type:classifier:version'.",
+ "has invalid format '" + id + "', must be
'groupId:artifactId[:]', "
+ + "'groupId:artifactId:version', "
+ + "'groupId:artifactId:type:[version]', or "
+ +
"'groupId:artifactId:type:classifier:[version]'.",
dependency);
return;
}
diff --git
a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelNormalizerTest.java
b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelNormalizerTest.java
index e4a84dbbb3..8eca6c66ad 100644
---
a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelNormalizerTest.java
+++
b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelNormalizerTest.java
@@ -35,6 +35,30 @@ class DefaultModelNormalizerTest {
private final DefaultModelNormalizer normalizer = new
DefaultModelNormalizer();
+ @Test
+ void testExpandDependencyId2parts() {
+ Dependency dep =
Dependency.newBuilder(false).id("org.example:lib").build();
+
+ Dependency result = normalizer.expandDependencyId(dep);
+
+ assertEquals("org.example", result.getGroupId());
+ assertEquals("lib", result.getArtifactId());
+ assertNull(result.getVersion());
+ assertNull(result.getId());
+ }
+
+ @Test
+ void testExpandDependencyId2partsTrailingColon() {
+ Dependency dep =
Dependency.newBuilder(false).id("org.example:lib:").build();
+
+ Dependency result = normalizer.expandDependencyId(dep);
+
+ assertEquals("org.example", result.getGroupId());
+ assertEquals("lib", result.getArtifactId());
+ assertNull(result.getVersion());
+ assertNull(result.getId());
+ }
+
@Test
void testExpandDependencyId3parts() {
Dependency dep =
@@ -62,6 +86,20 @@ void testExpandDependencyId4parts() {
assertNull(result.getId());
}
+ @Test
+ void testExpandDependencyId4partsTrailingColon() {
+ Dependency dep =
+
Dependency.newBuilder(false).id("org.example:lib-b:pom:").build();
+
+ Dependency result = normalizer.expandDependencyId(dep);
+
+ assertEquals("org.example", result.getGroupId());
+ assertEquals("lib-b", result.getArtifactId());
+ assertEquals("pom", result.getType());
+ assertNull(result.getVersion());
+ assertNull(result.getId());
+ }
+
@Test
void testExpandDependencyId5parts() {
Dependency dep = Dependency.newBuilder(false)
@@ -78,6 +116,22 @@ void testExpandDependencyId5parts() {
assertNull(result.getId());
}
+ @Test
+ void testExpandDependencyId5partsTrailingColon() {
+ Dependency dep = Dependency.newBuilder(false)
+ .id("org.example:lib-c:jar:sources:")
+ .build();
+
+ Dependency result = normalizer.expandDependencyId(dep);
+
+ assertEquals("org.example", result.getGroupId());
+ assertEquals("lib-c", result.getArtifactId());
+ assertEquals("jar", result.getType());
+ assertEquals("sources", result.getClassifier());
+ assertNull(result.getVersion());
+ assertNull(result.getId());
+ }
+
@Test
void testExpandDependencyIdDoesNotOverrideExistingFields() {
Dependency dep = Dependency.newBuilder(false)
diff --git
a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelValidatorTest.java
b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelValidatorTest.java
index 3514ee326f..f9e649cafb 100644
---
a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelValidatorTest.java
+++
b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelValidatorTest.java
@@ -1023,6 +1023,12 @@ void profileActivationConditionWithBasedirExpression()
throws Exception {
assertViolations(result, 0, 0, 0);
}
+ @Test
+ void testDependencyId2partsValid() throws Exception {
+ SimpleProblemCollector result =
validateFile("dependency-id-2parts-valid.xml");
+ assertViolations(result, 0, 0, 0);
+ }
+
@Test
void testDependencyId3partsValid() throws Exception {
SimpleProblemCollector result =
validateFile("dependency-id-3parts-valid.xml");
@@ -1041,6 +1047,24 @@ void testDependencyId5partsValid() throws Exception {
assertViolations(result, 0, 0, 0);
}
+ @Test
+ void testDependencyIdTrailingColon3partsValid() throws Exception {
+ SimpleProblemCollector result =
validateFile("dependency-id-trailing-colon-3parts-valid.xml");
+ assertViolations(result, 0, 0, 0);
+ }
+
+ @Test
+ void testDependencyIdTrailingColon4partsValid() throws Exception {
+ SimpleProblemCollector result =
validateFile("dependency-id-trailing-colon-4parts-valid.xml");
+ assertViolations(result, 0, 0, 0);
+ }
+
+ @Test
+ void testDependencyIdTrailingColon5partsValid() throws Exception {
+ SimpleProblemCollector result =
validateFile("dependency-id-trailing-colon-5parts-valid.xml");
+ assertViolations(result, 0, 0, 0);
+ }
+
@Test
void testDependencyIdBadFormat() throws Exception {
SimpleProblemCollector result =
validateFile("dependency-id-bad-format.xml");
diff --git
a/impl/maven-impl/src/test/resources/poms/validation/dependency-id-2parts-valid.xml
b/impl/maven-impl/src/test/resources/poms/validation/dependency-id-2parts-valid.xml
new file mode 100644
index 0000000000..3702648630
--- /dev/null
+++
b/impl/maven-impl/src/test/resources/poms/validation/dependency-id-2parts-valid.xml
@@ -0,0 +1,11 @@
+<project xmlns="http://maven.apache.org/POM/4.2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.2.0
https://maven.apache.org/xsd/maven-4.2.0.xsd">
+ <modelVersion>4.2.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <dependencies>
+ <dependency id="org.example:lib-a"/>
+ </dependencies>
+</project>
diff --git
a/impl/maven-impl/src/test/resources/poms/validation/dependency-id-trailing-colon-3parts-valid.xml
b/impl/maven-impl/src/test/resources/poms/validation/dependency-id-trailing-colon-3parts-valid.xml
new file mode 100644
index 0000000000..a76ebe1741
--- /dev/null
+++
b/impl/maven-impl/src/test/resources/poms/validation/dependency-id-trailing-colon-3parts-valid.xml
@@ -0,0 +1,11 @@
+<project xmlns="http://maven.apache.org/POM/4.2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.2.0
https://maven.apache.org/xsd/maven-4.2.0.xsd">
+ <modelVersion>4.2.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <dependencies>
+ <dependency id="org.example:lib-a:"/>
+ </dependencies>
+</project>
diff --git
a/impl/maven-impl/src/test/resources/poms/validation/dependency-id-trailing-colon-4parts-valid.xml
b/impl/maven-impl/src/test/resources/poms/validation/dependency-id-trailing-colon-4parts-valid.xml
new file mode 100644
index 0000000000..731c4d5c43
--- /dev/null
+++
b/impl/maven-impl/src/test/resources/poms/validation/dependency-id-trailing-colon-4parts-valid.xml
@@ -0,0 +1,11 @@
+<project xmlns="http://maven.apache.org/POM/4.2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.2.0
https://maven.apache.org/xsd/maven-4.2.0.xsd">
+ <modelVersion>4.2.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <dependencies>
+ <dependency id="org.example:lib-b:pom:"/>
+ </dependencies>
+</project>
diff --git
a/impl/maven-impl/src/test/resources/poms/validation/dependency-id-trailing-colon-5parts-valid.xml
b/impl/maven-impl/src/test/resources/poms/validation/dependency-id-trailing-colon-5parts-valid.xml
new file mode 100644
index 0000000000..e169e92fa9
--- /dev/null
+++
b/impl/maven-impl/src/test/resources/poms/validation/dependency-id-trailing-colon-5parts-valid.xml
@@ -0,0 +1,11 @@
+<project xmlns="http://maven.apache.org/POM/4.2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.2.0
https://maven.apache.org/xsd/maven-4.2.0.xsd">
+ <modelVersion>4.2.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <dependencies>
+ <dependency id="org.example:lib-c:jar:sources:"/>
+ </dependencies>
+</project>