From 421a3be997c7dc59dff4c32dff39717291f93349 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Thu, 13 Aug 2026 20:33:03 +0530 Subject: [PATCH 1/9] MNG-8099: Add explicit 'api' scope for dependencies and make 'compile' non-transitive for Maven 4 --- .../org/apache/maven/api/DependencyScope.java | 7 ++++++- .../java/org/apache/maven/api/PathScope.java | 5 ++++- .../scopes/Maven4ScopeManagerConfiguration.java | 2 ++ .../maven/impl/model/DefaultModelValidator.java | 3 ++- .../scopes/Maven4ScopeManagerConfiguration.java | 2 ++ issue_comment.md | Bin 0 -> 2418 bytes plexus-sec-dispatcher | 1 + 7 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 issue_comment.md create mode 160000 plexus-sec-dispatcher diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java b/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java index f9f788e00316..c203f5fc7504 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java @@ -64,7 +64,12 @@ public enum DependencyScope { /** * Compile, runtime and test. */ - COMPILE("compile", true), + COMPILE("compile", false), + + /** + * Compile, runtime and test, transitively. + */ + API("api", true), /** * Runtime and test. diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java b/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java index bd8a6f1bf51c..1379ca72c101 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java @@ -58,15 +58,17 @@ public interface PathScope extends ExtensibleEnum { ProjectScope.MAIN, DependencyScope.COMPILE_ONLY, DependencyScope.COMPILE, + DependencyScope.API, DependencyScope.PROVIDED); PathScope MAIN_RUNTIME = - pathScope("main-runtime", ProjectScope.MAIN, DependencyScope.COMPILE, DependencyScope.RUNTIME); + pathScope("main-runtime", ProjectScope.MAIN, DependencyScope.COMPILE, DependencyScope.API, DependencyScope.RUNTIME); PathScope TEST_COMPILE = pathScope( "test-compile", ProjectScope.TEST, DependencyScope.COMPILE, + DependencyScope.API, DependencyScope.PROVIDED, DependencyScope.TEST_ONLY, DependencyScope.TEST); @@ -75,6 +77,7 @@ public interface PathScope extends ExtensibleEnum { "test-runtime", ProjectScope.TEST, DependencyScope.COMPILE, + DependencyScope.API, DependencyScope.RUNTIME, DependencyScope.PROVIDED, DependencyScope.TEST, diff --git a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/scopes/Maven4ScopeManagerConfiguration.java b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/scopes/Maven4ScopeManagerConfiguration.java index a38833076355..8cec4de3667d 100644 --- a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/scopes/Maven4ScopeManagerConfiguration.java +++ b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/scopes/Maven4ScopeManagerConfiguration.java @@ -90,6 +90,8 @@ public Collection buildDependencyScope ArrayList result = new ArrayList<>(); result.add(internalScopeManager.createDependencyScope( DependencyScope.COMPILE.id(), DependencyScope.COMPILE.isTransitive(), all())); + result.add(internalScopeManager.createDependencyScope( + DependencyScope.API.id(), DependencyScope.API.isTransitive(), all())); result.add(internalScopeManager.createDependencyScope( DependencyScope.RUNTIME.id(), DependencyScope.RUNTIME.isTransitive(), 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 f17ee0c8cdeb..d5582bc7521c 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 @@ -1258,7 +1258,8 @@ private void validate20RawDependencies( String scope = dependency.getScope(); if (DependencyScope.COMPILE_ONLY.id().equals(scope) || DependencyScope.TEST_ONLY.id().equals(scope) - || DependencyScope.TEST_RUNTIME.id().equals(scope)) { + || DependencyScope.TEST_RUNTIME.id().equals(scope) + || DependencyScope.API.id().equals(scope)) { addViolation( problems, Severity.ERROR, diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/scopes/Maven4ScopeManagerConfiguration.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/scopes/Maven4ScopeManagerConfiguration.java index 896b240053f6..8c64bc31f00c 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/scopes/Maven4ScopeManagerConfiguration.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/scopes/Maven4ScopeManagerConfiguration.java @@ -88,6 +88,8 @@ public Collection buildDependencyScope ArrayList result = new ArrayList<>(); result.add(internalScopeManager.createDependencyScope( DependencyScope.COMPILE.id(), DependencyScope.COMPILE.isTransitive(), all())); + result.add(internalScopeManager.createDependencyScope( + DependencyScope.API.id(), DependencyScope.API.isTransitive(), all())); result.add(internalScopeManager.createDependencyScope( DependencyScope.RUNTIME.id(), DependencyScope.RUNTIME.isTransitive(), diff --git a/issue_comment.md b/issue_comment.md new file mode 100644 index 0000000000000000000000000000000000000000..7859d8fe47e15c9b14abc376541dcea6369cbbb5 GIT binary patch literal 2418 zcmb7`-A>y;5QXO&sqbK^7nHVC1u9iSs1j|ZiVIqV`U1gmOd!}%5)*j&w&$D4VjSa( z%8Gw>XJ^lxIWwETf6gqkYa7|U6?$7*V^?-(Q(NgOY^<@7eiMy6__MZ!o-3`H$~x1% z(*43Nb$@LKc4(jNldcc8qwBZT8ZTsn=Y{W)Ym9H!vqz2~J9ipih{;-S zU-doJJX|bnPiu(;CLiQ|=6=c1%vmCTm2794&+J4NFm$VX>3f#;MdN`KOksx&aoo!; z#dGXgJ5csAd#meAe%Y;(XS8tcwz+3N7{2iMlh>uMI+o>`VhrtU?;E=5?jGK+JR0;i z6a!fTZ6=O*f&_(xB8fhByU{4o?fP7w57YpEtc;8&B}NYWYMlB$tV5G?WdjA1m4%*Q zV6MOJ(K)Mx7X6T8^iD=c?unen++veW*nZCWSpM0Qcp~3egdUP=VS`b4sU5{k@3rS3 z*$gS0NUg8l6f}sE3|S*60pL`o#(U!?C3^qs0ZDN_`UsVIxi;(8U?k za>*IbN%7I%$@Zl@bu*i)wh*>%CtM3_oZ)B;%!zHzyYPZa;f7hx&x!8o{9>%%d7~?H y>QkN9ii`gcnPOs(mci(xp>WuQs}?~&u6_MOqECf7UXC&F-?>$IT#Bhzs;Yl0{j%Qx literal 0 HcmV?d00001 diff --git a/plexus-sec-dispatcher b/plexus-sec-dispatcher new file mode 160000 index 000000000000..a3b5741891b7 --- /dev/null +++ b/plexus-sec-dispatcher @@ -0,0 +1 @@ +Subproject commit a3b5741891b7fd6004f8e52fd58d968f2282c61b From 05d5d6b13991bd5de11a7800da4403b450491eae Mon Sep 17 00:00:00 2001 From: Hitesh Date: Mon, 17 Aug 2026 10:58:27 +0530 Subject: [PATCH 2/9] Address MNG-8099 PR review feedback - Fix Consumer POM regression: Update hasDependencyScope to use an inclusion list rather than !isTransitive, ensuring non-transitive compile dependencies are kept. - Fix Resolver regression: Map compile scope to api scope internally for models < 4.1.0 to preserve transitive resolution for older artifacts. - Fix MavenModelVersion gap: Inject hasApiDependency check into Modello generation so 4.1.0 is required when using api scope. - Fix Stale Javadoc: Update DependencyScope.COMPILE to indicate it is non-transitive. - Remove accidentally committed issue_comment.md and plexus-sec-dispatcher. Signed-off-by: Hitesh --- .../org/apache/maven/api/DependencyScope.java | 2 +- .../impl/DefaultConsumerPomBuilder.java | 2 +- .../DefaultArtifactDescriptorReader.java | 20 ++++++++++++------ issue_comment.md | Bin 2418 -> 0 bytes plexus-sec-dispatcher | 1 - src/mdo/model-version.vm | 19 +++++++++++++++++ 6 files changed, 34 insertions(+), 10 deletions(-) delete mode 100644 issue_comment.md delete mode 160000 plexus-sec-dispatcher diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java b/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java index c203f5fc7504..7aee624dae8e 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java @@ -62,7 +62,7 @@ public enum DependencyScope { COMPILE_ONLY("compile-only", false), /** - * Compile, runtime and test. + * Compile, runtime and test (non-transitive). */ COMPILE("compile", false), diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java b/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java index 67a869302a29..9e17b159f47a 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java @@ -616,7 +616,7 @@ private static boolean hasDependencyScope(Dependency dependency) { } else { scope = DependencyScope.forId(scopeId); } - return scope == null || !scope.isTransitive(); + return scope != DependencyScope.COMPILE && scope != DependencyScope.RUNTIME && scope != DependencyScope.API; } private Dependency merge(Dependency dep1, Dependency dep2) { diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java index 4283cfceaec2..11b733216dca 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java @@ -355,7 +355,7 @@ private void populateResult(InternalSession session, ArtifactDescriptorResult re logger.debug("Filtered dependency with uninterpolated expression: {}", dependency); continue; } - result.addDependency(convert(dependency, stereotypes)); + result.addDependency(convert(dependency, stereotypes, model)); } DependencyManagement dependencyManagement = model.getDependencyManagement(); @@ -365,7 +365,7 @@ private void populateResult(InternalSession session, ArtifactDescriptorResult re logger.debug("Filtered managed dependency with uninterpolated expression: {}", dependency); continue; } - result.addManagedDependency(convert(dependency, stereotypes)); + result.addManagedDependency(convert(dependency, stereotypes, model)); } } @@ -391,7 +391,8 @@ private void populateResult(InternalSession session, ArtifactDescriptorResult re setArtifactProperties(result, model); } - private Dependency convert(org.apache.maven.api.model.Dependency dependency, ArtifactTypeRegistry stereotypes) { + private Dependency convert( + org.apache.maven.api.model.Dependency dependency, ArtifactTypeRegistry stereotypes, Model model) { ArtifactType stereotype = stereotypes.get(dependency.getType()); if (stereotype == null) { stereotype = new DefaultType(dependency.getType(), Language.NONE, dependency.getType(), null, false) @@ -420,11 +421,16 @@ private Dependency convert(org.apache.maven.api.model.Dependency dependency, Art exclusions.add(convert(exclusion)); } + String scope = dependency.getScope() != null ? dependency.getScope() : ""; + if ("compile".equals(scope) || "".equals(scope)) { + String modelVersion = new org.apache.maven.model.v4.MavenModelVersion().getModelVersion(model); + if (modelVersion == null || modelVersion.startsWith("4.0.")) { + scope = "api"; + } + } + return new Dependency( - artifact, - dependency.getScope(), - dependency.getOptional() != null ? dependency.isOptional() : null, - exclusions); + artifact, scope, dependency.getOptional() != null ? dependency.isOptional() : null, exclusions); } private Exclusion convert(org.apache.maven.api.model.Exclusion exclusion) { diff --git a/issue_comment.md b/issue_comment.md deleted file mode 100644 index 7859d8fe47e15c9b14abc376541dcea6369cbbb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2418 zcmb7`-A>y;5QXO&sqbK^7nHVC1u9iSs1j|ZiVIqV`U1gmOd!}%5)*j&w&$D4VjSa( z%8Gw>XJ^lxIWwETf6gqkYa7|U6?$7*V^?-(Q(NgOY^<@7eiMy6__MZ!o-3`H$~x1% z(*43Nb$@LKc4(jNldcc8qwBZT8ZTsn=Y{W)Ym9H!vqz2~J9ipih{;-S zU-doJJX|bnPiu(;CLiQ|=6=c1%vmCTm2794&+J4NFm$VX>3f#;MdN`KOksx&aoo!; z#dGXgJ5csAd#meAe%Y;(XS8tcwz+3N7{2iMlh>uMI+o>`VhrtU?;E=5?jGK+JR0;i z6a!fTZ6=O*f&_(xB8fhByU{4o?fP7w57YpEtc;8&B}NYWYMlB$tV5G?WdjA1m4%*Q zV6MOJ(K)Mx7X6T8^iD=c?unen++veW*nZCWSpM0Qcp~3egdUP=VS`b4sU5{k@3rS3 z*$gS0NUg8l6f}sE3|S*60pL`o#(U!?C3^qs0ZDN_`UsVIxi;(8U?k za>*IbN%7I%$@Zl@bu*i)wh*>%CtM3_oZ)B;%!zHzyYPZa;f7hx&x!8o{9>%%d7~?H y>QkN9ii`gcnPOs(mci(xp>WuQs}?~&u6_MOqECf7UXC&F-?>$IT#Bhzs;Yl0{j%Qx diff --git a/plexus-sec-dispatcher b/plexus-sec-dispatcher deleted file mode 160000 index a3b5741891b7..000000000000 --- a/plexus-sec-dispatcher +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a3b5741891b7fd6004f8e52fd58d968f2282c61b diff --git a/src/mdo/model-version.vm b/src/mdo/model-version.vm index 0fcca232e44a..bdaccb404a09 100644 --- a/src/mdo/model-version.vm +++ b/src/mdo/model-version.vm @@ -161,11 +161,30 @@ public class ${className} { #end #set ( $pfx = "||" ) #end + #if ( $v == "4_1_0" && $class.name == "Model" ) + $pfx hasApiDependency(${var}) // Dependency scope api + #end ); } #end #end + private boolean hasApiDependency(Model model) { + return hasApiDependency((ModelBase) model) + || model.getProfiles().stream().anyMatch(this::hasApiDependency); + } + + private boolean hasApiDependency(Profile profile) { + return hasApiDependency((ModelBase) profile); + } + + private boolean hasApiDependency(ModelBase model) { + return model != null + && (model.getDependencies().stream().anyMatch(dependency -> "api".equals(dependency.getScope())) + || (model.getDependencyManagement() != null + && model.getDependencyManagement().getDependencies().stream() + .anyMatch(dependency -> "api".equals(dependency.getScope())))); + } private boolean has(String str) { return str != null; } From d4722ec06af4435ffbd5439e1a5f7340846fe0d2 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Mon, 17 Aug 2026 14:51:34 +0530 Subject: [PATCH 3/9] Add tests and fix review feedback for api scope Signed-off-by: Hitesh --- .../org/apache/maven/api/DependencyScope.java | 8 ++ .../java/org/apache/maven/api/PathScope.java | 4 +- .../maven/model/v4/MavenModelVersionTest.java | 41 +++++++ .../impl/ConsumerPomBuilderTest.java | 104 ++++++++++++++++++ .../DefaultArtifactDescriptorReader.java | 21 ++-- src/mdo/model-version.vm | 1 + 6 files changed, 169 insertions(+), 10 deletions(-) diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java b/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java index 7aee624dae8e..dc1e0c3650c8 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java @@ -63,6 +63,14 @@ public enum DependencyScope { /** * Compile, runtime and test (non-transitive). + *

+ * Note: If a project uses this scope and doesn't explicitly opt-in to + * Maven 4 modelVersion (e.g., 4.1.0) through other features, the generated + * consumer POM may be downgraded to 4.0.0. In this case, downstream resolvers + * will remap this scope to {@code api} for backward compatibility, making it + * behave transitively again. To enforce non-transitive behavior, the project + * must ensure its modelVersion is preserved (e.g., via {@code preserveModelVersion=true}). + *

*/ COMPILE("compile", false), diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java b/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java index 1379ca72c101..16188f23ca15 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java @@ -61,8 +61,8 @@ public interface PathScope extends ExtensibleEnum { DependencyScope.API, DependencyScope.PROVIDED); - PathScope MAIN_RUNTIME = - pathScope("main-runtime", ProjectScope.MAIN, DependencyScope.COMPILE, DependencyScope.API, DependencyScope.RUNTIME); + PathScope MAIN_RUNTIME = pathScope( + "main-runtime", ProjectScope.MAIN, DependencyScope.COMPILE, DependencyScope.API, DependencyScope.RUNTIME); PathScope TEST_COMPILE = pathScope( "test-compile", diff --git a/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java b/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java index 9c3555aefff7..bf8a5e32b4e2 100644 --- a/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java +++ b/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java @@ -20,8 +20,11 @@ import java.io.InputStream; import java.util.Collections; +import java.util.List; import org.apache.maven.api.model.Build; +import org.apache.maven.api.model.Dependency; +import org.apache.maven.api.model.DependencyManagement; import org.apache.maven.api.model.Model; import org.apache.maven.api.model.Plugin; import org.apache.maven.api.model.PluginExecution; @@ -72,4 +75,42 @@ void testV4ModelPriority() { PluginExecution.newInstance().withPriority(5)))))); assertEquals("4.0.0", new MavenModelVersion().getModelVersion(m)); } + + @Test + void testApiScopeDependencyRequires410() { + // A model with an api-scoped dependency should require modelVersion 4.1.0 + Model m = model.withDependencies(List.of(Dependency.newBuilder() + .groupId("org.example") + .artifactId("api-lib") + .version("1.0") + .scope("api") + .build())); + assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m)); + } + + @Test + void testApiScopeInDependencyManagementRequires410() { + // A model with an api-scoped dependency in dependencyManagement should require 4.1.0 + Model m = model.withDependencyManagement(DependencyManagement.newBuilder() + .dependencies(List.of(Dependency.newBuilder() + .groupId("org.example") + .artifactId("api-lib") + .version("1.0") + .scope("api") + .build())) + .build()); + assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m)); + } + + @Test + void testCompileScopeDependencyRemains400() { + // A model with only compile-scoped dependencies should stay at 4.0.0 + Model m = model.withDependencies(List.of(Dependency.newBuilder() + .groupId("org.example") + .artifactId("compile-lib") + .version("1.0") + .scope("compile") + .build())); + assertEquals("4.0.0", new MavenModelVersion().getModelVersion(m)); + } } diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java index 5f6661a66563..b093745a1381 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java @@ -1499,4 +1499,108 @@ void testHasNonModelPropertiesReturnsFalseForEnvAndSettingsProperties() { DefaultConsumerPomBuilder.hasNonModelProperties("${env.HOME}-${ext.qualifier}", Map.of()), "Should return true when at least one property is not built-in or in the model"); } + + @Test + void testConsumerPomRetainsCompileApiRuntimeDeps() throws Exception { + // Consumer POMs must retain compile, api, and runtime dependencies + org.apache.maven.api.model.Dependency compileDep = org.apache.maven.api.model.Dependency.newBuilder() + .groupId("g") + .artifactId("compile-dep") + .version("1") + .scope("compile") + .build(); + org.apache.maven.api.model.Dependency apiDep = org.apache.maven.api.model.Dependency.newBuilder() + .groupId("g") + .artifactId("api-dep") + .version("1") + .scope("api") + .build(); + org.apache.maven.api.model.Dependency runtimeDep = org.apache.maven.api.model.Dependency.newBuilder() + .groupId("g") + .artifactId("runtime-dep") + .version("1") + .scope("runtime") + .build(); + org.apache.maven.api.model.Dependency unscopedDep = org.apache.maven.api.model.Dependency.newBuilder() + .groupId("g") + .artifactId("unscoped-dep") + .version("1") + .build(); + + Model model = Model.newBuilder() + .groupId("test") + .artifactId("test") + .version("1.0") + .dependencies(List.of(compileDep, apiDep, runtimeDep, unscopedDep)) + .build(); + + Model transformed = DefaultConsumerPomBuilder.transformNonPom(model, null); + assertNotNull(transformed.getDependencies()); + // All four should be retained + assertTrue( + transformed.getDependencies().stream().anyMatch(d -> "compile-dep".equals(d.getArtifactId())), + "compile-scoped dep should be retained"); + assertTrue( + transformed.getDependencies().stream().anyMatch(d -> "api-dep".equals(d.getArtifactId())), + "api-scoped dep should be retained"); + assertTrue( + transformed.getDependencies().stream().anyMatch(d -> "runtime-dep".equals(d.getArtifactId())), + "runtime-scoped dep should be retained"); + assertTrue( + transformed.getDependencies().stream().anyMatch(d -> "unscoped-dep".equals(d.getArtifactId())), + "unscoped (default compile) dep should be retained"); + } + + @Test + void testConsumerPomStripsProvidedTestSystemDeps() throws Exception { + // Consumer POMs must strip provided, test, and system dependencies + org.apache.maven.api.model.Dependency compileDep = org.apache.maven.api.model.Dependency.newBuilder() + .groupId("g") + .artifactId("compile-dep") + .version("1") + .scope("compile") + .build(); + org.apache.maven.api.model.Dependency providedDep = org.apache.maven.api.model.Dependency.newBuilder() + .groupId("g") + .artifactId("provided-dep") + .version("1") + .scope("provided") + .build(); + org.apache.maven.api.model.Dependency testDep = org.apache.maven.api.model.Dependency.newBuilder() + .groupId("g") + .artifactId("test-dep") + .version("1") + .scope("test") + .build(); + org.apache.maven.api.model.Dependency systemDep = org.apache.maven.api.model.Dependency.newBuilder() + .groupId("g") + .artifactId("system-dep") + .version("1") + .scope("system") + .build(); + + Model model = Model.newBuilder() + .groupId("test") + .artifactId("test") + .version("1.0") + .dependencies(List.of(compileDep, providedDep, testDep, systemDep)) + .build(); + + Model transformed = DefaultConsumerPomBuilder.transformNonPom(model, null); + assertNotNull(transformed.getDependencies()); + // compile should be retained + assertTrue( + transformed.getDependencies().stream().anyMatch(d -> "compile-dep".equals(d.getArtifactId())), + "compile-scoped dep should be retained"); + // provided, test, system should be stripped + assertFalse( + transformed.getDependencies().stream().anyMatch(d -> "provided-dep".equals(d.getArtifactId())), + "provided-scoped dep should be stripped"); + assertFalse( + transformed.getDependencies().stream().anyMatch(d -> "test-dep".equals(d.getArtifactId())), + "test-scoped dep should be stripped"); + assertFalse( + transformed.getDependencies().stream().anyMatch(d -> "system-dep".equals(d.getArtifactId())), + "system-scoped dep should be stripped"); + } } diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java index 11b733216dca..433f3cfb5b34 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java @@ -345,6 +345,12 @@ private int getPolicy(RepositorySystemSession session, Artifact a, ArtifactDescr private void populateResult(InternalSession session, ArtifactDescriptorResult result, Model model) { ArtifactTypeRegistry stereotypes = session.getSession().getArtifactTypeRegistry(); + // Compute once whether compile-scoped dependencies should be remapped to api (transitive) + // for backward compatibility. Use the declared modelVersion rather than feature detection + // to respect the developer's explicit intent. + String declaredModelVersion = model.getModelVersion(); + boolean remapCompileToApi = declaredModelVersion == null || declaredModelVersion.startsWith("4.0."); + for (Repository repository : model.getRepositories()) { result.addRepository(session.toRepository( session.getService(RepositoryFactory.class).createRemote(repository))); @@ -355,7 +361,7 @@ private void populateResult(InternalSession session, ArtifactDescriptorResult re logger.debug("Filtered dependency with uninterpolated expression: {}", dependency); continue; } - result.addDependency(convert(dependency, stereotypes, model)); + result.addDependency(convert(dependency, stereotypes, remapCompileToApi)); } DependencyManagement dependencyManagement = model.getDependencyManagement(); @@ -365,7 +371,7 @@ private void populateResult(InternalSession session, ArtifactDescriptorResult re logger.debug("Filtered managed dependency with uninterpolated expression: {}", dependency); continue; } - result.addManagedDependency(convert(dependency, stereotypes, model)); + result.addManagedDependency(convert(dependency, stereotypes, remapCompileToApi)); } } @@ -392,7 +398,9 @@ private void populateResult(InternalSession session, ArtifactDescriptorResult re } private Dependency convert( - org.apache.maven.api.model.Dependency dependency, ArtifactTypeRegistry stereotypes, Model model) { + org.apache.maven.api.model.Dependency dependency, + ArtifactTypeRegistry stereotypes, + boolean remapCompileToApi) { ArtifactType stereotype = stereotypes.get(dependency.getType()); if (stereotype == null) { stereotype = new DefaultType(dependency.getType(), Language.NONE, dependency.getType(), null, false) @@ -422,11 +430,8 @@ private Dependency convert( } String scope = dependency.getScope() != null ? dependency.getScope() : ""; - if ("compile".equals(scope) || "".equals(scope)) { - String modelVersion = new org.apache.maven.model.v4.MavenModelVersion().getModelVersion(model); - if (modelVersion == null || modelVersion.startsWith("4.0.")) { - scope = "api"; - } + if (remapCompileToApi && ("compile".equals(scope) || scope.isEmpty())) { + scope = "api"; } return new Dependency( diff --git a/src/mdo/model-version.vm b/src/mdo/model-version.vm index bdaccb404a09..b7be7f1c975a 100644 --- a/src/mdo/model-version.vm +++ b/src/mdo/model-version.vm @@ -185,6 +185,7 @@ public class ${className} { && model.getDependencyManagement().getDependencies().stream() .anyMatch(dependency -> "api".equals(dependency.getScope())))); } + private boolean has(String str) { return str != null; } From bb369a86f10b47659457feeb404589dcfb73cc70 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Mon, 17 Aug 2026 15:57:27 +0530 Subject: [PATCH 4/9] Add tests for descriptor reader remap and profile api scope Signed-off-by: Hitesh --- .../maven/model/v4/MavenModelVersionTest.java | 15 +++++++++++++++ .../impl/DefaultConsumerPomBuilder.java | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java b/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java index bf8a5e32b4e2..7a9618e77267 100644 --- a/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java +++ b/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java @@ -113,4 +113,19 @@ void testCompileScopeDependencyRemains400() { .build())); assertEquals("4.0.0", new MavenModelVersion().getModelVersion(m)); } + + @Test + void testApiScopeInProfileRequires410() { + // A model with an api-scoped dependency in a profile should require 4.1.0 + Model m = model.withProfiles(List.of(org.apache.maven.api.model.Profile.newBuilder() + .id("my-profile") + .dependencies(List.of(Dependency.newBuilder() + .groupId("org.example") + .artifactId("api-lib") + .version("1.0") + .scope("api") + .build())) + .build())); + assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m)); + } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java b/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java index 9e17b159f47a..995dede6f6c6 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java @@ -582,7 +582,7 @@ private Model buildEffectiveModel(RepositorySystemSession session, MavenProject } return dependency; }); - // Only keep transitive scopes (null/empty => COMPILE) + // Only keep consumer-visible scopes (compile, api, runtime) directDependencies.values().removeIf(DefaultConsumerPomBuilder::hasDependencyScope); managedDependencies.keySet().removeAll(directDependencies.keySet()); @@ -600,7 +600,7 @@ private Model buildEffectiveModel(RepositorySystemSession session, MavenProject Function.identity(), this::merge, LinkedHashMap::new)); - // Only keep transitive scopes + // Only keep consumer-visible scopes (compile, api, runtime) directDependencies.values().removeIf(DefaultConsumerPomBuilder::hasDependencyScope); model = model.withDependencies(directDependencies.isEmpty() ? null : directDependencies.values()); } From 1124211cad94c36b77e7aed797c91fe2a5eef042 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Mon, 17 Aug 2026 15:57:49 +0530 Subject: [PATCH 5/9] Add unit test for DefaultArtifactDescriptorReader remap logic Signed-off-by: Hitesh --- .../DefaultArtifactDescriptorReaderTest.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReaderTest.java diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReaderTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReaderTest.java new file mode 100644 index 000000000000..c9a7cce408e6 --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReaderTest.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.impl.resolver; + +import java.lang.reflect.Method; + +import org.apache.maven.api.model.Dependency; +import org.eclipse.aether.artifact.ArtifactType; +import org.eclipse.aether.artifact.ArtifactTypeRegistry; +import org.eclipse.aether.artifact.DefaultArtifactType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DefaultArtifactDescriptorReaderTest { + + @Test + void testRemapCompileToApi() throws Exception { + // Create an instance of DefaultArtifactDescriptorReader + DefaultArtifactDescriptorReader reader = new DefaultArtifactDescriptorReader(null, null, null, null, null); + + // Get the private convert() method via reflection + Method convertMethod = DefaultArtifactDescriptorReader.class.getDeclaredMethod( + "convert", Dependency.class, ArtifactTypeRegistry.class, boolean.class); + convertMethod.setAccessible(true); + + // Mock ArtifactTypeRegistry + ArtifactTypeRegistry stereotypes = new ArtifactTypeRegistry() { + @Override + public ArtifactType get(String typeId) { + return new DefaultArtifactType(typeId, "jar", "", "java"); + } + }; + + // 1. Test remapCompileToApi = true, scope = compile -> api + Dependency depCompile = Dependency.newBuilder() + .groupId("g") + .artifactId("a") + .version("1") + .type("jar") + .scope("compile") + .build(); + org.eclipse.aether.graph.Dependency aetherDep1 = + (org.eclipse.aether.graph.Dependency) convertMethod.invoke(reader, depCompile, stereotypes, true); + assertEquals("api", aetherDep1.getScope(), "Compile scope should be remapped to api when remap=true"); + + // 2. Test remapCompileToApi = true, scope = "" -> api + Dependency depEmpty = Dependency.newBuilder() + .groupId("g") + .artifactId("a") + .version("1") + .type("jar") + .scope("") + .build(); + org.eclipse.aether.graph.Dependency aetherDep2 = + (org.eclipse.aether.graph.Dependency) convertMethod.invoke(reader, depEmpty, stereotypes, true); + assertEquals("api", aetherDep2.getScope(), "Empty scope should be remapped to api when remap=true"); + + // 3. Test remapCompileToApi = false, scope = compile -> compile + org.eclipse.aether.graph.Dependency aetherDep3 = + (org.eclipse.aether.graph.Dependency) convertMethod.invoke(reader, depCompile, stereotypes, false); + assertEquals("compile", aetherDep3.getScope(), "Compile scope should remain compile when remap=false"); + + // 4. Test remapCompileToApi = false, scope = "" -> "" + org.eclipse.aether.graph.Dependency aetherDep4 = + (org.eclipse.aether.graph.Dependency) convertMethod.invoke(reader, depEmpty, stereotypes, false); + assertEquals("", aetherDep4.getScope(), "Empty scope should remain empty when remap=false"); + } +} From 3c8322e778f2f6d36d0b150cc4e618b5b35ea884 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Mon, 17 Aug 2026 16:18:06 +0530 Subject: [PATCH 6/9] Fix DefaultArtifactDescriptorReaderTest constructor NPE using Mockito Signed-off-by: Hitesh --- .../DefaultArtifactDescriptorReaderTest.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReaderTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReaderTest.java index c9a7cce408e6..d5051078787b 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReaderTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReaderTest.java @@ -19,12 +19,18 @@ package org.apache.maven.impl.resolver; import java.lang.reflect.Method; +import java.util.Collections; import org.apache.maven.api.model.Dependency; +import org.apache.maven.api.services.ModelBuilder; import org.eclipse.aether.artifact.ArtifactType; import org.eclipse.aether.artifact.ArtifactTypeRegistry; import org.eclipse.aether.artifact.DefaultArtifactType; +import org.eclipse.aether.impl.ArtifactResolver; +import org.eclipse.aether.impl.RepositoryEventDispatcher; +import org.eclipse.aether.impl.VersionResolver; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -33,7 +39,12 @@ class DefaultArtifactDescriptorReaderTest { @Test void testRemapCompileToApi() throws Exception { // Create an instance of DefaultArtifactDescriptorReader - DefaultArtifactDescriptorReader reader = new DefaultArtifactDescriptorReader(null, null, null, null, null); + DefaultArtifactDescriptorReader reader = new DefaultArtifactDescriptorReader( + Mockito.mock(VersionResolver.class), + Mockito.mock(ArtifactResolver.class), + Mockito.mock(ModelBuilder.class), + Mockito.mock(RepositoryEventDispatcher.class), + Collections.emptyMap()); // Get the private convert() method via reflection Method convertMethod = DefaultArtifactDescriptorReader.class.getDeclaredMethod( From feaf9e917d355c9b4544e50335a6ee0f6a1e7ee8 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 9 Sep 2026 23:21:23 +0000 Subject: [PATCH 7/9] Redesign MNG-8099: additive api/implementation scopes for modelVersion 4.2.0+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the approach of making compile non-transitive with an additive design: - Keep compile scope unchanged (transitive=true) — no semantic flip - Add API scope: semantically identical to compile, expresses intent that a dependency is part of the public API (transitive, all path scopes) - Add IMPLEMENTATION scope: non-transitive, for internal dependencies that consumers should not compile against (present on all path scopes of the declaring project, but not exposed transitively) Both new scopes are only valid for modelVersion 4.2.0+. The validator rejects them on 4.0.0/4.1.0 POMs. MavenModelVersion detects them and forces the declared version to 4.2.0. Consumer POM compatibility (clean 4.0.0 equivalents, no forced upgrade): - api → compile (semantically identical) - implementation → runtime (consumers can't compile against it, present at runtime — slightly wider transitivity than ideal but safe) The DefaultArtifactDescriptorReader remap hack (compile→api for 4.0.0 POMs) is removed entirely — it is no longer needed since compile remains transitive. Tests: - MavenModelVersionTest: verify api/implementation force 4.2.0 - ConsumerPomBuilderTest: verify scope filter and mapping logic directly --- .../org/apache/maven/api/DependencyScope.java | 29 +++--- .../java/org/apache/maven/api/PathScope.java | 10 +- .../maven/model/v4/MavenModelVersionTest.java | 30 ++++-- .../Maven4ScopeManagerConfiguration.java | 2 + .../impl/DefaultConsumerPomBuilder.java | 32 ++++++- .../impl/ConsumerPomBuilderTest.java | 96 ++++++++++--------- .../impl/model/DefaultModelValidator.java | 25 ++++- .../DefaultArtifactDescriptorReader.java | 25 ++--- .../Maven4ScopeManagerConfiguration.java | 2 + .../DefaultArtifactDescriptorReaderTest.java | 96 ------------------- src/mdo/model-version.vm | 24 ++--- 11 files changed, 172 insertions(+), 199 deletions(-) delete mode 100644 impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReaderTest.java diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java b/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java index dc1e0c3650c8..89e4a205eb55 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/DependencyScope.java @@ -62,23 +62,30 @@ public enum DependencyScope { COMPILE_ONLY("compile-only", false), /** - * Compile, runtime and test (non-transitive). - *

- * Note: If a project uses this scope and doesn't explicitly opt-in to - * Maven 4 modelVersion (e.g., 4.1.0) through other features, the generated - * consumer POM may be downgraded to 4.0.0. In this case, downstream resolvers - * will remap this scope to {@code api} for backward compatibility, making it - * behave transitively again. To enforce non-transitive behavior, the project - * must ensure its modelVersion is preserved (e.g., via {@code preserveModelVersion=true}). - *

+ * Compile, runtime and test. */ - COMPILE("compile", false), + COMPILE("compile", true), /** - * Compile, runtime and test, transitively. + * Compile, runtime and test, transitively exposed to consumers. + * Semantically equivalent to {@code compile}, but explicitly declares + * that this dependency forms part of the project's public API. + * Only valid for {@code modelVersion 4.2.0+}. + * + * @since 4.2.0 */ API("api", true), + /** + * Compile, runtime and test, but not transitively exposed to consumers. + * Use for dependencies that are internal implementation details. + * Mapped to {@code runtime} scope in consumer POMs for Maven 3 compatibility. + * Only valid for {@code modelVersion 4.2.0+}. + * + * @since 4.2.0 + */ + IMPLEMENTATION("implementation", false), + /** * Runtime and test. */ diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java b/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java index 16188f23ca15..42ec56cc63ed 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/PathScope.java @@ -59,16 +59,23 @@ public interface PathScope extends ExtensibleEnum { DependencyScope.COMPILE_ONLY, DependencyScope.COMPILE, DependencyScope.API, + DependencyScope.IMPLEMENTATION, DependencyScope.PROVIDED); PathScope MAIN_RUNTIME = pathScope( - "main-runtime", ProjectScope.MAIN, DependencyScope.COMPILE, DependencyScope.API, DependencyScope.RUNTIME); + "main-runtime", + ProjectScope.MAIN, + DependencyScope.COMPILE, + DependencyScope.API, + DependencyScope.IMPLEMENTATION, + DependencyScope.RUNTIME); PathScope TEST_COMPILE = pathScope( "test-compile", ProjectScope.TEST, DependencyScope.COMPILE, DependencyScope.API, + DependencyScope.IMPLEMENTATION, DependencyScope.PROVIDED, DependencyScope.TEST_ONLY, DependencyScope.TEST); @@ -78,6 +85,7 @@ public interface PathScope extends ExtensibleEnum { ProjectScope.TEST, DependencyScope.COMPILE, DependencyScope.API, + DependencyScope.IMPLEMENTATION, DependencyScope.RUNTIME, DependencyScope.PROVIDED, DependencyScope.TEST, diff --git a/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java b/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java index 7a9618e77267..d89b14146772 100644 --- a/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java +++ b/compat/maven-model/src/test/java/org/apache/maven/model/v4/MavenModelVersionTest.java @@ -77,20 +77,32 @@ void testV4ModelPriority() { } @Test - void testApiScopeDependencyRequires410() { - // A model with an api-scoped dependency should require modelVersion 4.1.0 + void testApiScopeDependencyRequires420() { + // A model with an api-scoped dependency should require modelVersion 4.2.0 Model m = model.withDependencies(List.of(Dependency.newBuilder() .groupId("org.example") .artifactId("api-lib") .version("1.0") .scope("api") .build())); - assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m)); + assertEquals("4.2.0", new MavenModelVersion().getModelVersion(m)); + } + + @Test + void testImplementationScopeDependencyRequires420() { + // A model with an implementation-scoped dependency should require modelVersion 4.2.0 + Model m = model.withDependencies(List.of(Dependency.newBuilder() + .groupId("org.example") + .artifactId("impl-lib") + .version("1.0") + .scope("implementation") + .build())); + assertEquals("4.2.0", new MavenModelVersion().getModelVersion(m)); } @Test - void testApiScopeInDependencyManagementRequires410() { - // A model with an api-scoped dependency in dependencyManagement should require 4.1.0 + void testApiScopeInDependencyManagementRequires420() { + // A model with an api-scoped dependency in dependencyManagement should require 4.2.0 Model m = model.withDependencyManagement(DependencyManagement.newBuilder() .dependencies(List.of(Dependency.newBuilder() .groupId("org.example") @@ -99,7 +111,7 @@ void testApiScopeInDependencyManagementRequires410() { .scope("api") .build())) .build()); - assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m)); + assertEquals("4.2.0", new MavenModelVersion().getModelVersion(m)); } @Test @@ -115,8 +127,8 @@ void testCompileScopeDependencyRemains400() { } @Test - void testApiScopeInProfileRequires410() { - // A model with an api-scoped dependency in a profile should require 4.1.0 + void testApiScopeInProfileRequires420() { + // A model with an api-scoped dependency in a profile should require 4.2.0 Model m = model.withProfiles(List.of(org.apache.maven.api.model.Profile.newBuilder() .id("my-profile") .dependencies(List.of(Dependency.newBuilder() @@ -126,6 +138,6 @@ void testApiScopeInProfileRequires410() { .scope("api") .build())) .build())); - assertEquals("4.1.0", new MavenModelVersion().getModelVersion(m)); + assertEquals("4.2.0", new MavenModelVersion().getModelVersion(m)); } } diff --git a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/scopes/Maven4ScopeManagerConfiguration.java b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/scopes/Maven4ScopeManagerConfiguration.java index 8cec4de3667d..52e61fa9bbf6 100644 --- a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/scopes/Maven4ScopeManagerConfiguration.java +++ b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/scopes/Maven4ScopeManagerConfiguration.java @@ -92,6 +92,8 @@ public Collection buildDependencyScope DependencyScope.COMPILE.id(), DependencyScope.COMPILE.isTransitive(), all())); result.add(internalScopeManager.createDependencyScope( DependencyScope.API.id(), DependencyScope.API.isTransitive(), all())); + result.add(internalScopeManager.createDependencyScope( + DependencyScope.IMPLEMENTATION.id(), DependencyScope.IMPLEMENTATION.isTransitive(), all())); result.add(internalScopeManager.createDependencyScope( DependencyScope.RUNTIME.id(), DependencyScope.RUNTIME.isTransitive(), diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java b/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java index 995dede6f6c6..6119f9ab73d1 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/transformation/impl/DefaultConsumerPomBuilder.java @@ -582,8 +582,10 @@ private Model buildEffectiveModel(RepositorySystemSession session, MavenProject } return dependency; }); - // Only keep consumer-visible scopes (compile, api, runtime) + // Only keep consumer-visible scopes (compile, api, runtime, implementation) directDependencies.values().removeIf(DefaultConsumerPomBuilder::hasDependencyScope); + // Map 4.2.0 scopes to their 4.0.0 consumer POM equivalents (api→compile, implementation→runtime) + directDependencies.replaceAll((k, v) -> mapScopeForConsumerPom(v)); managedDependencies.keySet().removeAll(directDependencies.keySet()); model = model.withDependencyManagement( @@ -600,15 +602,17 @@ private Model buildEffectiveModel(RepositorySystemSession session, MavenProject Function.identity(), this::merge, LinkedHashMap::new)); - // Only keep consumer-visible scopes (compile, api, runtime) + // Only keep consumer-visible scopes (compile, api, runtime, implementation) directDependencies.values().removeIf(DefaultConsumerPomBuilder::hasDependencyScope); + // Map 4.2.0 scopes to their 4.0.0 consumer POM equivalents (api→compile, implementation→runtime) + directDependencies.replaceAll((k, v) -> mapScopeForConsumerPom(v)); model = model.withDependencies(directDependencies.isEmpty() ? null : directDependencies.values()); } return model; } - private static boolean hasDependencyScope(Dependency dependency) { + static boolean hasDependencyScope(Dependency dependency) { String scopeId = dependency.getScope(); DependencyScope scope; if (scopeId == null || scopeId.isEmpty()) { @@ -616,7 +620,27 @@ private static boolean hasDependencyScope(Dependency dependency) { } else { scope = DependencyScope.forId(scopeId); } - return scope != DependencyScope.COMPILE && scope != DependencyScope.RUNTIME && scope != DependencyScope.API; + return scope != DependencyScope.COMPILE + && scope != DependencyScope.RUNTIME + && scope != DependencyScope.API + && scope != DependencyScope.IMPLEMENTATION; + } + + /** + * Maps a 4.2.0 dependency scope to its 4.0.0 consumer POM equivalent. + *
    + *
  • {@code api} → {@code compile} (semantically identical, expresses intent)
  • + *
  • {@code implementation} → {@code runtime} (consumers cannot compile against it, present at runtime)
  • + *
+ */ + static Dependency mapScopeForConsumerPom(Dependency dependency) { + String scope = dependency.getScope(); + if (DependencyScope.API.id().equals(scope)) { + return dependency.withScope("compile"); + } else if (DependencyScope.IMPLEMENTATION.id().equals(scope)) { + return dependency.withScope("runtime"); + } + return dependency; } private Dependency merge(Dependency dep1, Dependency dep2) { diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java index b093745a1381..2d3a6ff8656e 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/transformation/impl/ConsumerPomBuilderTest.java @@ -1552,55 +1552,57 @@ void testConsumerPomRetainsCompileApiRuntimeDeps() throws Exception { } @Test - void testConsumerPomStripsProvidedTestSystemDeps() throws Exception { - // Consumer POMs must strip provided, test, and system dependencies - org.apache.maven.api.model.Dependency compileDep = org.apache.maven.api.model.Dependency.newBuilder() - .groupId("g") - .artifactId("compile-dep") - .version("1") - .scope("compile") - .build(); - org.apache.maven.api.model.Dependency providedDep = org.apache.maven.api.model.Dependency.newBuilder() - .groupId("g") - .artifactId("provided-dep") - .version("1") - .scope("provided") - .build(); - org.apache.maven.api.model.Dependency testDep = org.apache.maven.api.model.Dependency.newBuilder() - .groupId("g") - .artifactId("test-dep") - .version("1") - .scope("test") - .build(); - org.apache.maven.api.model.Dependency systemDep = org.apache.maven.api.model.Dependency.newBuilder() - .groupId("g") - .artifactId("system-dep") - .version("1") - .scope("system") - .build(); - - Model model = Model.newBuilder() - .groupId("test") - .artifactId("test") - .version("1.0") - .dependencies(List.of(compileDep, providedDep, testDep, systemDep)) - .build(); - - Model transformed = DefaultConsumerPomBuilder.transformNonPom(model, null); - assertNotNull(transformed.getDependencies()); - // compile should be retained - assertTrue( - transformed.getDependencies().stream().anyMatch(d -> "compile-dep".equals(d.getArtifactId())), - "compile-scoped dep should be retained"); - // provided, test, system should be stripped + void testConsumerPomScopeFilter() { + // hasDependencyScope returns true for deps to REMOVE + // compile/runtime/api/implementation should be KEPT (returns false) + assertFalse(DefaultConsumerPomBuilder.hasDependencyScope(dep("compile")), "compile should be kept"); + assertFalse(DefaultConsumerPomBuilder.hasDependencyScope(dep("runtime")), "runtime should be kept"); + assertFalse(DefaultConsumerPomBuilder.hasDependencyScope(dep("api")), "api should be kept"); assertFalse( - transformed.getDependencies().stream().anyMatch(d -> "provided-dep".equals(d.getArtifactId())), - "provided-scoped dep should be stripped"); + DefaultConsumerPomBuilder.hasDependencyScope(dep("implementation")), "implementation should be kept"); assertFalse( - transformed.getDependencies().stream().anyMatch(d -> "test-dep".equals(d.getArtifactId())), - "test-scoped dep should be stripped"); + DefaultConsumerPomBuilder.hasDependencyScope(dep(null)), + "unscoped (defaults to compile) should be kept"); assertFalse( - transformed.getDependencies().stream().anyMatch(d -> "system-dep".equals(d.getArtifactId())), - "system-scoped dep should be stripped"); + DefaultConsumerPomBuilder.hasDependencyScope(dep("")), + "empty scope (defaults to compile) should be kept"); + // provided/test/system should be STRIPPED (returns true) + assertTrue(DefaultConsumerPomBuilder.hasDependencyScope(dep("provided")), "provided should be stripped"); + assertTrue(DefaultConsumerPomBuilder.hasDependencyScope(dep("test")), "test should be stripped"); + assertTrue(DefaultConsumerPomBuilder.hasDependencyScope(dep("system")), "system should be stripped"); + assertTrue(DefaultConsumerPomBuilder.hasDependencyScope(dep("test-only")), "test-only should be stripped"); + } + + @Test + void testConsumerPomScopeMappings() { + // api → compile, implementation → runtime, others unchanged + assertEquals( + "compile", + DefaultConsumerPomBuilder.mapScopeForConsumerPom(dep("api")).getScope(), + "api should map to compile in consumer POM"); + assertEquals( + "runtime", + DefaultConsumerPomBuilder.mapScopeForConsumerPom(dep("implementation")) + .getScope(), + "implementation should map to runtime in consumer POM"); + assertEquals( + "compile", + DefaultConsumerPomBuilder.mapScopeForConsumerPom(dep("compile")).getScope(), + "compile should remain compile"); + assertEquals( + "runtime", + DefaultConsumerPomBuilder.mapScopeForConsumerPom(dep("runtime")).getScope(), + "runtime should remain runtime"); + } + + private static org.apache.maven.api.model.Dependency dep(String scope) { + org.apache.maven.api.model.Dependency.Builder b = org.apache.maven.api.model.Dependency.newBuilder() + .groupId("g") + .artifactId("a") + .version("1"); + if (scope != null) { + b.scope(scope); + } + return b.build(); } } 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 d5582bc7521c..0148edae54d5 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 @@ -500,6 +500,7 @@ && equals(parent.getArtifactId(), model.getArtifactId())) { } boolean isModelVersion41OrMore = !Objects.equals(ModelBuilder.MODEL_VERSION_4_0_0, model.getModelVersion()); + boolean isModelVersion42OrMore = Objects.equals(ModelBuilder.MODEL_VERSION_4_2_0, model.getModelVersion()); if (isModelVersion41OrMore) { validateStringNoExpression("groupId", problems, Severity.FATAL, Version.V41, model.getGroupId(), model); @@ -547,6 +548,7 @@ && equals(parent.getArtifactId(), model.getArtifactId())) { "dependencies.dependency.", EMPTY, isModelVersion41OrMore, + isModelVersion42OrMore, validationLevel); validate20RawDependenciesSelfReferencing( @@ -559,6 +561,7 @@ && equals(parent.getArtifactId(), model.getArtifactId())) { "dependencyManagement.dependencies.dependency.", EMPTY, isModelVersion41OrMore, + isModelVersion42OrMore, validationLevel); } @@ -603,6 +606,7 @@ && equals(parent.getArtifactId(), model.getArtifactId())) { prefix, "dependencies.dependency.", isModelVersion41OrMore, + isModelVersion42OrMore, validationLevel); if (profile.getDependencyManagement() != null) { @@ -612,6 +616,7 @@ && equals(parent.getArtifactId(), model.getArtifactId())) { prefix, "dependencyManagement.dependencies.dependency.", isModelVersion41OrMore, + isModelVersion42OrMore, validationLevel); } @@ -1183,6 +1188,7 @@ private void validate20RawDependencies( String prefix, String prefix2, boolean is41OrBeyond, + boolean is42OrBeyond, int validationLevel) { Severity errOn30 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_0); Severity errOn31 = getSeverity(validationLevel, ModelValidator.VALIDATION_LEVEL_MAVEN_3_1); @@ -1258,8 +1264,7 @@ private void validate20RawDependencies( String scope = dependency.getScope(); if (DependencyScope.COMPILE_ONLY.id().equals(scope) || DependencyScope.TEST_ONLY.id().equals(scope) - || DependencyScope.TEST_RUNTIME.id().equals(scope) - || DependencyScope.API.id().equals(scope)) { + || DependencyScope.TEST_RUNTIME.id().equals(scope)) { addViolation( problems, Severity.ERROR, @@ -1271,6 +1276,22 @@ private void validate20RawDependencies( dependency); } } + // MNG-8099: api and implementation scopes require modelVersion 4.2.0+ + if (!is42OrBeyond) { + String scope = dependency.getScope(); + if (DependencyScope.API.id().equals(scope) + || DependencyScope.IMPLEMENTATION.id().equals(scope)) { + addViolation( + problems, + Severity.ERROR, + Version.V20, + prefix + prefix2 + "scope", + SourceHint.dependencyManagementKey(dependency), + "scope '" + scope + "' is not supported with modelVersion 4.0.0 or 4.1.0; " + + "use modelVersion 4.2.0 or remove this scope.", + dependency); + } + } if (equals("LATEST", dependency.getVersion()) || equals("RELEASE", dependency.getVersion())) { addViolation( diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java index 433f3cfb5b34..4283cfceaec2 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReader.java @@ -345,12 +345,6 @@ private int getPolicy(RepositorySystemSession session, Artifact a, ArtifactDescr private void populateResult(InternalSession session, ArtifactDescriptorResult result, Model model) { ArtifactTypeRegistry stereotypes = session.getSession().getArtifactTypeRegistry(); - // Compute once whether compile-scoped dependencies should be remapped to api (transitive) - // for backward compatibility. Use the declared modelVersion rather than feature detection - // to respect the developer's explicit intent. - String declaredModelVersion = model.getModelVersion(); - boolean remapCompileToApi = declaredModelVersion == null || declaredModelVersion.startsWith("4.0."); - for (Repository repository : model.getRepositories()) { result.addRepository(session.toRepository( session.getService(RepositoryFactory.class).createRemote(repository))); @@ -361,7 +355,7 @@ private void populateResult(InternalSession session, ArtifactDescriptorResult re logger.debug("Filtered dependency with uninterpolated expression: {}", dependency); continue; } - result.addDependency(convert(dependency, stereotypes, remapCompileToApi)); + result.addDependency(convert(dependency, stereotypes)); } DependencyManagement dependencyManagement = model.getDependencyManagement(); @@ -371,7 +365,7 @@ private void populateResult(InternalSession session, ArtifactDescriptorResult re logger.debug("Filtered managed dependency with uninterpolated expression: {}", dependency); continue; } - result.addManagedDependency(convert(dependency, stereotypes, remapCompileToApi)); + result.addManagedDependency(convert(dependency, stereotypes)); } } @@ -397,10 +391,7 @@ private void populateResult(InternalSession session, ArtifactDescriptorResult re setArtifactProperties(result, model); } - private Dependency convert( - org.apache.maven.api.model.Dependency dependency, - ArtifactTypeRegistry stereotypes, - boolean remapCompileToApi) { + private Dependency convert(org.apache.maven.api.model.Dependency dependency, ArtifactTypeRegistry stereotypes) { ArtifactType stereotype = stereotypes.get(dependency.getType()); if (stereotype == null) { stereotype = new DefaultType(dependency.getType(), Language.NONE, dependency.getType(), null, false) @@ -429,13 +420,11 @@ private Dependency convert( exclusions.add(convert(exclusion)); } - String scope = dependency.getScope() != null ? dependency.getScope() : ""; - if (remapCompileToApi && ("compile".equals(scope) || scope.isEmpty())) { - scope = "api"; - } - return new Dependency( - artifact, scope, dependency.getOptional() != null ? dependency.isOptional() : null, exclusions); + artifact, + dependency.getScope(), + dependency.getOptional() != null ? dependency.isOptional() : null, + exclusions); } private Exclusion convert(org.apache.maven.api.model.Exclusion exclusion) { diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/scopes/Maven4ScopeManagerConfiguration.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/scopes/Maven4ScopeManagerConfiguration.java index 8c64bc31f00c..f88edd1ab6f0 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/scopes/Maven4ScopeManagerConfiguration.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/scopes/Maven4ScopeManagerConfiguration.java @@ -90,6 +90,8 @@ public Collection buildDependencyScope DependencyScope.COMPILE.id(), DependencyScope.COMPILE.isTransitive(), all())); result.add(internalScopeManager.createDependencyScope( DependencyScope.API.id(), DependencyScope.API.isTransitive(), all())); + result.add(internalScopeManager.createDependencyScope( + DependencyScope.IMPLEMENTATION.id(), DependencyScope.IMPLEMENTATION.isTransitive(), all())); result.add(internalScopeManager.createDependencyScope( DependencyScope.RUNTIME.id(), DependencyScope.RUNTIME.isTransitive(), diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReaderTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReaderTest.java deleted file mode 100644 index d5051078787b..000000000000 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/DefaultArtifactDescriptorReaderTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.maven.impl.resolver; - -import java.lang.reflect.Method; -import java.util.Collections; - -import org.apache.maven.api.model.Dependency; -import org.apache.maven.api.services.ModelBuilder; -import org.eclipse.aether.artifact.ArtifactType; -import org.eclipse.aether.artifact.ArtifactTypeRegistry; -import org.eclipse.aether.artifact.DefaultArtifactType; -import org.eclipse.aether.impl.ArtifactResolver; -import org.eclipse.aether.impl.RepositoryEventDispatcher; -import org.eclipse.aether.impl.VersionResolver; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class DefaultArtifactDescriptorReaderTest { - - @Test - void testRemapCompileToApi() throws Exception { - // Create an instance of DefaultArtifactDescriptorReader - DefaultArtifactDescriptorReader reader = new DefaultArtifactDescriptorReader( - Mockito.mock(VersionResolver.class), - Mockito.mock(ArtifactResolver.class), - Mockito.mock(ModelBuilder.class), - Mockito.mock(RepositoryEventDispatcher.class), - Collections.emptyMap()); - - // Get the private convert() method via reflection - Method convertMethod = DefaultArtifactDescriptorReader.class.getDeclaredMethod( - "convert", Dependency.class, ArtifactTypeRegistry.class, boolean.class); - convertMethod.setAccessible(true); - - // Mock ArtifactTypeRegistry - ArtifactTypeRegistry stereotypes = new ArtifactTypeRegistry() { - @Override - public ArtifactType get(String typeId) { - return new DefaultArtifactType(typeId, "jar", "", "java"); - } - }; - - // 1. Test remapCompileToApi = true, scope = compile -> api - Dependency depCompile = Dependency.newBuilder() - .groupId("g") - .artifactId("a") - .version("1") - .type("jar") - .scope("compile") - .build(); - org.eclipse.aether.graph.Dependency aetherDep1 = - (org.eclipse.aether.graph.Dependency) convertMethod.invoke(reader, depCompile, stereotypes, true); - assertEquals("api", aetherDep1.getScope(), "Compile scope should be remapped to api when remap=true"); - - // 2. Test remapCompileToApi = true, scope = "" -> api - Dependency depEmpty = Dependency.newBuilder() - .groupId("g") - .artifactId("a") - .version("1") - .type("jar") - .scope("") - .build(); - org.eclipse.aether.graph.Dependency aetherDep2 = - (org.eclipse.aether.graph.Dependency) convertMethod.invoke(reader, depEmpty, stereotypes, true); - assertEquals("api", aetherDep2.getScope(), "Empty scope should be remapped to api when remap=true"); - - // 3. Test remapCompileToApi = false, scope = compile -> compile - org.eclipse.aether.graph.Dependency aetherDep3 = - (org.eclipse.aether.graph.Dependency) convertMethod.invoke(reader, depCompile, stereotypes, false); - assertEquals("compile", aetherDep3.getScope(), "Compile scope should remain compile when remap=false"); - - // 4. Test remapCompileToApi = false, scope = "" -> "" - org.eclipse.aether.graph.Dependency aetherDep4 = - (org.eclipse.aether.graph.Dependency) convertMethod.invoke(reader, depEmpty, stereotypes, false); - assertEquals("", aetherDep4.getScope(), "Empty scope should remain empty when remap=false"); - } -} diff --git a/src/mdo/model-version.vm b/src/mdo/model-version.vm index b7be7f1c975a..05f02f59d1fb 100644 --- a/src/mdo/model-version.vm +++ b/src/mdo/model-version.vm @@ -161,31 +161,33 @@ public class ${className} { #end #set ( $pfx = "||" ) #end - #if ( $v == "4_1_0" && $class.name == "Model" ) - $pfx hasApiDependency(${var}) // Dependency scope api + #if ( $v == "4_2_0" && $class.name == "Model" ) + $pfx hasNewScopes(${var}) // Dependency scopes api / implementation #end ); } #end #end - private boolean hasApiDependency(Model model) { - return hasApiDependency((ModelBase) model) - || model.getProfiles().stream().anyMatch(this::hasApiDependency); + private boolean hasNewScopes(Model model) { + return hasNewScopes((ModelBase) model) + || model.getProfiles().stream().anyMatch(this::hasNewScopes); } - private boolean hasApiDependency(Profile profile) { - return hasApiDependency((ModelBase) profile); + private boolean hasNewScopes(Profile profile) { + return hasNewScopes((ModelBase) profile); } - private boolean hasApiDependency(ModelBase model) { + private boolean hasNewScopes(ModelBase model) { return model != null - && (model.getDependencies().stream().anyMatch(dependency -> "api".equals(dependency.getScope())) + && (model.getDependencies().stream().anyMatch(dependency -> + "api".equals(dependency.getScope()) || "implementation".equals(dependency.getScope())) || (model.getDependencyManagement() != null && model.getDependencyManagement().getDependencies().stream() - .anyMatch(dependency -> "api".equals(dependency.getScope())))); + .anyMatch(dependency -> + "api".equals(dependency.getScope()) + || "implementation".equals(dependency.getScope())))); } - private boolean has(String str) { return str != null; } From 1caa5b926973e065eaa6753962dda212713eb5cb Mon Sep 17 00:00:00 2001 From: B V HITESH SAI Date: Thu, 10 Sep 2026 11:55:10 +0530 Subject: [PATCH 8/9] Update impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java Co-authored-by: Guillaume Nodet - AI Bot --- .../org/apache/maven/impl/model/DefaultModelValidator.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0148edae54d5..2bffb9a7be2d 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 @@ -500,7 +500,8 @@ && equals(parent.getArtifactId(), model.getArtifactId())) { } boolean isModelVersion41OrMore = !Objects.equals(ModelBuilder.MODEL_VERSION_4_0_0, model.getModelVersion()); - boolean isModelVersion42OrMore = Objects.equals(ModelBuilder.MODEL_VERSION_4_2_0, model.getModelVersion()); + boolean isModelVersion42OrMore = isModelVersion41OrMore + && !Objects.equals(ModelBuilder.MODEL_VERSION_4_1_0, model.getModelVersion()); if (isModelVersion41OrMore) { validateStringNoExpression("groupId", problems, Severity.FATAL, Version.V41, model.getGroupId(), model); From b094c4f1908e9ede63a149a894cf64a3668afc79 Mon Sep 17 00:00:00 2001 From: B V HITESH SAI Date: Thu, 10 Sep 2026 11:55:20 +0530 Subject: [PATCH 9/9] Update src/mdo/model-version.vm Co-authored-by: Guillaume Nodet - AI Bot --- src/mdo/model-version.vm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mdo/model-version.vm b/src/mdo/model-version.vm index 05f02f59d1fb..c3f67ac32ac8 100644 --- a/src/mdo/model-version.vm +++ b/src/mdo/model-version.vm @@ -188,6 +188,8 @@ public class ${className} { "api".equals(dependency.getScope()) || "implementation".equals(dependency.getScope())))); } + } + private boolean has(String str) { return str != null; }