From f362ddf2d2dd2b7aed898a6f2102501c8b22204b Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 27 Mar 2026 16:05:42 +0100 Subject: [PATCH 1/7] Advised and Enforced Management Originally, dependency management marked a "subject" as "managed", it was 0/1 true/false only. This PR now adds ability to mark management as "advised" (may be subjected to further management) or "enforced" (equivalent of previously "managed"). Changes: * `DependencyManager` applies management as "enforced", if they come from root (ie. project POM), otherwise as "advice" (this latter applies to transitive managers only, as for example classic manager have no data from "deeper levels"). * `ConflictManager` backs out for _enforced_ scope/optionality. * This makes the "isInheritedDerived" hack in dependency manager not needed, as now management rule tells the modality of it (is enforced or just advised) --- .../collection/DependencyManagement.java | 171 ++++++++++++++++-- .../aether/graph/DefaultDependencyNode.java | 81 ++++++++- .../eclipse/aether/graph/DependencyNode.java | 30 ++- .../collect/DependencyCollectorDelegate.java | 13 +- .../impl/collect/PremanagedDependency.java | 51 ++++-- .../collect/bf/BfDependencyCollector.java | 8 +- .../collect/df/DfDependencyCollector.java | 4 +- .../test/util/DependencyGraphParser.java | 9 +- .../manager/AbstractDependencyManager.java | 118 +++++------- .../graph/manager/DependencyManagerUtils.java | 11 +- .../manager/TransitiveDependencyManager.java | 26 --- .../transformer/ClassicConflictResolver.java | 5 +- .../transformer/PathConflictResolver.java | 5 +- 13 files changed, 362 insertions(+), 170 deletions(-) diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java index 71bfd51029..19c7bfbc05 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java @@ -19,6 +19,7 @@ package org.eclipse.aether.collection; import java.util.Collection; +import java.util.HashMap; import java.util.Map; import org.eclipse.aether.graph.Exclusion; @@ -29,22 +30,46 @@ * @see DependencyManager#manageDependency(org.eclipse.aether.graph.Dependency) */ public final class DependencyManagement { + /** + * Enumeration of manageable attributes, attributes that can be subjected to dependency management. + * + * @since 2.0.17 + */ + public enum Subject { + VERSION, + SCOPE, + OPTIONAL, + EXCLUSIONS, + PROPERTIES + } - private String version; - - private String scope; - - private Boolean optional; - - private Collection exclusions; - - private Map properties; + private final Map managedValues; + private final Map managedEnforced; /** * Creates an empty management update. */ public DependencyManagement() { - // enables default constructor + this.managedValues = new HashMap<>(); + this.managedEnforced = new HashMap<>(); + } + + /** + * Returns {@code true} if passed in subject is managed. + * + * @since 2.0.17 + */ + public boolean isManagedSubject(Subject subject) { + return managedValues.containsKey(subject); + } + + /** + * Returns {@code true} if passed in subject is managed and is enforced. + * + * @since 2.0.17 + */ + public boolean isManagedSubjectEnforced(Subject subject) { + return isManagedSubject(subject) && managedEnforced.getOrDefault(subject, false); } /** @@ -54,7 +79,7 @@ public DependencyManagement() { * remain unchanged. */ public String getVersion() { - return version; + return (String) managedValues.get(Subject.VERSION); } /** @@ -62,9 +87,29 @@ public String getVersion() { * * @param version The new version, may be {@code null} if the version is not managed. * @return This management update for chaining, never {@code null}. + * @deprecated Use {@link #setVersion(String, boolean)} instead. */ + @Deprecated public DependencyManagement setVersion(String version) { - this.version = version; + return setVersion(version, true); + } + + /** + * Sets the new version to apply to the dependency. + * + * @param version The new version, may be {@code null} if the version is not managed. + * @param enforced The enforcement of new value. + * @return This management update for chaining, never {@code null}. + * @since 2.0.17 + */ + public DependencyManagement setVersion(String version, boolean enforced) { + if (version == null) { + this.managedValues.remove(Subject.VERSION); + this.managedEnforced.remove(Subject.VERSION); + } else { + this.managedValues.put(Subject.VERSION, version); + this.managedEnforced.put(Subject.VERSION, enforced); + } return this; } @@ -75,7 +120,7 @@ public DependencyManagement setVersion(String version) { * unchanged. */ public String getScope() { - return scope; + return (String) managedValues.get(Subject.SCOPE); } /** @@ -83,9 +128,29 @@ public String getScope() { * * @param scope The new scope, may be {@code null} if the scope is not managed. * @return This management update for chaining, never {@code null}. + * @deprecated Use {@link #setScope(String, boolean)} instead. */ + @Deprecated public DependencyManagement setScope(String scope) { - this.scope = scope; + return setScope(scope, true); + } + + /** + * Sets the new scope to apply to the dependency. + * + * @param scope The new scope, may be {@code null} if the scope is not managed. + * @param enforced The enforcement of new value. + * @return This management update for chaining, never {@code null}. + * @since 2.0.17 + */ + public DependencyManagement setScope(String scope, boolean enforced) { + if (scope == null) { + this.managedValues.remove(Subject.SCOPE); + this.managedEnforced.remove(Subject.SCOPE); + } else { + this.managedValues.put(Subject.SCOPE, scope); + this.managedEnforced.put(Subject.SCOPE, enforced); + } return this; } @@ -96,7 +161,7 @@ public DependencyManagement setScope(String scope) { * dependency should remain unchanged. */ public Boolean getOptional() { - return optional; + return (Boolean) managedValues.get(Subject.OPTIONAL); } /** @@ -104,9 +169,29 @@ public Boolean getOptional() { * * @param optional The optional flag, may be {@code null} if the flag is not managed. * @return This management update for chaining, never {@code null}. + * @deprecated Use {@link #setOptional(Boolean, boolean)} instead. */ + @Deprecated public DependencyManagement setOptional(Boolean optional) { - this.optional = optional; + return setOptional(optional, true); + } + + /** + * Sets the new optional flag to apply to the dependency. + * + * @param optional The optional flag, may be {@code null} if the flag is not managed. + * @param enforced The enforcement of new value. + * @return This management update for chaining, never {@code null}. + * @since 2.0.17 + */ + public DependencyManagement setOptional(Boolean optional, boolean enforced) { + if (optional == null) { + this.managedValues.remove(Subject.OPTIONAL); + this.managedEnforced.remove(Subject.OPTIONAL); + } else { + this.managedValues.put(Subject.OPTIONAL, optional); + this.managedEnforced.put(Subject.OPTIONAL, enforced); + } return this; } @@ -118,8 +203,9 @@ public DependencyManagement setOptional(Boolean optional) { * @return The new exclusions or {@code null} if the exclusions are not managed and the existing dependency * exclusions should remain unchanged. */ + @SuppressWarnings("unchecked") public Collection getExclusions() { - return exclusions; + return (Collection) managedValues.get(Subject.EXCLUSIONS); } /** @@ -129,9 +215,31 @@ public Collection getExclusions() { * * @param exclusions The new exclusions, may be {@code null} if the exclusions are not managed. * @return This management update for chaining, never {@code null}. + * @deprecated Use {@link #setExclusions(Collection, boolean)} instead. */ + @Deprecated public DependencyManagement setExclusions(Collection exclusions) { - this.exclusions = exclusions; + return setExclusions(exclusions, true); + } + + /** + * Sets the new exclusions to apply to the dependency. Note that this collection denotes the complete set of + * exclusions for the dependency, i.e. the dependency manager controls whether any existing exclusions get merged + * with information from dependency management or overridden by it. + * + * @param exclusions The new exclusions, may be {@code null} if the exclusions are not managed. + * @param enforced The enforcement of new value. + * @return This management update for chaining, never {@code null}. + * @since 2.0.17 + */ + public DependencyManagement setExclusions(Collection exclusions, boolean enforced) { + if (exclusions == null) { + this.managedValues.remove(Subject.EXCLUSIONS); + this.managedEnforced.remove(Subject.EXCLUSIONS); + } else { + this.managedValues.put(Subject.EXCLUSIONS, exclusions); + this.managedEnforced.put(Subject.EXCLUSIONS, enforced); + } return this; } @@ -143,8 +251,9 @@ public DependencyManagement setExclusions(Collection exclusions) { * @return The new artifact properties or {@code null} if the properties are not managed and the existing properties * should remain unchanged. */ + @SuppressWarnings("unchecked") public Map getProperties() { - return properties; + return (Map) managedValues.get(Subject.PROPERTIES); } /** @@ -154,9 +263,31 @@ public Map getProperties() { * * @param properties The new artifact properties, may be {@code null} if the properties are not managed. * @return This management update for chaining, never {@code null}. + * @deprecated Use {@link #setProperties(Map, boolean)} instead. */ + @Deprecated public DependencyManagement setProperties(Map properties) { - this.properties = properties; + return setProperties(properties, true); + } + + /** + * Sets the new properties to apply to the dependency. Note that this map denotes the complete set of properties, + * i.e. the dependency manager controls whether any existing properties get merged with the information from + * dependency management or overridden by it. + * + * @param properties The new artifact properties, may be {@code null} if the properties are not managed. + * @param enforced The enforcement of new value. + * @return This management update for chaining, never {@code null}. + * @since 2.0.17 + */ + public DependencyManagement setProperties(Map properties, boolean enforced) { + if (properties == null) { + this.managedValues.remove(Subject.PROPERTIES); + this.managedEnforced.remove(Subject.PROPERTIES); + } else { + this.managedValues.put(Subject.PROPERTIES, properties); + this.managedEnforced.put(Subject.PROPERTIES, enforced); + } return this; } } diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java index 3d7c4fde0b..fdfdddf641 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java @@ -26,6 +26,7 @@ import java.util.Map; import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.collection.DependencyManagement; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.version.Version; import org.eclipse.aether.version.VersionConstraint; @@ -51,7 +52,7 @@ public final class DefaultDependencyNode implements DependencyNode { private Version version; - private byte managedBits; + private Map managedSubjects = Collections.emptyMap(); private List repositories; @@ -104,7 +105,19 @@ public DefaultDependencyNode(DependencyNode node) { children = new ArrayList<>(0); setAliases(node.getAliases()); setRequestContext(node.getRequestContext()); - setManagedBits(node.getManagedBits()); + + HashMap managedSubjects = new HashMap<>(); + for (DependencyManagement.Subject subject : DependencyManagement.Subject.values()) { + if (node.isManagedSubject(subject)) { + managedSubjects.put(subject, node.isManagedSubjectEnforced(subject)); + } + } + if (managedSubjects.isEmpty()) { + setManagedSubjects(null); + } else { + setManagedSubjects(managedSubjects); + } + setRelocations(node.getRelocations()); setRepositories(node.getRepositories()); setVersion(node.getVersion()); @@ -113,10 +126,12 @@ public DefaultDependencyNode(DependencyNode node) { setData(data.isEmpty() ? null : new HashMap<>(data)); } + @Override public List getChildren() { return children; } + @Override public void setChildren(List children) { if (children == null) { this.children = new ArrayList<>(0); @@ -125,14 +140,17 @@ public void setChildren(List children) { } } + @Override public Dependency getDependency() { return dependency; } + @Override public Artifact getArtifact() { return artifact; } + @Override public void setArtifact(Artifact artifact) { if (dependency == null) { throw new IllegalStateException("node does not have a dependency"); @@ -141,6 +159,7 @@ public void setArtifact(Artifact artifact) { this.artifact = dependency.getArtifact(); } + @Override public List getRelocations() { return relocations; } @@ -158,6 +177,7 @@ public void setRelocations(List relocations) { } } + @Override public Collection getAliases() { return aliases; } @@ -175,6 +195,7 @@ public void setAliases(Collection aliases) { } } + @Override public VersionConstraint getVersionConstraint() { return versionConstraint; } @@ -188,6 +209,7 @@ public void setVersionConstraint(VersionConstraint versionConstraint) { this.versionConstraint = versionConstraint; } + @Override public Version getVersion() { return version; } @@ -201,6 +223,7 @@ public void setVersion(Version version) { this.version = version; } + @Override public void setScope(String scope) { if (dependency == null) { throw new IllegalStateException("node does not have a dependency"); @@ -208,6 +231,7 @@ public void setScope(String scope) { dependency = dependency.setScope(scope); } + @Override public void setOptional(Boolean optional) { if (dependency == null) { throw new IllegalStateException("node does not have a dependency"); @@ -215,20 +239,51 @@ public void setOptional(Boolean optional) { dependency = dependency.setOptional(optional); } + @Override public int getManagedBits() { - return managedBits; + byte res = 0; + if (isManagedSubject(DependencyManagement.Subject.VERSION)) { + res |= DependencyNode.MANAGED_VERSION; + } + if (isManagedSubject(DependencyManagement.Subject.SCOPE)) { + res |= DependencyNode.MANAGED_SCOPE; + } + if (isManagedSubject(DependencyManagement.Subject.OPTIONAL)) { + res |= DependencyNode.MANAGED_OPTIONAL; + } + if (isManagedSubject(DependencyManagement.Subject.PROPERTIES)) { + res |= DependencyNode.MANAGED_PROPERTIES; + } + if (isManagedSubject(DependencyManagement.Subject.EXCLUSIONS)) { + res |= DependencyNode.MANAGED_EXCLUSIONS; + } + return res; } - /** - * Sets a bit field indicating which attributes of this node were subject to dependency management. - * - * @param managedBits The bit field indicating the managed attributes or {@code 0} if dependency management wasn't - * applied. - */ + @Deprecated public void setManagedBits(int managedBits) { - this.managedBits = (byte) (managedBits & 0x1F); + throw new IllegalArgumentException("bits are not supported"); + } + + public void setManagedSubjects(Map managedSubjects) { + if (managedSubjects == null) { + this.managedSubjects = Collections.emptyMap(); + } else { + this.managedSubjects = managedSubjects; + } + } + + @Override + public boolean isManagedSubject(DependencyManagement.Subject subject) { + return managedSubjects.containsKey(subject); } + @Override + public boolean isManagedSubjectEnforced(DependencyManagement.Subject subject) { + return managedSubjects.getOrDefault(subject, false); + } + + @Override public List getRepositories() { return repositories; } @@ -246,18 +301,22 @@ public void setRepositories(List repositories) { } } + @Override public String getRequestContext() { return context; } + @Override public void setRequestContext(String context) { this.context = (context != null) ? context.intern() : ""; } + @Override public Map getData() { return data; } + @Override public void setData(Map data) { if (data == null) { this.data = Collections.emptyMap(); @@ -266,6 +325,7 @@ public void setData(Map data) { } } + @Override public void setData(Object key, Object value) { requireNonNull(key, "key cannot be null"); @@ -285,6 +345,7 @@ public void setData(Object key, Object value) { } } + @Override public boolean accept(DependencyVisitor visitor) { if (Thread.currentThread().isInterrupted()) { throw new RuntimeException(new InterruptedException("Thread interrupted")); diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java index 7471639852..d4497cd200 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java @@ -23,6 +23,7 @@ import java.util.Map; import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.collection.DependencyManagement; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.version.Version; import org.eclipse.aether.version.VersionConstraint; @@ -38,40 +39,49 @@ * @noextend This interface is not intended to be extended by clients. */ public interface DependencyNode { - /** * A bit flag indicating the dependency version was subject to dependency management * * @see #getManagedBits() + * @deprecated Use {@link #isManagedSubject(DependencyManagement.Subject)} and {@link #isManagedSubjectEnforced(DependencyManagement.Subject)} instead. */ + @Deprecated int MANAGED_VERSION = 0x01; /** * A bit flag indicating the dependency scope was subject to dependency management * * @see #getManagedBits() + * @deprecated Use {@link #isManagedSubject(DependencyManagement.Subject)} and {@link #isManagedSubjectEnforced(DependencyManagement.Subject)} instead. */ + @Deprecated int MANAGED_SCOPE = 0x02; /** * A bit flag indicating the optional flag was subject to dependency management * * @see #getManagedBits() + * @deprecated Use {@link #isManagedSubject(DependencyManagement.Subject)} and {@link #isManagedSubjectEnforced(DependencyManagement.Subject)} instead. */ + @Deprecated int MANAGED_OPTIONAL = 0x04; /** * A bit flag indicating the artifact properties were subject to dependency management * * @see #getManagedBits() + * @deprecated Use {@link #isManagedSubject(DependencyManagement.Subject)} and {@link #isManagedSubjectEnforced(DependencyManagement.Subject)} instead. */ + @Deprecated int MANAGED_PROPERTIES = 0x08; /** * A bit flag indicating the exclusions were subject to dependency management * * @see #getManagedBits() + * @deprecated Use {@link #isManagedSubject(DependencyManagement.Subject)} and {@link #isManagedSubjectEnforced(DependencyManagement.Subject)} instead. */ + @Deprecated int MANAGED_EXCLUSIONS = 0x10; /** @@ -170,9 +180,27 @@ public interface DependencyNode { * @return A bit field containing any of the bits {@link #MANAGED_VERSION}, {@link #MANAGED_SCOPE}, * {@link #MANAGED_OPTIONAL}, {@link #MANAGED_PROPERTIES} and {@link #MANAGED_EXCLUSIONS} if the * corresponding attribute was set via dependency management. + * @deprecated Use {@link #isManagedSubject(DependencyManagement.Subject)} and {@link #isManagedSubjectEnforced(DependencyManagement.Subject)} instead. */ + @Deprecated int getManagedBits(); + /** + * Returns {@code true} if given subject is managed. + * + * @see org.eclipse.aether.collection.DependencyManagement.Subject + * @since 2.0.17 + */ + boolean isManagedSubject(DependencyManagement.Subject subject); + + /** + * Returns {@code true} if given subject is managed with enforcing modality on this node. + * + * @see org.eclipse.aether.collection.DependencyManagement.Subject + * @since 2.0.17 + */ + boolean isManagedSubjectEnforced(DependencyManagement.Subject subject); + /** * Gets the remote repositories from which this node's artifact shall be resolved. * diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java index 8e715efdd1..8cf4fd6025 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DependencyCollectorDelegate.java @@ -35,6 +35,7 @@ import org.eclipse.aether.collection.CollectResult; import org.eclipse.aether.collection.DependencyCollectionException; import org.eclipse.aether.collection.DependencyGraphTransformer; +import org.eclipse.aether.collection.DependencyManager; import org.eclipse.aether.collection.DependencyTraverser; import org.eclipse.aether.collection.VersionFilter; import org.eclipse.aether.graph.DefaultDependencyNode; @@ -344,8 +345,16 @@ protected static String getId(Artifact a) { return a.getGroupId() + ':' + a.getArtifactId() + ':' + a.getClassifier() + ':' + a.getExtension(); } + protected PremanagedDependency createPremanagedDependency( + DependencyManager depManager, + Dependency dependency, + boolean disableVersionManagement, + boolean premanagedState) { + return PremanagedDependency.create(depManager, dependency, disableVersionManagement, premanagedState); + } + @SuppressWarnings("checkstyle:parameternumber") - protected static DefaultDependencyNode createDependencyNode( + protected DefaultDependencyNode createDependencyNode( List relocations, PremanagedDependency preManaged, VersionRangeResult rangeResult, @@ -365,7 +374,7 @@ protected static DefaultDependencyNode createDependencyNode( return child; } - protected static DefaultDependencyNode createDependencyNode( + protected DefaultDependencyNode createDependencyNodeCycle( List relocations, PremanagedDependency preManaged, VersionRangeResult rangeResult, diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/PremanagedDependency.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/PremanagedDependency.java index c0a6ef3bb6..28be7bb226 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/PremanagedDependency.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/PremanagedDependency.java @@ -29,7 +29,6 @@ import org.eclipse.aether.collection.DependencyManager; import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; -import org.eclipse.aether.graph.DependencyNode; import org.eclipse.aether.graph.Exclusion; import org.eclipse.aether.util.graph.manager.DependencyManagerUtils; @@ -38,27 +37,30 @@ */ public class PremanagedDependency { - final String premanagedVersion; + private final String premanagedVersion; - final String premanagedScope; + private final String premanagedScope; - final Boolean premanagedOptional; + private final Boolean premanagedOptional; /** * @since 1.1.0 */ - final Collection premanagedExclusions; + private final Collection premanagedExclusions; /** * @since 1.1.0 */ - final Map premanagedProperties; + private final Map premanagedProperties; - final int managedBits; + /** + * @since 2.0.17 + */ + private final Map managedSubjects; - final Dependency managedDependency; + private final Dependency managedDependency; - final boolean premanagedState; + private final boolean premanagedState; @SuppressWarnings("checkstyle:parameternumber") PremanagedDependency( @@ -67,7 +69,7 @@ public class PremanagedDependency { Boolean premanagedOptional, Collection premanagedExclusions, Map premanagedProperties, - int managedBits, + Map managedSubjects, Dependency managedDependency, boolean premanagedState) { this.premanagedVersion = premanagedVersion; @@ -80,7 +82,7 @@ public class PremanagedDependency { this.premanagedProperties = premanagedProperties != null ? Collections.unmodifiableMap(new HashMap<>(premanagedProperties)) : null; - this.managedBits = managedBits; + this.managedSubjects = managedSubjects; this.managedDependency = managedDependency; this.premanagedState = premanagedState; } @@ -92,7 +94,7 @@ public static PremanagedDependency create( boolean premanagedState) { DependencyManagement depMngt = depManager != null ? depManager.manageDependency(dependency) : null; - int managedBits = 0; + Map managedSubjects = new HashMap<>(); String premanagedVersion = null; String premanagedScope = null; Boolean premanagedOptional = null; @@ -104,37 +106,48 @@ public static PremanagedDependency create( Artifact artifact = dependency.getArtifact(); premanagedVersion = artifact.getVersion(); dependency = dependency.setArtifact(artifact.setVersion(depMngt.getVersion())); - managedBits |= DependencyNode.MANAGED_VERSION; + managedSubjects.put( + DependencyManagement.Subject.VERSION, + depMngt.isManagedSubjectEnforced(DependencyManagement.Subject.VERSION)); } if (depMngt.getProperties() != null) { Artifact artifact = dependency.getArtifact(); premanagedProperties = artifact.getProperties(); dependency = dependency.setArtifact(artifact.setProperties(depMngt.getProperties())); - managedBits |= DependencyNode.MANAGED_PROPERTIES; + managedSubjects.put( + DependencyManagement.Subject.PROPERTIES, + depMngt.isManagedSubjectEnforced(DependencyManagement.Subject.PROPERTIES)); } if (depMngt.getScope() != null) { premanagedScope = dependency.getScope(); dependency = dependency.setScope(depMngt.getScope()); - managedBits |= DependencyNode.MANAGED_SCOPE; + managedSubjects.put( + DependencyManagement.Subject.SCOPE, + depMngt.isManagedSubjectEnforced(DependencyManagement.Subject.SCOPE)); } if (depMngt.getOptional() != null) { premanagedOptional = dependency.isOptional(); dependency = dependency.setOptional(depMngt.getOptional()); - managedBits |= DependencyNode.MANAGED_OPTIONAL; + managedSubjects.put( + DependencyManagement.Subject.OPTIONAL, + depMngt.isManagedSubjectEnforced(DependencyManagement.Subject.OPTIONAL)); } if (depMngt.getExclusions() != null) { premanagedExclusions = dependency.getExclusions(); dependency = dependency.setExclusions(depMngt.getExclusions()); - managedBits |= DependencyNode.MANAGED_EXCLUSIONS; + managedSubjects.put( + DependencyManagement.Subject.EXCLUSIONS, + depMngt.isManagedSubjectEnforced(DependencyManagement.Subject.EXCLUSIONS)); } } + return new PremanagedDependency( premanagedVersion, premanagedScope, premanagedOptional, premanagedExclusions, premanagedProperties, - managedBits, + managedSubjects, dependency, premanagedState); } @@ -144,7 +157,7 @@ public Dependency getManagedDependency() { } public void applyTo(DefaultDependencyNode child) { - child.setManagedBits(managedBits); + child.setManagedSubjects(managedSubjects); if (premanagedState) { child.setData(DependencyManagerUtils.NODE_DATA_PREMANAGED_VERSION, premanagedVersion); child.setData(DependencyManagerUtils.NODE_DATA_PREMANAGED_SCOPE, premanagedScope); diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java index 759e227e79..c6c77d3602 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java @@ -209,7 +209,7 @@ protected void doCollectDependencies( managedDependencies, parents, dependency, - PremanagedDependency.create(rootDepManager, dependency, false, args.premanagedState)); + createPremanagedDependency(rootDepManager, dependency, false, args.premanagedState)); if (!filter(processingContext)) { processingContext.withDependency(processingContext.premanagedDependency.getManagedDependency()); resolveArtifactDescriptorAsync(args, processingContext, results); @@ -274,7 +274,7 @@ private void processDependency( results.addCycle(context.parents, cycleEntry, d); DependencyNode cycleNode = context.parents.get(cycleEntry); if (cycleNode.getDependency() != null) { - DefaultDependencyNode child = createDependencyNode( + DefaultDependencyNode child = createDependencyNodeCycle( relocations, preManaged, rangeResult, version, d, descriptorResult, cycleNode); context.getParent().getChildren().add(child); continue; @@ -288,7 +288,7 @@ private void processDependency( .getArtifactId() .equals(d.getArtifact().getArtifactId()); - PremanagedDependency premanagedDependency = PremanagedDependency.create( + PremanagedDependency premanagedDependency = createPremanagedDependency( context.depManager, d, disableVersionManagementSubsequently, args.premanagedState); DependencyProcessingContext relocatedContext = new DependencyProcessingContext( context.depSelector, @@ -406,7 +406,7 @@ private void doRecurse( for (Dependency dependency : descriptorResult.getDependencies()) { RequestTrace childTrace = collectStepTrace( parentContext.trace, args.request.getRequestContext(), parents, dependency); - PremanagedDependency premanagedDependency = PremanagedDependency.create( + PremanagedDependency premanagedDependency = createPremanagedDependency( childManager, dependency, disableVersionManagement, args.premanagedState); DependencyProcessingContext processingContext = new DependencyProcessingContext( childSelector, diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollector.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollector.java index 034d6691e2..44a9b223f0 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollector.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/df/DfDependencyCollector.java @@ -191,7 +191,7 @@ private void processDependency( RequestTrace trace = collectStepTrace(parent, args.request.getRequestContext(), args.nodes.nodes, dependency); PremanagedDependency preManaged = - PremanagedDependency.create(depManager, dependency, disableVersionManagement, args.premanagedState); + createPremanagedDependency(depManager, dependency, disableVersionManagement, args.premanagedState); dependency = preManaged.getManagedDependency(); boolean noDescriptor = isLackingDescriptor(args.session, dependency.getArtifact()); @@ -231,7 +231,7 @@ private void processDependency( results.addCycle(args.nodes.nodes, cycleEntry, d); DependencyNode cycleNode = args.nodes.get(cycleEntry); if (cycleNode.getDependency() != null) { - DefaultDependencyNode child = createDependencyNode( + DefaultDependencyNode child = createDependencyNodeCycle( relocations, preManaged, rangeResult, version, d, descriptorResult, cycleNode); node.getChildren().add(child); continue; diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java index 18da45bb70..9448081046 100644 --- a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java +++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java @@ -36,6 +36,7 @@ import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.DependencyManagement; import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyNode; @@ -291,16 +292,16 @@ private DependencyNode build(DependencyNode parent, LineContext ctx, boolean isR DefaultArtifact artifact = new DefaultArtifact(def.coords, def.properties); Dependency dependency = new Dependency(artifact, def.scope, def.optional); node = new DefaultDependencyNode(dependency); - int managedBits = 0; + Map managedSubjects = new HashMap<>(); if (def.premanagedScope != null) { - managedBits |= DependencyNode.MANAGED_SCOPE; + managedSubjects.put(DependencyManagement.Subject.SCOPE, true); node.setData("premanaged.scope", def.premanagedScope); } if (def.premanagedVersion != null) { - managedBits |= DependencyNode.MANAGED_VERSION; + managedSubjects.put(DependencyManagement.Subject.VERSION, true); node.setData("premanaged.version", def.premanagedVersion); } - node.setManagedBits(managedBits); + node.setManagedSubjects(managedSubjects); if (def.relocations != null) { List relocations = new ArrayList<>(); for (String relocation : def.relocations) { diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java index 2b2eeab358..07ae50a333 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java @@ -86,9 +86,10 @@ *

