From bda6953ec8e7ac5507bc9004196eeb899a5049c4 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Thu, 13 Aug 2026 20:33:03 +0530 Subject: [PATCH 1/6] 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 0c8739a22ade..6211f14d947b 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 @@ -1239,7 +1239,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 3a48a68e9984a0ac6724d17dcb7c3e490097a9f7 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Mon, 17 Aug 2026 10:58:27 +0530 Subject: [PATCH 2/6] 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 5aba71386686..bbe76762907d 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 @@ -291,7 +291,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 589b172e7e77f93e25a04d3f20891f989c8e6293 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Mon, 17 Aug 2026 14:51:34 +0530 Subject: [PATCH 3/6] 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 76a9752f5506..e87a97bc8020 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 @@ -276,4 +276,108 @@ void testConsumerPomPassesProjectRepositoriesToModelBuilder() throws Exception { consumerRequest.getRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId())); assertTrue(hasCustomRepo, "Consumer POM model builder request should include the project's custom repository"); } + + @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 03557bcaacf7ac99e9503d64462eebdd4bc45423 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Mon, 17 Aug 2026 15:57:27 +0530 Subject: [PATCH 4/6] 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 bbe76762907d..2bc05fe854fd 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 @@ -257,7 +257,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()); @@ -275,7 +275,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 587beb50267204442b87310b46dacc062838f61a Mon Sep 17 00:00:00 2001 From: Hitesh Date: Mon, 17 Aug 2026 15:57:49 +0530 Subject: [PATCH 5/6] 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 7d6761856de2167d2587584727c0cadc28471fb0 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Mon, 17 Aug 2026 16:18:06 +0530 Subject: [PATCH 6/6] 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(