Managed Bits and Graph Transformations

*

* When a {@link org.eclipse.aether.graph.DependencyNode} becomes "managed" by any property - * provided from this manager, {@link org.eclipse.aether.graph.DependencyNode#getManagedBits()} + * provided from this manager, {@link org.eclipse.aether.graph.DependencyNode#isManagedSubject(DependencyManagement.Subject)} + * and {@link org.eclipse.aether.graph.DependencyNode#isManagedSubjectEnforced(DependencyManagement.Subject)} * will carry this information for the given property. Later graph transformations will abstain - * from modifying these properties of marked nodes (assuming the node already has the property + * from modifying these properties of marked enforced nodes (assuming the node already has the property * set to what it should have). Sometimes this is unwanted, especially for properties that need * to be inherited in the graph (values derived from parent-child context of the actual node, * like "scope" or "optional"). @@ -206,14 +207,14 @@ private boolean containsManagedVersion(Key key, MMap managedVersion return managedVersions != null && managedVersions.containsKey(key); } - private String getManagedVersion(Key key) { + private AbstractDependencyManager getManagedVersion(Key key) { for (AbstractDependencyManager ancestor : path) { if (ancestor.managedVersions != null && ancestor.managedVersions.containsKey(key)) { - return ancestor.managedVersions.get(key); + return ancestor; } } if (depth == 1 && managedVersions != null && managedVersions.containsKey(key)) { - return managedVersions.get(key); + return this; } return null; } @@ -227,14 +228,14 @@ private boolean containsManagedScope(Key key, MMap managedScopes) { return managedScopes != null && managedScopes.containsKey(key); } - private String getManagedScope(Key key) { + private AbstractDependencyManager getManagedScope(Key key) { for (AbstractDependencyManager ancestor : path) { if (ancestor.managedScopes != null && ancestor.managedScopes.containsKey(key)) { - return ancestor.managedScopes.get(key); + return ancestor; } } if (depth == 1 && managedScopes != null && managedScopes.containsKey(key)) { - return managedScopes.get(key); + return this; } return null; } @@ -248,14 +249,14 @@ private boolean containsManagedOptional(Key key, MMap managedOptio return managedOptionals != null && managedOptionals.containsKey(key); } - private Boolean getManagedOptional(Key key) { + private AbstractDependencyManager getManagedOptional(Key key) { for (AbstractDependencyManager ancestor : path) { if (ancestor.managedOptionals != null && ancestor.managedOptionals.containsKey(key)) { - return ancestor.managedOptionals.get(key); + return ancestor; } } if (depth == 1 && managedOptionals != null && managedOptionals.containsKey(key)) { - return managedOptionals.get(key); + return this; } return null; } @@ -276,14 +277,14 @@ private boolean containsManagedLocalPath(Key key, MMap managedLocal * @param key the dependency key * @return the managed local path, or null if not managed */ - private String getManagedLocalPath(Key key) { + private AbstractDependencyManager getManagedLocalPath(Key key) { for (AbstractDependencyManager ancestor : path) { if (ancestor.managedLocalPaths != null && ancestor.managedLocalPaths.containsKey(key)) { - return ancestor.managedLocalPaths.get(key); + return ancestor; } } if (managedLocalPaths != null && managedLocalPaths.containsKey(key)) { - return managedLocalPaths.get(key); + return this; } return null; } @@ -334,22 +335,20 @@ public DependencyManager deriveChildManager(DependencyCollectionContext context) managedVersions.put(key, version); } - if (isInheritedDerived()) { - String scope = managedDependency.getScope(); - if (!scope.isEmpty() && !containsManagedScope(key, managedScopes)) { - if (managedScopes == null) { - managedScopes = MMap.emptyNotDone(); - } - managedScopes.put(key, scope); + String scope = managedDependency.getScope(); + if (!scope.isEmpty() && !containsManagedScope(key, managedScopes)) { + if (managedScopes == null) { + managedScopes = MMap.emptyNotDone(); } + managedScopes.put(key, scope); + } - Boolean optional = managedDependency.getOptional(); - if (optional != null && !containsManagedOptional(key, managedOptionals)) { - if (managedOptionals == null) { - managedOptionals = MMap.emptyNotDone(); - } - managedOptionals.put(key, optional); + Boolean optional = managedDependency.getOptional(); + if (optional != null && !containsManagedOptional(key, managedOptionals)) { + if (managedOptionals == null) { + managedOptionals = MMap.emptyNotDone(); } + managedOptionals.put(key, optional); } String localPath = systemDependencyScope == null @@ -394,58 +393,59 @@ public DependencyManagement manageDependency(Dependency dependency) { Key key = new Key(dependency.getArtifact()); if (isApplied()) { - String version = getManagedVersion(key); + AbstractDependencyManager versionOwner = getManagedVersion(key); // is managed locally by model builder // apply only rules coming from "higher" levels - if (version != null) { + if (versionOwner != null) { management = new DependencyManagement(); - management.setVersion(version); + management.setVersion(versionOwner.managedVersions.get(key), versionOwner.path.isEmpty()); } - String scope = getManagedScope(key); + AbstractDependencyManager scopeOwner = getManagedScope(key); // is managed locally by model builder // apply only rules coming from "higher" levels - if (scope != null) { + if (scopeOwner != null) { if (management == null) { management = new DependencyManagement(); } - management.setScope(scope); + String managedScope = scopeOwner.managedScopes.get(key); + management.setScope(managedScope, scopeOwner.path.isEmpty()); if (systemDependencyScope != null - && !systemDependencyScope.is(scope) + && !systemDependencyScope.is(managedScope) && systemDependencyScope.getSystemPath(dependency.getArtifact()) != null) { HashMap properties = new HashMap<>(dependency.getArtifact().getProperties()); systemDependencyScope.setSystemPath(properties, null); - management.setProperties(properties); + management.setProperties(properties, false); } } // system scope paths always applied to have them aligned // (same artifact == same path) in whole graph if (systemDependencyScope != null - && (scope != null && systemDependencyScope.is(scope) - || (scope == null && systemDependencyScope.is(dependency.getScope())))) { - String localPath = getManagedLocalPath(key); - if (localPath != null) { + && (scopeOwner != null && systemDependencyScope.is(scopeOwner.managedScopes.get(key)) + || (scopeOwner == null && systemDependencyScope.is(dependency.getScope())))) { + AbstractDependencyManager localPathOwner = getManagedLocalPath(key); + if (localPathOwner != null) { if (management == null) { management = new DependencyManagement(); } HashMap properties = new HashMap<>(dependency.getArtifact().getProperties()); - systemDependencyScope.setSystemPath(properties, localPath); - management.setProperties(properties); + systemDependencyScope.setSystemPath(properties, localPathOwner.managedLocalPaths.get(key)); + management.setProperties(properties, false); } } // optional is not managed by model builder // apply only rules coming from "higher" levels - Boolean optional = getManagedOptional(key); - if (optional != null) { + AbstractDependencyManager optionalOwner = getManagedOptional(key); + if (optionalOwner != null) { if (management == null) { management = new DependencyManagement(); } - management.setOptional(optional); + management.setOptional(optionalOwner.managedOptionals.get(key), optionalOwner.path.isEmpty()); } } @@ -461,7 +461,7 @@ public DependencyManagement manageDependency(Dependency dependency) { } Collection result = new LinkedHashSet<>(dependency.getExclusions()); result.addAll(exclusions); - management.setExclusions(result); + management.setExclusions(result, false); } return management; @@ -474,34 +474,6 @@ protected boolean isDerived() { return depth < deriveUntil; } - /** - * Manages dependency properties including "version", "scope", "optional", "local path", and "exclusions". - *

- * Property management behavior: - *

    - *
  • Version: Follows {@link #isDerived()} pattern. Management is applied only at higher - * levels to avoid interference with the model builder.
  • - *
  • Scope: Derived from root only due to inheritance in dependency graphs. Special handling - * for "system" scope to align artifact paths.
  • - *
  • Optional: Derived from root only due to inheritance in dependency graphs.
  • - *
  • Local path: Managed only when scope is or was set to "system" to ensure consistent - * artifact path alignment.
  • - *
  • Exclusions: Accumulated additively from root to current level throughout the entire - * dependency path.
  • - *
- *

- * Inheritance handling: Since "scope" and "optional" properties inherit through dependency - * graphs (beyond model builder scope), they are derived only from the root node. The actual manager - * implementation determines specific handling behavior. - *

- * Default behavior: Defaults to {@link #isDerived()} to maintain compatibility with - * "classic" behavior (equivalent to {@code deriveUntil=2}). For custom transitivity management, override - * this method or ensure inherited managed properties are handled during graph transformation. - */ - protected boolean isInheritedDerived() { - return isDerived(); - } - /** * Returns {@code true} if current dependency should be managed according to so far collected/derived rules. */ diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/DependencyManagerUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/DependencyManagerUtils.java index e83fd06698..685d005f44 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/DependencyManagerUtils.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/DependencyManagerUtils.java @@ -23,6 +23,7 @@ import org.eclipse.aether.ConfigurationProperties; import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.collection.DependencyManagement; import org.eclipse.aether.graph.DependencyNode; import org.eclipse.aether.graph.Exclusion; @@ -87,7 +88,7 @@ public final class DependencyManagerUtils { * or if {@link #CONFIG_PROP_VERBOSE} was not enabled */ public static String getPremanagedVersion(DependencyNode node) { - if ((node.getManagedBits() & DependencyNode.MANAGED_VERSION) == 0) { + if (!node.isManagedSubject(DependencyManagement.Subject.VERSION)) { return null; } return cast(node.getData().get(NODE_DATA_PREMANAGED_VERSION), String.class); @@ -101,7 +102,7 @@ public static String getPremanagedVersion(DependencyNode node) { * if {@link #CONFIG_PROP_VERBOSE} was not enabled */ public static String getPremanagedScope(DependencyNode node) { - if ((node.getManagedBits() & DependencyNode.MANAGED_SCOPE) == 0) { + if (!node.isManagedSubject(DependencyManagement.Subject.SCOPE)) { return null; } return cast(node.getData().get(NODE_DATA_PREMANAGED_SCOPE), String.class); @@ -115,7 +116,7 @@ public static String getPremanagedScope(DependencyNode node) { * {@link #CONFIG_PROP_VERBOSE} was not enabled */ public static Boolean getPremanagedOptional(DependencyNode node) { - if ((node.getManagedBits() & DependencyNode.MANAGED_OPTIONAL) == 0) { + if (!node.isManagedSubject(DependencyManagement.Subject.OPTIONAL)) { return null; } return cast(node.getData().get(NODE_DATA_PREMANAGED_OPTIONAL), Boolean.class); @@ -131,7 +132,7 @@ public static Boolean getPremanagedOptional(DependencyNode node) { */ @SuppressWarnings("unchecked") public static Collection getPremanagedExclusions(DependencyNode node) { - if ((node.getManagedBits() & DependencyNode.MANAGED_EXCLUSIONS) == 0) { + if (!node.isManagedSubject(DependencyManagement.Subject.EXCLUSIONS)) { return null; } return cast(node.getData().get(NODE_DATA_PREMANAGED_EXCLUSIONS), Collection.class); @@ -147,7 +148,7 @@ public static Collection getPremanagedExclusions(DependencyNode node) */ @SuppressWarnings("unchecked") public static Map getPremanagedProperties(DependencyNode node) { - if ((node.getManagedBits() & DependencyNode.MANAGED_PROPERTIES) == 0) { + if (!node.isManagedSubject(DependencyManagement.Subject.PROPERTIES)) { return null; } return cast(node.getData().get(NODE_DATA_PREMANAGED_PROPERTIES), Map.class); diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/TransitiveDependencyManager.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/TransitiveDependencyManager.java index b46211be0a..460a52c014 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/TransitiveDependencyManager.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/TransitiveDependencyManager.java @@ -145,30 +145,4 @@ protected DependencyManager newInstance( managedExclusions, systemDependencyScope); } - - /** - * Controls inheritance-based property derivation for scope and optional properties. - *

- * Why scope and optional are special: In dependency graphs, these two properties - * are subject to inheritance during graph transformation (which is outside ModelBuilder's scope). - * Therefore, scope and optional are derived only from the root to prevent interference with - * inheritance logic. - *

- *

- * The inheritance problem: If we managed scope/optional from sources below the root, - * we would mark nodes as "managed" in the dependency graph. The "managed" flag means "do not touch it, - * it is as it should be", which would prevent proper inheritance application during later graph - * transformation, causing nodes to end up with incorrect scope or optional states. - *

- *

- * Special case: The "system" scope has special handling due to its unique path requirements. - *

- * - * @return true only at depth 0 (root level) to ensure inheritance-based properties are only - * derived from the root, false otherwise - */ - @Override - protected boolean isInheritedDerived() { - return depth == 0; - } } diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ClassicConflictResolver.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ClassicConflictResolver.java index 4ecd53b2eb..4825e433e5 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ClassicConflictResolver.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ClassicConflictResolver.java @@ -33,6 +33,7 @@ import org.eclipse.aether.RepositoryException; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.collection.DependencyGraphTransformationContext; +import org.eclipse.aether.collection.DependencyManagement; import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyNode; @@ -675,7 +676,7 @@ private DependencyNode parent() { } private String deriveScope(DependencyNode node, String conflictId) throws RepositoryException { - if ((node.getManagedBits() & DependencyNode.MANAGED_SCOPE) != 0 + if (node.isManagedSubjectEnforced(DependencyManagement.Subject.SCOPE) || (conflictId != null && resolvedIds.containsKey(conflictId))) { return scope(node.getDependency()); } @@ -702,7 +703,7 @@ private boolean deriveOptional(DependencyNode node, String conflictId) { Dependency dep = node.getDependency(); boolean optional = (dep != null) && dep.isOptional(); if (optional - || (node.getManagedBits() & DependencyNode.MANAGED_OPTIONAL) != 0 + || node.isManagedSubjectEnforced(DependencyManagement.Subject.OPTIONAL) || (conflictId != null && resolvedIds.containsKey(conflictId))) { return optional; } diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java index 985106a0b0..519e608739 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java @@ -32,6 +32,7 @@ import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.collection.DependencyGraphTransformationContext; +import org.eclipse.aether.collection.DependencyManagement; import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyNode; @@ -423,12 +424,12 @@ private void pull(int levels) { private void derive(int levels, boolean winner) throws RepositoryException { if (!winner) { if (this.parent != null) { - if ((dn.getManagedBits() & DependencyNode.MANAGED_SCOPE) == 0) { + if (!dn.isManagedSubjectEnforced(DependencyManagement.Subject.SCOPE)) { ScopeContext context = new ScopeContext(this.parent.scope, this.scope); state.scopeDeriver.deriveScope(context); this.scope = context.derivedScope; } - if ((dn.getManagedBits() & DependencyNode.MANAGED_OPTIONAL) == 0) { + if (!dn.isManagedSubjectEnforced(DependencyManagement.Subject.OPTIONAL)) { if (!this.optional && this.parent.optional) { this.optional = true; } From 2ab1352f3997a256c0f9ebc18a2bacb482b931b5 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 27 Mar 2026 17:47:44 +0100 Subject: [PATCH 2/7] Use EnumMap for less resource hunger --- .../collection/DependencyManagement.java | 10 +++---- .../aether/graph/DefaultDependencyNode.java | 30 +++++++++++++++---- .../eclipse/aether/graph/DependencyNode.java | 21 +++++++++++-- .../impl/collect/PremanagedDependency.java | 8 +++-- .../test/util/DependencyGraphParser.java | 4 ++- 5 files changed, 57 insertions(+), 16 deletions(-) diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java index 19c7bfbc05..177220dce6 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/DependencyManagement.java @@ -19,7 +19,7 @@ package org.eclipse.aether.collection; import java.util.Collection; -import java.util.HashMap; +import java.util.EnumMap; import java.util.Map; import org.eclipse.aether.graph.Exclusion; @@ -43,15 +43,15 @@ public enum Subject { PROPERTIES } - private final Map managedValues; - private final Map managedEnforced; + private final EnumMap managedValues; + private final EnumMap managedEnforced; /** * Creates an empty management update. */ public DependencyManagement() { - this.managedValues = new HashMap<>(); - this.managedEnforced = new HashMap<>(); + this.managedValues = new EnumMap<>(Subject.class); + this.managedEnforced = new EnumMap<>(Subject.class); } /** diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java index fdfdddf641..9effe1440a 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -37,6 +38,8 @@ * A node within a dependency graph. */ public final class DefaultDependencyNode implements DependencyNode { + private final Map emptyManagedSubjects = + Collections.unmodifiableMap(new EnumMap<>(DependencyManagement.Subject.class)); private List children; @@ -52,7 +55,7 @@ public final class DefaultDependencyNode implements DependencyNode { private Version version; - private Map managedSubjects = Collections.emptyMap(); + private Map managedSubjects = emptyManagedSubjects; private List repositories; @@ -106,7 +109,8 @@ public DefaultDependencyNode(DependencyNode node) { setAliases(node.getAliases()); setRequestContext(node.getRequestContext()); - HashMap managedSubjects = new HashMap<>(); + EnumMap managedSubjects = + new EnumMap<>(DependencyManagement.Subject.class); for (DependencyManagement.Subject subject : DependencyManagement.Subject.values()) { if (node.isManagedSubject(subject)) { managedSubjects.put(subject, node.isManagedSubjectEnforced(subject)); @@ -262,12 +266,28 @@ public int getManagedBits() { @Deprecated public void setManagedBits(int managedBits) { - throw new IllegalArgumentException("bits are not supported"); + EnumMap subjects = new EnumMap<>(DependencyManagement.Subject.class); + if ((managedBits & DependencyNode.MANAGED_VERSION) != 0) { + subjects.put(DependencyManagement.Subject.VERSION, true); + } + if ((managedBits & DependencyNode.MANAGED_SCOPE) != 0) { + subjects.put(DependencyManagement.Subject.SCOPE, true); + } + if ((managedBits & DependencyNode.MANAGED_OPTIONAL) != 0) { + subjects.put(DependencyManagement.Subject.OPTIONAL, true); + } + if ((managedBits & DependencyNode.MANAGED_PROPERTIES) != 0) { + subjects.put(DependencyManagement.Subject.PROPERTIES, true); + } + if ((managedBits & DependencyNode.MANAGED_EXCLUSIONS) != 0) { + subjects.put(DependencyManagement.Subject.EXCLUSIONS, true); + } + setManagedSubjects(subjects.isEmpty() ? null : subjects); } public void setManagedSubjects(Map managedSubjects) { - if (managedSubjects == null) { - this.managedSubjects = Collections.emptyMap(); + if (managedSubjects == null || managedSubjects.isEmpty()) { + this.managedSubjects = emptyManagedSubjects; } else { this.managedSubjects = managedSubjects; } diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java index d4497cd200..6e1dda6a50 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java @@ -191,7 +191,22 @@ public interface DependencyNode { * @see org.eclipse.aether.collection.DependencyManagement.Subject * @since 2.0.17 */ - boolean isManagedSubject(DependencyManagement.Subject subject); + default boolean isManagedSubject(DependencyManagement.Subject subject) { + switch (subject) { + case VERSION: + return (getManagedBits() & MANAGED_VERSION) != 0; + case SCOPE: + return (getManagedBits() & MANAGED_SCOPE) != 0; + case OPTIONAL: + return (getManagedBits() & MANAGED_OPTIONAL) != 0; + case PROPERTIES: + return (getManagedBits() & MANAGED_PROPERTIES) != 0; + case EXCLUSIONS: + return (getManagedBits() & MANAGED_EXCLUSIONS) != 0; + default: + throw new IllegalArgumentException("Unknown subject: " + subject.name()); + } + } /** * Returns {@code true} if given subject is managed with enforcing modality on this node. @@ -199,7 +214,9 @@ public interface DependencyNode { * @see org.eclipse.aether.collection.DependencyManagement.Subject * @since 2.0.17 */ - boolean isManagedSubjectEnforced(DependencyManagement.Subject subject); + default boolean isManagedSubjectEnforced(DependencyManagement.Subject subject) { + return isManagedSubject(subject); + } /** * Gets the remote repositories from which this node's artifact shall be resolved. diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/PremanagedDependency.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/PremanagedDependency.java index 28be7bb226..d5dd73eff9 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/PremanagedDependency.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/PremanagedDependency.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.Map; @@ -56,7 +57,7 @@ public class PremanagedDependency { /** * @since 2.0.17 */ - private final Map managedSubjects; + private final EnumMap managedSubjects; private final Dependency managedDependency; @@ -69,7 +70,7 @@ public class PremanagedDependency { Boolean premanagedOptional, Collection premanagedExclusions, Map premanagedProperties, - Map managedSubjects, + EnumMap managedSubjects, Dependency managedDependency, boolean premanagedState) { this.premanagedVersion = premanagedVersion; @@ -94,7 +95,8 @@ public static PremanagedDependency create( boolean premanagedState) { DependencyManagement depMngt = depManager != null ? depManager.manageDependency(dependency) : null; - Map managedSubjects = new HashMap<>(); + EnumMap managedSubjects = + new EnumMap<>(DependencyManagement.Subject.class); String premanagedVersion = null; String premanagedScope = null; Boolean premanagedOptional = null; diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java index 9448081046..98d66fbfdb 100644 --- a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java +++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/DependencyGraphParser.java @@ -28,6 +28,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; @@ -292,7 +293,8 @@ private DependencyNode build(DependencyNode parent, LineContext ctx, boolean isR DefaultArtifact artifact = new DefaultArtifact(def.coords, def.properties); Dependency dependency = new Dependency(artifact, def.scope, def.optional); node = new DefaultDependencyNode(dependency); - Map managedSubjects = new HashMap<>(); + EnumMap managedSubjects = + new EnumMap<>(DependencyManagement.Subject.class); if (def.premanagedScope != null) { managedSubjects.put(DependencyManagement.Subject.SCOPE, true); node.setData("premanaged.scope", def.premanagedScope); From afd841004f9f538f090d58bdaf0d93ea9b4097b2 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 27 Mar 2026 18:16:30 +0100 Subject: [PATCH 3/7] Align semantic --- .../java/org/eclipse/aether/graph/DefaultDependencyNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java index 9effe1440a..7c57521c98 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java @@ -300,7 +300,7 @@ public boolean isManagedSubject(DependencyManagement.Subject subject) { @Override public boolean isManagedSubjectEnforced(DependencyManagement.Subject subject) { - return managedSubjects.getOrDefault(subject, false); + return isManagedSubject(subject) && managedSubjects.getOrDefault(subject, false); } @Override From a14abd601ec55c77ba400fe90f29d3a08b3464a0 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 27 Mar 2026 18:29:10 +0100 Subject: [PATCH 4/7] No need for "empty", use `null` --- .../aether/graph/DefaultDependencyNode.java | 43 +++++++++---------- .../eclipse/aether/graph/DependencyNode.java | 2 + 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java index 7c57521c98..2d7cce810a 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DefaultDependencyNode.java @@ -38,9 +38,6 @@ * A node within a dependency graph. */ public final class DefaultDependencyNode implements DependencyNode { - private final Map emptyManagedSubjects = - Collections.unmodifiableMap(new EnumMap<>(DependencyManagement.Subject.class)); - private List children; private Dependency dependency; @@ -55,7 +52,7 @@ public final class DefaultDependencyNode implements DependencyNode { private Version version; - private Map managedSubjects = emptyManagedSubjects; + private Map managedSubjects; private List repositories; @@ -70,13 +67,14 @@ public final class DefaultDependencyNode implements DependencyNode { */ public DefaultDependencyNode(Dependency dependency) { this.dependency = dependency; - artifact = (dependency != null) ? dependency.getArtifact() : null; - children = new ArrayList<>(0); - aliases = Collections.emptyList(); - relocations = Collections.emptyList(); - repositories = Collections.emptyList(); - context = ""; - data = Collections.emptyMap(); + this.artifact = (dependency != null) ? dependency.getArtifact() : null; + this.children = new ArrayList<>(0); + this.aliases = Collections.emptyList(); + this.relocations = Collections.emptyList(); + this.managedSubjects = null; + this.repositories = Collections.emptyList(); + this.context = ""; + this.data = Collections.emptyMap(); } /** @@ -88,12 +86,13 @@ public DefaultDependencyNode(Dependency dependency) { */ public DefaultDependencyNode(Artifact artifact) { this.artifact = artifact; - children = new ArrayList<>(0); - aliases = Collections.emptyList(); - relocations = Collections.emptyList(); - repositories = Collections.emptyList(); - context = ""; - data = Collections.emptyMap(); + this.children = new ArrayList<>(0); + this.aliases = Collections.emptyList(); + this.relocations = Collections.emptyList(); + this.managedSubjects = null; + this.repositories = Collections.emptyList(); + this.context = ""; + this.data = Collections.emptyMap(); } /** @@ -103,9 +102,9 @@ public DefaultDependencyNode(Artifact artifact) { * @param node The node to copy, must not be {@code null}. */ public DefaultDependencyNode(DependencyNode node) { - dependency = node.getDependency(); - artifact = node.getArtifact(); - children = new ArrayList<>(0); + this.dependency = node.getDependency(); + this.artifact = node.getArtifact(); + this.children = new ArrayList<>(0); setAliases(node.getAliases()); setRequestContext(node.getRequestContext()); @@ -287,7 +286,7 @@ public void setManagedBits(int managedBits) { public void setManagedSubjects(Map managedSubjects) { if (managedSubjects == null || managedSubjects.isEmpty()) { - this.managedSubjects = emptyManagedSubjects; + this.managedSubjects = null; } else { this.managedSubjects = managedSubjects; } @@ -295,7 +294,7 @@ public void setManagedSubjects(Map manage @Override public boolean isManagedSubject(DependencyManagement.Subject subject) { - return managedSubjects.containsKey(subject); + return managedSubjects != null && managedSubjects.containsKey(subject); } @Override diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java index 6e1dda6a50..fb3c46d1b8 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/graph/DependencyNode.java @@ -188,6 +188,7 @@ public interface DependencyNode { /** * Returns {@code true} if given subject is managed. * + * @param subject the {@link org.eclipse.aether.collection.DependencyManagement.Subject}, must not be {@code null}. * @see org.eclipse.aether.collection.DependencyManagement.Subject * @since 2.0.17 */ @@ -211,6 +212,7 @@ default boolean isManagedSubject(DependencyManagement.Subject subject) { /** * Returns {@code true} if given subject is managed with enforcing modality on this node. * + * @param subject the {@link org.eclipse.aether.collection.DependencyManagement.Subject}, must not be {@code null}. * @see org.eclipse.aether.collection.DependencyManagement.Subject * @since 2.0.17 */ From cd189d8f4b81c8d5a1e9e44c17947dcb10058cae Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 27 Mar 2026 18:37:37 +0100 Subject: [PATCH 5/7] Make exclusions applied as before, enforcing. --- .../aether/util/graph/manager/AbstractDependencyManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java index 07ae50a333..905d448512 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java @@ -461,7 +461,7 @@ public DependencyManagement manageDependency(Dependency dependency) { } Collection result = new LinkedHashSet<>(dependency.getExclusions()); result.addAll(exclusions); - management.setExclusions(result, false); + management.setExclusions(result, true); } return management; From edfb4b1a6bb7e8c6cfa8f3e739da88720d6e1396 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 27 Mar 2026 19:11:18 +0100 Subject: [PATCH 6/7] Introduce helper method and use it. --- .../graph/manager/AbstractDependencyManager.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java index 905d448512..6f942408c3 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java @@ -207,6 +207,13 @@ private boolean containsManagedVersion(Key key, MMap managedVersion return managedVersions != null && managedVersions.containsKey(key); } + /** + * Root manager is the one not being derived with {@link #deriveChildManager(DependencyCollectionContext)}. + */ + private boolean isRootManager() { + return path.isEmpty(); + } + private AbstractDependencyManager getManagedVersion(Key key) { for (AbstractDependencyManager ancestor : path) { if (ancestor.managedVersions != null && ancestor.managedVersions.containsKey(key)) { @@ -398,7 +405,7 @@ public DependencyManagement manageDependency(Dependency dependency) { // apply only rules coming from "higher" levels if (versionOwner != null) { management = new DependencyManagement(); - management.setVersion(versionOwner.managedVersions.get(key), versionOwner.path.isEmpty()); + management.setVersion(versionOwner.managedVersions.get(key), versionOwner.isRootManager()); } AbstractDependencyManager scopeOwner = getManagedScope(key); @@ -409,7 +416,7 @@ public DependencyManagement manageDependency(Dependency dependency) { management = new DependencyManagement(); } String managedScope = scopeOwner.managedScopes.get(key); - management.setScope(managedScope, scopeOwner.path.isEmpty()); + management.setScope(managedScope, scopeOwner.isRootManager()); if (systemDependencyScope != null && !systemDependencyScope.is(managedScope) @@ -445,7 +452,7 @@ public DependencyManagement manageDependency(Dependency dependency) { if (management == null) { management = new DependencyManagement(); } - management.setOptional(optionalOwner.managedOptionals.get(key), optionalOwner.path.isEmpty()); + management.setOptional(optionalOwner.managedOptionals.get(key), optionalOwner.isRootManager()); } } From 17497d254fd95d16dab07b22f2042d84cbae8940 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 27 Mar 2026 20:37:13 +0100 Subject: [PATCH 7/7] Add tests for advised/enforced management and fix isRootManager() - Fix isRootManager() to use depth <= 1 instead of path.isEmpty(), which only matched depth=0 (factory) but never depth=1 (root POM) where managed entries are actually stored. - Add DependencyManagerTest cases for enforcement from root vs non-root depth, setManagedBits backwards compat, and managedSubjects lifecycle. - Add ConflictResolverTest cases verifying enforced scope/optional are not derived from parent, while advised/unmanaged ones are. Co-Authored-By: Claude Opus 4.6 --- .../manager/AbstractDependencyManager.java | 6 +- .../graph/manager/DependencyManagerTest.java | 175 ++++++++++++++++++ .../transformer/ConflictResolverTest.java | 80 ++++++++ 3 files changed, 259 insertions(+), 2 deletions(-) diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java index 6f942408c3..1c67889b97 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/manager/AbstractDependencyManager.java @@ -208,10 +208,12 @@ private boolean containsManagedVersion(Key key, MMap managedVersion } /** - * Root manager is the one not being derived with {@link #deriveChildManager(DependencyCollectionContext)}. + * Returns {@code true} if this manager represents the root level (factory or root POM level). + * Per the depth model: 0 = factory (seed), 1 = root (project POM), 2+ = descendants. + * Management entries from root level should be enforced, while those from descendants are advised. */ private boolean isRootManager() { - return path.isEmpty(); + return depth <= 1; } private AbstractDependencyManager getManagedVersion(Key key) { diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/manager/DependencyManagerTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/manager/DependencyManagerTest.java index db5c4afe17..0524587bc4 100644 --- a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/manager/DependencyManagerTest.java +++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/manager/DependencyManagerTest.java @@ -20,14 +20,18 @@ import java.util.Arrays; import java.util.Collections; +import java.util.EnumMap; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.collection.DependencyCollectionContext; import org.eclipse.aether.collection.DependencyManagement; +import org.eclipse.aether.collection.DependencyManagement.Subject; import org.eclipse.aether.collection.DependencyManager; +import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.graph.DependencyNode; import org.eclipse.aether.graph.Exclusion; import org.eclipse.aether.internal.test.util.TestUtils; import org.junit.jupiter.api.BeforeEach; @@ -268,4 +272,175 @@ void testDefault() { // DO NOT APPLY ONTO ITSELF assertNull(mngt); } + + /** + * Tests that root-level management produces enforced results for version, scope, and optional. + */ + @Test + void testTransitiveEnforcementFromRoot() { + DependencyManager manager = new TransitiveDependencyManager(null); + + // depth=1: derive from root with managed dependencies + manager = manager.deriveChildManager(newContext( + new Dependency(A2, null, null), new Dependency(B, null, true), new Dependency(C1, "newscope", null))); + + // depth=2: management is applied — check enforcement flags + manager = manager.deriveChildManager(newContext()); + DependencyManagement mngt = manager.manageDependency(new Dependency(A1, null)); + assertNotNull(mngt); + assertEquals(A2.getVersion(), mngt.getVersion()); + // version management from root should be enforced + assertTrue(mngt.isManagedSubject(Subject.VERSION)); + assertTrue(mngt.isManagedSubjectEnforced(Subject.VERSION)); + + mngt = manager.manageDependency(new Dependency(C1, null)); + assertNotNull(mngt); + assertEquals("newscope", mngt.getScope()); + // scope management from root should be enforced + assertTrue(mngt.isManagedSubject(Subject.SCOPE)); + assertTrue(mngt.isManagedSubjectEnforced(Subject.SCOPE)); + + mngt = manager.manageDependency(new Dependency(B1, null)); + assertNotNull(mngt); + assertEquals(Boolean.TRUE, mngt.getOptional()); + // optional management from root should be enforced + assertTrue(mngt.isManagedSubject(Subject.OPTIONAL)); + assertTrue(mngt.isManagedSubjectEnforced(Subject.OPTIONAL)); + } + + /** + * Tests that transitive (non-root) management produces advised (not enforced) results + * for scope and optional. + */ + @Test + void testTransitiveEnforcementFromNonRoot() { + DependencyManager manager = new TransitiveDependencyManager(null); + + // depth=1: no managed dependencies at root level + manager = manager.deriveChildManager(newContext()); + + // depth=2: managed dependencies introduced at transitive level + manager = manager.deriveChildManager(newContext( + new Dependency(A2, null, null), new Dependency(B, null, true), new Dependency(C1, "newscope", null))); + + // depth=3: management is applied — check enforcement flags + manager = manager.deriveChildManager(newContext()); + DependencyManagement mngt = manager.manageDependency(new Dependency(A1, null)); + assertNotNull(mngt); + assertEquals(A2.getVersion(), mngt.getVersion()); + // version management from non-root should NOT be enforced (advised) + assertTrue(mngt.isManagedSubject(Subject.VERSION)); + assertFalse(mngt.isManagedSubjectEnforced(Subject.VERSION)); + + mngt = manager.manageDependency(new Dependency(C1, null)); + assertNotNull(mngt); + assertEquals("newscope", mngt.getScope()); + // scope management from non-root should NOT be enforced (advised) + assertTrue(mngt.isManagedSubject(Subject.SCOPE)); + assertFalse(mngt.isManagedSubjectEnforced(Subject.SCOPE)); + + mngt = manager.manageDependency(new Dependency(B1, null)); + assertNotNull(mngt); + assertEquals(Boolean.TRUE, mngt.getOptional()); + // optional management from non-root should NOT be enforced (advised) + assertTrue(mngt.isManagedSubject(Subject.OPTIONAL)); + assertFalse(mngt.isManagedSubjectEnforced(Subject.OPTIONAL)); + } + + /** + * Tests that classic dependency manager also produces enforced results from root. + */ + @Test + void testClassicEnforcementFromRoot() { + DependencyManager manager = new ClassicDependencyManager(null); + + // depth=1: derive from root + manager = manager.deriveChildManager( + newContext(new Dependency(A2, null, null), new Dependency(C1, "newscope", null))); + + // depth=2: management is applied + manager = manager.deriveChildManager(newContext()); + DependencyManagement mngt = manager.manageDependency(new Dependency(A1, null)); + assertNotNull(mngt); + assertTrue(mngt.isManagedSubjectEnforced(Subject.VERSION)); + + mngt = manager.manageDependency(new Dependency(C1, null)); + assertNotNull(mngt); + assertTrue(mngt.isManagedSubjectEnforced(Subject.SCOPE)); + } + + /** + * Tests backwards compatibility: setManagedBits maps to isManagedSubject/isManagedSubjectEnforced. + */ + @Test + void testSetManagedBitsBackwardsCompat() { + DefaultDependencyNode node = new DefaultDependencyNode(new Dependency(A1, "compile")); + + // Set via deprecated API + node.setManagedBits(DependencyNode.MANAGED_VERSION | DependencyNode.MANAGED_SCOPE); + + // Check via new API — all mapped as enforced + assertTrue(node.isManagedSubject(Subject.VERSION)); + assertTrue(node.isManagedSubjectEnforced(Subject.VERSION)); + assertTrue(node.isManagedSubject(Subject.SCOPE)); + assertTrue(node.isManagedSubjectEnforced(Subject.SCOPE)); + + // Unset subjects + assertFalse(node.isManagedSubject(Subject.OPTIONAL)); + assertFalse(node.isManagedSubjectEnforced(Subject.OPTIONAL)); + assertFalse(node.isManagedSubject(Subject.PROPERTIES)); + assertFalse(node.isManagedSubject(Subject.EXCLUSIONS)); + + // Round-trip: getManagedBits still works + assertEquals(DependencyNode.MANAGED_VERSION | DependencyNode.MANAGED_SCOPE, node.getManagedBits()); + } + + /** + * Tests that setManagedSubjects with explicit enforcement flags works correctly. + */ + @Test + void testSetManagedSubjectsWithEnforcement() { + DefaultDependencyNode node = new DefaultDependencyNode(new Dependency(A1, "compile")); + + EnumMap subjects = new EnumMap<>(Subject.class); + subjects.put(Subject.VERSION, true); // enforced + subjects.put(Subject.SCOPE, false); // advised + subjects.put(Subject.OPTIONAL, false); // advised + node.setManagedSubjects(subjects); + + // All are managed + assertTrue(node.isManagedSubject(Subject.VERSION)); + assertTrue(node.isManagedSubject(Subject.SCOPE)); + assertTrue(node.isManagedSubject(Subject.OPTIONAL)); + assertFalse(node.isManagedSubject(Subject.PROPERTIES)); + + // Only version is enforced + assertTrue(node.isManagedSubjectEnforced(Subject.VERSION)); + assertFalse(node.isManagedSubjectEnforced(Subject.SCOPE)); + assertFalse(node.isManagedSubjectEnforced(Subject.OPTIONAL)); + assertFalse(node.isManagedSubjectEnforced(Subject.PROPERTIES)); + + // getManagedBits still reports all managed subjects (regardless of enforcement) + assertEquals( + DependencyNode.MANAGED_VERSION | DependencyNode.MANAGED_SCOPE | DependencyNode.MANAGED_OPTIONAL, + node.getManagedBits()); + } + + /** + * Tests that setManagedSubjects with null clears all managed subjects. + */ + @Test + void testSetManagedSubjectsNull() { + DefaultDependencyNode node = new DefaultDependencyNode(new Dependency(A1, "compile")); + + EnumMap subjects = new EnumMap<>(Subject.class); + subjects.put(Subject.VERSION, true); + node.setManagedSubjects(subjects); + assertTrue(node.isManagedSubject(Subject.VERSION)); + + // Clear + node.setManagedSubjects(null); + assertFalse(node.isManagedSubject(Subject.VERSION)); + assertEquals(0, node.getManagedBits()); + } } diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java index 9d8fca505f..a9bb31fa44 100644 --- a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java +++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverTest.java @@ -20,11 +20,13 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.EnumMap; import java.util.List; import java.util.stream.Stream; import org.eclipse.aether.RepositoryException; import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.DependencyManagement; import org.eclipse.aether.graph.DefaultDependencyNode; import org.eclipse.aether.graph.Dependency; import org.eclipse.aether.graph.DependencyNode; @@ -522,6 +524,84 @@ void winnerCycleRemoved(ConflictResolver conflictResolver) throws RepositoryExce } } + @ParameterizedTest + @MethodSource("conflictResolverSource") + void enforcedScopeNotDerived(ConflictResolver conflictResolver) throws RepositoryException { + // Root -> Parent (test) -> Child (compile, scope enforced) + // Scope derivation would normally narrow Child's scope to "test" (from parent), + // but enforced scope should prevent that. + DependencyNode root = makeDependencyNode("some-group", "root", "1.0"); + DependencyNode parent = makeDependencyNode("some-group", "parent", "1.0", "test"); + DefaultDependencyNode child = + (DefaultDependencyNode) makeDependencyNode("some-group", "child", "1.0", "compile"); + EnumMap enforcedScope = + new EnumMap<>(DependencyManagement.Subject.class); + enforcedScope.put(DependencyManagement.Subject.SCOPE, true); + child.setManagedSubjects(enforcedScope); + root.setChildren(mutableList(parent)); + parent.setChildren(mutableList(child)); + + transform(conflictResolver, root); + assertEquals("compile", child.getDependency().getScope(), "Enforced scope should not be derived from parent"); + } + + @ParameterizedTest + @MethodSource("conflictResolverSource") + void advisedScopeIsDerived(ConflictResolver conflictResolver) throws RepositoryException { + // Root -> Parent (test) -> Child (compile, scope advised) + // Scope derivation should narrow Child's scope to "test" because it's only advised. + DependencyNode root = makeDependencyNode("some-group", "root", "1.0"); + DependencyNode parent = makeDependencyNode("some-group", "parent", "1.0", "test"); + DefaultDependencyNode child = + (DefaultDependencyNode) makeDependencyNode("some-group", "child", "1.0", "compile"); + EnumMap advisedScope = new EnumMap<>(DependencyManagement.Subject.class); + advisedScope.put(DependencyManagement.Subject.SCOPE, false); + child.setManagedSubjects(advisedScope); + root.setChildren(mutableList(parent)); + parent.setChildren(mutableList(child)); + + transform(conflictResolver, root); + assertEquals("test", child.getDependency().getScope(), "Advised scope should be derived from parent"); + } + + @ParameterizedTest + @MethodSource("conflictResolverSource") + void enforcedOptionalNotDerived(ConflictResolver conflictResolver) throws RepositoryException { + // Root -> Parent (optional=true) -> Child (optional=false, optional enforced) + // Optional derivation would normally set Child to optional (from parent), + // but enforced optional should prevent that. + DependencyNode root = makeDependencyNode("some-group", "root", "1.0"); + DependencyNode parent = makeDependencyNode("some-group", "parent", "1.0"); + parent.setOptional(true); + DefaultDependencyNode child = (DefaultDependencyNode) makeDependencyNode("some-group", "child", "1.0"); + child.setOptional(false); + EnumMap enforcedOptional = + new EnumMap<>(DependencyManagement.Subject.class); + enforcedOptional.put(DependencyManagement.Subject.OPTIONAL, true); + child.setManagedSubjects(enforcedOptional); + root.setChildren(mutableList(parent)); + parent.setChildren(mutableList(child)); + + transform(conflictResolver, root); + assertFalse(child.getDependency().isOptional(), "Enforced optional should not be derived from parent"); + } + + @ParameterizedTest + @MethodSource("conflictResolverSource") + void unmanagedScopeIsDerivedAsUsual(ConflictResolver conflictResolver) throws RepositoryException { + // Root -> Parent (test) -> Child (compile, no management) + // Scope derivation should narrow Child's scope to "test" as usual (backwards compat). + DependencyNode root = makeDependencyNode("some-group", "root", "1.0"); + DependencyNode parent = makeDependencyNode("some-group", "parent", "1.0", "test"); + DependencyNode child = makeDependencyNode("some-group", "child", "1.0", "compile"); + root.setChildren(mutableList(parent)); + parent.setChildren(mutableList(child)); + + transform(conflictResolver, root); + assertEquals( + "test", child.getDependency().getScope(), "Unmanaged scope should be derived from parent as usual"); + } + private static DependencyNode makeDependencyNode(String groupId, String artifactId, String version) { return makeDependencyNode(groupId, artifactId, version, "compile"); }